AppBarLayout嵌套RecyclerView滑动冲突

424 阅读2分钟

在AppBarLayout嵌套RecyclerView时,会出现滑动抖动问题。这通常是因为AppBarLayout的fling(惯性滑动)未结束时,又滑动了RecyclerView,导致事件冲突。

以下是AppbarLayoutBehavior的解决方案,通过自定义AppBarLayout.Behavior,在手指触摸屏幕的时候停止fling事件:

package com.yuruiyin.appbarlayoutbehavior;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.OverScroller;

import androidx.coordinatorlayout.widget.CoordinatorLayout;

import com.google.android.material.appbar.AppBarLayout;

import java.lang.reflect.Field;

/**
 *  解决AppBarLayout滑动问题的自定义Behavior
 *  (1)快速滑动AppBarLayout时出现回弹
 *  (2)快速滑动AppBarLayout到折叠状态下,立马下滑会出现抖动
 *  (3)滑动AppBarLayout时,无法通过手指按下让其停止滑动
 */
public class AppBarLayoutBehavior extends AppBarLayout.Behavior {

    private static final String TAG = "CustomAppbarLayoutBehavior";
    private static final int TYPE_FLING = 1;

    private boolean isFlinging;
    private boolean shouldBlockNestedScroll;

    public AppBarLayoutBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
        shouldBlockNestedScroll = false;
        if (isFlinging) {
            shouldBlockNestedScroll = true;
        }

        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            stopAppBarLayoutFling(child);  // 手指触摸屏幕时停止fling事件
        }

        return super.onInterceptTouchEvent(parent, child, ev);
    }

    /**
     * 通过反射获取私有的flingRunnable属性,考虑到support 28以后变量名的修改
     */
    private Field getFlingRunnableField() throws NoSuchFieldException {
        try {
            Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
            return headerBehaviorType.getDeclaredField("mFlingRunnable");
        } catch (NoSuchFieldException e) {
            Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass().getSuperclass();
            return headerBehaviorType.getDeclaredField("flingRunnable");
        }
    }

    /**
     * 通过反射获取私有的scroller属性,考虑到support 28以后变量名的修改
     */
    private Field getScrollerField() throws NoSuchFieldException {
        try {
            Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
            return headerBehaviorType.getDeclaredField("mScroller");
        } catch (NoSuchFieldException e) {
            Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass().getSuperclass();
            return headerBehaviorType.getDeclaredField("scroller");
        }
    }

    /**
     * 停止AppBarLayout的fling事件
     */
    private void stopAppBarLayoutFling(AppBarLayout appBarLayout) {
        try {
            Field flingRunnableField = getFlingRunnableField();
            Field scrollerField = getScrollerField();
            flingRunnableField.setAccessible(true);
            scrollerField.setAccessible(true);

            Runnable flingRunnable = (Runnable) flingRunnableField.get(this);
            OverScroller overScroller = (OverScroller) scrollerField.get(this);
            if (flingRunnable != null) {
                appBarLayout.removeCallbacks(flingRunnable);
                flingRunnableField.set(this, null);
            }
            if (overScroller != null && !overScroller.isFinished()) {
                overScroller.abortAnimation();
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
        stopAppBarLayoutFling(child);
        return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
        if (type == TYPE_FLING) {
            isFlinging = true;
        }
        if (!shouldBlockNestedScroll) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        }
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        if (!shouldBlockNestedScroll) {
            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
        }
    }

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
        super.onStopNestedScroll(coordinatorLayout, abl, target, type);
        isFlinging = false;
        shouldBlockNestedScroll = false;
    }
}

然后在XML布局中为AppBarLayout设置自定义的layout_behavior属性:

<com.google.android.material.appbar.AppBarLayout
    android:id="@+id/appbarLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white"
    android:fitsSystemWindows="false"
    app:layout_behavior="com.yuruiyin.appbarlayoutbehavior.AppBarLayoutBehavior">
    ...
</com.google.android.material.appbar.AppBarLayout>

参考资料

通过这种方式,可以有效地解决AppBarLayout和RecyclerView嵌套滑动时的冲突和抖动问题。