AppBarLayout各版本问题探究及解决

阅读 1423
收藏 75
2018-09-11
原文链接:blog.csdn.net

1.AppBarLayout嵌套滑动问题

前一阵将support库版本从25.4.0升级到了27.1.1后发现了这个问题。发现RecyclerView在滑动到底部后,会有近一秒的停滞,之后再去加载下一页数据。我们知道上拉加载实现方案基本都是监听滑动状态,当滑动停止时,再去加载下一页。代码基本如下:

@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
     super.onScrollStateChanged(recyclerView, newState);
     if (newState == RecyclerView.SCROLL_STATE_IDLE) {
         onLoadNextPage();
     }
}

我查看了几个有分页加载的页面,最终发现凡是使用了AppBarLayoutRecycleView的地方会有这种问题。那么我就写了个简单的页面来验证一下我的猜测。

页面布局的代码很普通,类似下面这种。

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        app:elevation="0dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <View
            android:background="@color/colorAccent"
            app:layout_scrollFlags="scroll|enterAlways"
            android:layout_width="match_parent"
            android:layout_height="150dp"/>

        <View
            android:background="@color/colorPrimary"
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="50dp"/>


    </android.support.design.widget.AppBarLayout>

    <android.support.v7.widget.RecyclerView
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</android.support.design.widget.CoordinatorLayout>

我首先使用25.4.0版本,我很快的滑动了一下来看下正常的结果:

这里写图片描述

0就是滑动停止。下来就是27.1.1版本,代码什么都没有变。

这里写图片描述

好吧,2.5秒,比我感觉的时间还长。。。那么这就说明虽然滑动停止了,但其实状态还是滑动中。当然这个时间不是固定的,完全取决于你的手速。你滑动的越快这个时间越长,这不禁让我想到了惯性滑动。下来先看看27.1.1的RecyclerView是怎么样实现惯性滑动的。

惯性滑动,那么首先你要在滑动时,放手。也就是onTouchEvent方法中的 ACTION_UP

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        ...

        switch (action) {
            ...

            case MotionEvent.ACTION_UP: {
                mVelocityTracker.addMovement(vtev);
                // 计算一秒时间内移动了多少个像素, mMaxFlingVelocity为速度上限(测试机为22000)
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                final float xvel = canScrollHorizontally
                        ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically
                        ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                // fling方法判断是否有抛动,也就是惯性滑动,如果为true,则滑动状态就不会直接为SCROLL_STATE_IDLE。        
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);
                }
                resetTouch();
            } 
            break;

        }
        ...
        return true;
    }

fling方法实现:

    public boolean fling(int velocityX, int velocityY) {
        ...
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            dispatchNestedFling(velocityX, velocityY, canScroll);

            if (canScroll) {
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontal) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertical) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);

                velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
                // 核心在这里,将计算出的最大速度传入ViewFlinger来实现滚动
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

