记录一次CoordinatorLayout在support-compat27下滑动的问题

这里记录一下在support-compat27包中主要发现了两个滑动时候的问题。

首先看下xml文件:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    android:id="@+id/testscor"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity">

    <com.sogou.testforall.CustomCoordinatorLayout
        android:id="@+id/coord"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <android.support.design.widget.AppBarLayout
            android:id="@+id/appbar"
            android:layout_width="match_parent"
            android:layout_height="400dp"
            android:orientation="vertical"
            app:layout_behavior="com.sogou.testforall.CustomBehavior">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical"
                app:layout_scrollFlags="scroll">

            </LinearLayout>
        </android.support.design.widget.AppBarLayout>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rec"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

        </android.support.v7.widget.RecyclerView>

    </com.sogou.testforall.CustomCoordinatorLayout>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/btn1"
        android:text="打开滑动问题一"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:id="@+id/btn2"
        android:text="打开滑动问题二"/>
</FrameLayout>

主要是以CoordinatorLayout+AppBarLayout+RecyclerView的方式呈现滑动嵌套的布局方式。在使用当前布局的时候主要遇到了两个滑动时候的问题,下面依次介绍。


问题一

该问题的复现场景描述为:触摸AppBarLayout手指向上滑动,即布局向下移动,当进行fling时候,手指向下滑动RecyclerView,就会造成滑动的问题。可以看下下面的gif图:

造成这个的原因主要是AppBarLayout的fling操作和NestedScrollView联动造成的问题,关于源码的分析可以看我写的文章:

在AppBarLayout的Behavior中的onTouchEvent()事件中处理了fling事件:


    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
		...
            case MotionEvent.ACTION_UP:
                if (mVelocityTracker != null) {
                    mVelocityTracker.addMovement(ev);
                    mVelocityTracker.computeCurrentVelocity(1000);
                    float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
                }
		...
        return true;
    }

在fling的方法中使用OverScroller来模拟进行fling操作,最终会调到setHeaderTopBottomOffset(...)来使AppBarLayout进行fling的滑动操作。在绝大部分滑动逻辑中,这样处理是正确的,但是如果在AppBarLayout在fling的时候主动滑动RecyclerView,那么就会造成动画抖动的问题了。

在当前情况下,RecyclerView滑动到头了,那么就会把未消费的事件通过NestedScrollingChild2交付由CoordinatorLayout(实现了NestedScrollingParent)处理,parent又最终交付由AppBarLayout.Behavior进行处理的,其中调用的方法如下:

        @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
                int type) {
            if (dyUnconsumed < 0) {
                // If the scrolling view is scrolling down but not consuming, it's probably be at
                // the top of it's content
                scroll(coordinatorLayout, child, dyUnconsumed,
                        -child.getDownNestedScrollRange(), 0);
            }
        }

这里的scroll方法最终会调用setHeaderTopBottomOffset(...),由于两次分别触摸在AppBarLayout和RecyclerView的方向不一致,导致了最终的抖动的效果。

解决方式也很简单,只要在CoordinatorLayout的onInterceptedTouchEvent()中停止AppBarLayout的fling操作就可以了,直接操作的对象就是AppBarLayout中的Behavior,该Behavior继承自HeaderBehavior,而fling操作由OverScroller产生,所以自定义一个CustomBehavior:

扫描二维码关注公众号,回复: 4368285 查看本文章
public class CustomBehavior extends AppBarLayout.Behavior {
    private OverScroller mOverScroller;

    public CustomBehavior() {
        super();
    }

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

    @Override
    public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams params) {
        super.onAttachedToLayoutParams(params);
    }

    @Override
    public void onDetachedFromLayoutParams() {
        super.onDetachedFromLayoutParams();
    }

    @Override
    public boolean onTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            reflectOverScroller();
        }
        return super.onTouchEvent(parent, child, ev);
    }

    /**
     *
     */
    public void stopFling() {
        if (mOverScroller != null) {
            mOverScroller.abortAnimation();
        }
    }

    /**
     * 解决AppbarLayout在fling的时候,再主动滑动RecyclerView导致的动画错误的问题
     */
    private void reflectOverScroller() {
        if (mOverScroller == null) {
            Field field = null;
            try {
                field = getClass().getSuperclass()
                        .getSuperclass().getDeclaredField("mScroller");
                field.setAccessible(true);
                Object object = field.get(this);
                mOverScroller = (OverScroller) object;
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

        }
    }
}

然后在重写CoordinatorLayout,暴露一个接口:

public class CustomCoordinatorLayout extends CoordinatorLayout {
    private OnInterceptTouchListener mListener;

    public void setOnInterceptTouchListener(OnInterceptTouchListener listener) {
        mListener = listener;
    }

    public CustomCoordinatorLayout(Context context) {
        super(context);
    }

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

    public CustomCoordinatorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (mListener != null) {
            mListener.onIntercept();
        }
        return super.onInterceptTouchEvent(ev);
    }


    public interface OnInterceptTouchListener {
        void onIntercept();
    }
}

接着在接口中处理滑动问题即可:

 val customCoordinatorLayout = findViewById<CustomCoordinatorLayout>(R.id.coord)
        customCoordinatorLayout.setOnInterceptTouchListener {
            //RecyclerView滑动的时候禁止AppBarLayout的滑动
            if (customBehavior != null && !flagOne) {
                customBehavior!!.stopFling()
            }
        }

问题二

第二个问题产生的原因跟第一个问题的操作相反,首先在RecyclerView到头的时候手指向下滑动RecyclerView,在手指离开后,再通过手指向上滑动AppBarLayout,就会造成这个问题,可以看下gif图:

可以看到手指向上滑动AppBarLayout的时候,直至AppBarLayout完全滑出屏幕,接着又反弹回到屏幕中了,这个问题造成的原因是因为在手指向上滑动后造成RecyclerView的fling操作执行,具体的代码在RecyclerView内部类ViewFlinger中。由于对RecyclerView的源码不是很熟,所以通过debug发现ViewFlinger中一直调用dispatchNestedScroll(...)方法,自然而然就通知到了CoordinatorLayout中,也就自然到了AppBarlayout.Behavior当中的onNestedScroll(...)中了。问题一也说了AppBarlayout.Behavior当中的onNestedScroll(...)会调用setHeaderTopBottomOffset(...),由于RecyclerView一直在fling导致了反弹效果的出现。

解决方式就是在CoordinatorLayout中停止RecyclerView的滑动,由于RecyclerView提供了对应的stopScroll()方法,所以直接调用即可:

 customCoordinatorLayout.setOnInterceptTouchListener {
            if (!flagTwo) {
                mRecyclerView.stopScroll()
            }
        }

猜你喜欢

转载自my.oschina.net/u/3863980/blog/2967598
今日推荐