1.AppBarLayout嵌套滑动问题
前一阵将support库版本从25.4.0升级到了27.1.1后发现了这个问题。发现RecyclerView在滑动到底部后,会有近一秒的停滞,之后再去加载下一页数据。我们知道上拉加载实现方案基本都是监听滑动状态,当滑动停止时,再去加载下一页。代码基本如下:
1@Override
2public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
3 super.onScrollStateChanged(recyclerView, newState);
4 if (newState == RecyclerView.SCROLL_STATE_IDLE) {
5 onLoadNextPage();
6 }
7}
我查看了几个有分页加载的页面,最终发现凡是使用了AppBarLayout 与 RecycleView的地方会有这种问题。那么我就写了个简单的页面来验证一下我的猜测。
页面布局的代码很普通,类似下面这种。
1<?xml version="1.0" encoding="utf-8"?>
2<android.support.design.widget.CoordinatorLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent">
7
8 <android.support.design.widget.AppBarLayout
9 app:elevation="0dp"
10 android:layout_width="match_parent"
11 android:layout_height="wrap_content">
12
13 <View
14 android:background="@color/colorAccent"
15 app:layout_scrollFlags="scroll|enterAlways"
16 android:layout_width="match_parent"
17 android:layout_height="150dp"/>
18
19 <View
20 android:background="@color/colorPrimary"
21 android:orientation="horizontal"
22 android:layout_width="match_parent"
23 android:layout_height="50dp"/>
24
25
26 </android.support.design.widget.AppBarLayout>
27
28 <android.support.v7.widget.RecyclerView
29 app:layout_behavior="@string/appbar_scrolling_view_behavior"
30 android:id="@+id/recyclerView"
31 android:layout_width="match_parent"
32 android:layout_height="match_parent"/>
33
34</android.support.design.widget.CoordinatorLayout>
我首先使用25.4.0版本,我很快的滑动了一下来看下正常的结果:
0就是滑动停止。下来就是27.1.1版本,代码什么都没有变。
好吧,2.5秒,比我感觉的时间还长。。。那么这就说明虽然滑动停止了,但其实状态还是滑动中。当然这个时间不是固定的,完全取决于你的手速。你滑动的越快这个时间越长,这不禁让我想到了惯性滑动。下来先看看27.1.1的RecyclerView是怎么样实现惯性滑动的。
惯性滑动,那么首先你要在滑动时,放手。也就是onTouchEvent方法中的 ACTION_UP:
1@Override
2 public boolean onTouchEvent(MotionEvent e) {
3 ...
4
5 switch (action) {
6 ...
7
8 case MotionEvent.ACTION_UP: {
9 mVelocityTracker.addMovement(vtev);
10 // 计算一秒时间内移动了多少个像素, mMaxFlingVelocity为速度上限(测试机为22000)
11 mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
12 final float xvel = canScrollHorizontally
13 ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
14 final float yvel = canScrollVertically
15 ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
16 // fling方法判断是否有抛动,也就是惯性滑动,如果为true,则滑动状态就不会直接为SCROLL_STATE_IDLE。
17 if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
18 setScrollState(SCROLL_STATE_IDLE);
19 }
20 resetTouch();
21 }
22 break;
23
24 }
25 ...
26 return true;
27 }
fling方法实现:
1 public boolean fling(int velocityX, int velocityY) {
2 ...
3 if (!dispatchNestedPreFling(velocityX, velocityY)) {
4 final boolean canScroll = canScrollHorizontal || canScrollVertical;
5 dispatchNestedFling(velocityX, velocityY, canScroll);
6
7 if (canScroll) {
8 int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
9 if (canScrollHorizontal) {
10 nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
11 }
12 if (canScrollVertical) {
13 nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
14 }
15 startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);
16
17 velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
18 velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
19 // 核心在这里,将计算出的最大速度传入ViewFlinger来实现滚动
20 mViewFlinger.fling(velocityX, velocityY);
21 return true;
22 }
23 }
24 return false;
25 }
ViewFlinger代码很多,我精简一下:
1 static final Interpolator sQuinticInterpolator = new Interpolator() {
2 @Override
3 public float getInterpolation(float t) {
4 t -= 1.0f;
5 return t * t * t * t * t + 1.0f;
6 }
7 };
8
9 class ViewFlinger implements Runnable {
10
11 private OverScroller mScroller;
12 Interpolator mInterpolator = sQuinticInterpolator;
13
14 ViewFlinger() {
15 mScroller = new OverScroller(getContext(), sQuinticInterpolator);
16 }
17
18 @Override
19 public void run() {
20
21 final OverScroller scroller = mScroller;
22 // 判断是否完成了整个滑动
23 if (scroller.computeScrollOffset()) {
24
25 if (dispatchNestedPreScroll(dx, dy, scrollConsumed, null, TYPE_NON_TOUCH)) {}
26
27 if (!dispatchNestedScroll(hresult, vresult, overscrollX, overscrollY, null, TYPE_NON_TOUCH){}
28
29 if (scroller.isFinished()) {
30 // 惯性滑动结束,状态设为SCROLL_STATE_IDLE
31 setScrollState(SCROLL_STATE_IDLE);
32 stopNestedScroll(TYPE_NON_TOUCH);
33 }
34 }
35 }
36
37 // 惯性滑动,状态设为SCROLL_STATE_SETTLING
38 public void fling(int velocityX, int velocityY) {
39 setScrollState(SCROLL_STATE_SETTLING);
40 mScroller.fling(0, 0, velocityX, velocityY,
41 Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
42 }
43 ...
44
45 }
sQuinticInterpolator插值器是惯性滑动时间与距离的曲线,大致如下(速度先快后慢):
OverScroller中的fling方法,可以通过传入的速度值,计算出需要滑动的距离与时间。速度越大,对应的值就越大。 我的测试机最大速为22000,所以计算出的最长时间是 2544ms。这个也符合我们一开始打印出的信息。计算方法有兴趣的可以去看看源码一探究竟。
说了这么多,问题到底在哪?我对比了一下两版本的ViewFlinger 代码部分。
发现在25.4.0中并没有dispatchNestedPreScroll、dispatchNestedScroll 、hasNestedScrollingParent,stopNestedScroll这部分代码。其实这部分的作用是为了解决一个滑动不同步的bug。如下图:(图传上来有点。。。详细可以参看:对design库中AppBarLayout嵌套滚动问题的修复)
简单的描述一下问题原因:RecyclerView 在 fling 过程中并没有通知AppBarLayout,所以在fling结束之后,AppBarLayout不知道当前RecyclerView的滑动到的位置,所以导致了这个滑动被打断的问题。其实相关的滑动卡顿问题,病因都是这里。
所以在26+开始修复了这个问题,也就是上面看到的变化。不过新问题也就诞生了,就是我一开始提到的停滞问题。问题出在了hasNestedScrollingParent这个方法,判断是父View是否支持嵌套滑动 。显然在这个嵌套滑动场景始终是支持嵌套滑动,所以在判断中只有当滑动完成后才能在onScrollStateChanged收到 SCROLL_STATE_IDLE状态。
if (scroller.isFinished() ||
(!fullyConsumedAny && !true)) {}
—>
if (scroller.isFinished() || false) {}
这也就是在25.4.0版本和无AppBarLayout嵌套滑动的情况下,没有相关问题的原因。
2.解决方法
知道了原因,怎么去解决呢?
1. 升级版本
升级到28.0.0以上,以上问题一并解决。我看了一下当前最新的28.0.0-rc02版本,发现针对这个问题官方做了修改。我们对比一下:
27.1.1
28.0.0-rc02
可以看到添加了stopNestedScrollIfNeeded方法,在向上滑动到顶和向下滑动到底时,停止view的滚动。
2. 思路借鉴
如果你是26 和 28 之间 ,可以参考官方解决的思路
1public class FixAppBarLayoutBehavior extends AppBarLayout.Behavior {
2
3 public FixAppBarLayoutBehavior() {
4 super();
5 }
6
7 public FixAppBarLayoutBehavior(Context context, AttributeSet attrs) {
8 super(context, attrs);
9 }
10
11 @Override
12 public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
13 View target, int dx, int dy, int[] consumed, int type) {
14 super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
15 stopNestedScrollIfNeeded(dy, child, target, type);
16 }
17
18 @Override
19 public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target,
20 int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
21 super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
22 stopNestedScrollIfNeeded(dyUnconsumed, child, target, type);
23 }
24
25 private void stopNestedScrollIfNeeded(int dy, AppBarLayout child, View target, int type) {
26 if (type == ViewCompat.TYPE_NON_TOUCH) {
27 final int currOffset = getTopAndBottomOffset();
28 if ((dy < 0 && currOffset == 0) || (dy > 0 && currOffset == -child.getTotalScrollRange())) {
29 ViewCompat.stopNestedScroll(target, ViewCompat.TYPE_NON_TOUCH);
30 }
31 }
32 }
33}
使用:
1 <android.support.design.widget.AppBarLayout
2 ...
3 app:layout_behavior="yourPackage.FixAppBarLayoutBehavior">
或:
1 AppBarLayout mAppBarLayout = findViewById(R.id.app_bar);
2((CoordinatorLayout.LayoutParams) mAppBarLayout.getLayoutParams()).setBehavior(new FixAppBarLayoutBehavior());
3.其他
如果你是26以下的版本,那么建议还是升级到26以上吧!毕竟官方已经解决了这个问题。为此升级了NestedScrollingParent2 和NestedScrollingChild2接口,添加了NestedScrollType用来区分是手动触发的滑动还是非手动(惯性)触发的滑动。
为什么不从RecyclerView下手解决呢?我想了想道理和滑动冲突类似,有外部拦截、内部拦截。将主动权交给父类,比较合理,处理起来更加灵活方便。
喜欢 就关注吧,欢迎投稿!
作者:唯鹿
来源:CSDN
原文:https://blog.csdn.net/qq_17766199/article/details/82561216?utm_source=copy
版权声明:本文为博主原创文章,转载请附上博文链接!
如有任何疑问可在文章底部留言。为了防止恶意评论,本博客现已开启留言审核功能。但是博主会在后台第一时间看到您的留言,并会在第一时间对您的留言进行回复!欢迎交流!
本文链接: https://leetcode.jp/appbarlayout各版本问题探究及解决/