1. 程式人生 > >記錄一次CoordinatorLayout在support-compat27下滑動的問題

記錄一次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:

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()
            }
        }