ViewFlinger代码很多,我精简一下:

 static final Interpolator sQuinticInterpolator = new Interpolator() {
     @Override
     public float getInterpolation(float t) {
         t -= 1.0f;
         return t * t * t * t * t + 1.0f;
     }
 };

 class ViewFlinger implements Runnable {

        private OverScroller mScroller;
        Interpolator mInterpolator = sQuinticInterpolator;

        ViewFlinger() {
            mScroller = new OverScroller(getContext(), sQuinticInterpolator);
        }

        @Override
        public void run() {

            final OverScroller scroller = mScroller;
            // 判断是否完成了整个滑动
            if (scroller.computeScrollOffset()) {

                if (dispatchNestedPreScroll(dx, dy, scrollConsumed, null, TYPE_NON_TOUCH)) {}

                if (!dispatchNestedScroll(hresult, vresult, overscrollX, overscrollY, null, TYPE_NON_TOUCH){}

                if (scroller.isFinished()) {
                    // 惯性滑动结束,状态设为SCROLL_STATE_IDLE
                    setScrollState(SCROLL_STATE_IDLE);
                    stopNestedScroll(TYPE_NON_TOUCH);
                }
            }     
        }

        // 惯性滑动,状态设为SCROLL_STATE_SETTLING
        public void fling(int velocityX, int velocityY) {
            setScrollState(SCROLL_STATE_SETTLING);
            mScroller.fling(0, 0, velocityX, velocityY,
                    Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        }
      ...

    }

sQuinticInterpolator插值器是惯性滑动时间与距离的曲线,大致如下(速度先快后慢):

这里写图片描述

OverScroller中的fling方法,可以通过传入的速度值,计算出需要滑动的距离与时间。速度越大,对应的值就越大。 我的测试机最大速为22000,所以计算出的最长时间是 2632ms。这个也符合我们一开始打印出的信息。计算方法有兴趣的可以去看看源码一探究竟。

说了这么多,问题到底在哪?我对比了一下两版本的ViewFlinger 代码部分。

这里写图片描述

这里写图片描述

这里写图片描述

发现在25.4.0中并没有dispatchNestedPreScrolldispatchNestedScrollhasNestedScrollingParentstopNestedScroll这部分代码。其实这部分的作用是为了解决一个滑动不同步的bug。如下图:(图传上来有点。。。详细可以参看:对design库中AppBarLayout嵌套滚动问题的修复

这里写图片描述

简单的描述一下问题原因:RecyclerViewfling 过程中并没有通知AppBarLayout,所以在fling结束之后,AppBarLayout不知道当前RecyclerView的滑动到的位置,所以导致了这个滑动被打断的问题。其实相关的滑动卡顿问题,病因都是这里。

所以在26+开始修复了这个问题,也就是上面看到的变化。不过新问题也就诞生了,就是我一开始提到的停滞问题。问题出在了hasNestedScrollingParent这个方法,判断是父View是否支持嵌套滑动 。显然在这个嵌套滑动场景始终是有父View,所以在判断中只有当滑动完成后才能收到 SCROLL_STATE_IDLE

if (scroller.isFinished() || (!fullyConsumedAny && !true)) {}

这也就是在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 之间 ,可以参考官方解决的思路

public class FixAppBarLayoutBehavior extends AppBarLayout.Behavior {

    public FixAppBarLayoutBehavior() {
        super();
    }

    public FixAppBarLayoutBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
            View target, int dx, int dy, int[] consumed, int type) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        stopNestedScrollIfNeeded(dy, child, target, type);
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target,
                               int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
        stopNestedScrollIfNeeded(dyUnconsumed, child, target, type);
    }

    private void stopNestedScrollIfNeeded(int dy, AppBarLayout child, View target, int type) {
        if (type == ViewCompat.TYPE_NON_TOUCH) {
            final int currOffset = getTopAndBottomOffset();
            if ((dy < 0 && currOffset == 0) || (dy > 0 && currOffset == -child.getTotalScrollRange())) {
                ViewCompat.stopNestedScroll(target, ViewCompat.TYPE_NON_TOUCH);
            }
        }
    }
}

使用:

    <android.support.design.widget.AppBarLayout
            ...
            app:layout_behavior="yourPackage.FixAppBarLayoutBehavior">

或:

AppBarLayout mAppBarLayout = findViewById(R.id.app_bar);
((CoordinatorLayout.LayoutParams) mAppBarLayout.getLayoutParams()).setBehavior(new FixAppBarLayoutBehavior());

3.其他

  1. 如果你是26以下的版本,那么建议还是升级到26以上吧!毕竟官方已经解决了这个问题。为此升级了NestedScrollingParent2NestedScrollingChild2接口,添加了NestedScrollType用来区分是手动触发的滑动还是非手动(惯性)触发的滑动。

  2. 为什么不从RecyclerView下手解决呢?我想了想道理和滑动冲突类似,有外部拦截、内部拦截。将主动权交给父类,比较合理,处理起来更加灵活方便。

3.参考

评论