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

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

首先看下xml文件:android

<?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的方式呈现滑动嵌套的布局方式。在使用当前布局的时候主要遇到了两个滑动时候的问题,下面依次介绍。app


问题一

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

形成这个的缘由主要是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进行处理的,其中调用的方法以下:this

@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的方向不一致,致使了最终的抖动的效果。spa

解决方式也很简单,只要在CoordinatorLayout的onInterceptedTouchEvent()中中止AppBarLayout的fling操做就能够了,直接操做的对象就是AppBarLayout中的Behavior,该Behavior继承自HeaderBehavior,而fling操做由OverScroller产生,因此自定义一个CustomBehavior:.net

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()
            }
        }
相关文章
相关标签/搜索