这里记录一下在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()
}
}