記錄一次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聯動造成的問題,關於原始碼的分析可以看我寫的文章:
- CoordinatorLayout三部曲學習之一:Nest介面的實現
- CoordinatorLayout三部曲學習之二:CoordinateLayout原始碼學習
- CoordinatorLayout三部曲學習之三:AppBarLayout聯動原始碼學習
在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()
}
}