AppBarLayout各版本问题探究及解决

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(00, 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下手解决呢?我想了想道理和滑动冲突类似,有外部拦截、内部拦截。将主动权交给父类,比较合理,处理起来更加灵活方便。

                        喜欢 就关注吧,欢迎投稿!

640?wx_fmt=jpeg

作者:唯鹿 

来源:CSDN 

原文:https://blog.csdn.net/qq_17766199/article/details/82561216?utm_source=copy 

版权声明:本文为博主原创文章,转载请附上博文链接!

本网站文章均为原创内容,并可随意转载,但请标明本文链接
如有任何疑问可在文章底部留言。为了防止恶意评论,本博客现已开启留言审核功能。但是博主会在后台第一时间看到您的留言,并会在第一时间对您的留言进行回复!欢迎交流!
本文链接: https://leetcode.jp/appbarlayout各版本问题探究及解决/

此条目发表在Android分类目录。将固定链接加入收藏夹。

发表评论

您的电子邮箱地址不会被公开。