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各版本问题探究及解决/