AppBarLayout v26+抖动BugFix

Android v26.x support library中升级了NestedScrolling API,以此修复了CoordinatorLayout+AppBarLayout的fling卡顿的问题,戳这里看Chris大佬的文章说明。

所以我们可以仅通过布局就能实现以下效果:

期望.gif

一切看起来跟初恋一样美好,但是在实际的应用中却遇到了些问题。

一图胜千言,所以还是看图:

现实.gif

因为业务需求导致AppBarLayout高度很大,这样的情况下很容易触发一个操作:


粉色或绿色部分施加一个向上的fling,紧接着在白色列表部分施加一个向下的fling,就会出现上图这样的激烈的抖动回弹。


到这里,问题也描述的差不多了,有遇到同样问题&心急的朋友可能反手就是一拖鞋

AppBarLayout v26+抖动BugFix_第1张图片
交出代码.png

Demo在文章底部

public class FixBounceV26Behavior extends AppBarLayout.Behavior {

    private OverScroller mScroller1;

    public FixBounceV26Behavior() {
        super();
    }

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

    /**
     * 反射注入Scroller以获取其引用
     *
     * @param context
     */
    private void bindScrollerValue(Context context) {
        if (mScroller1 != null) return;
        mScroller1 = new OverScroller(context);
        try {
            Class clzHeaderBehavior = getClass().getSuperclass().getSuperclass();
            Field fieldScroller = clzHeaderBehavior.getDeclaredField("mScroller");
            fieldScroller.setAccessible(true);
            fieldScroller.set(this, mScroller1);
        } catch (Exception e) {}
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
        if (type == ViewCompat.TYPE_NON_TOUCH) {
            //fling上滑appbar然后迅速fling下滑list时, HeaderBehavior的mScroller并未停止, 会导致上下来回晃动
            if (mScroller1.computeScrollOffset()) {
                mScroller1.abortAnimation();
            }
            //当target滚动到边界时主动停止target fling,与下一次滑动产生冲突
            if (getTopAndBottomOffset() == 0) {
                ViewCompat.stopNestedScroll(target, type);
            }
        }
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
    }
}

这就完了?
是的。


上面的代码已经能简单地解决问题了,想知道的更详细的话可以往下看看。


AppBarLayout v26+抖动BugFix_第2张图片
我就知道你是喜欢我的.png
  • Round 1
    CoordinatorLayout和子View的联动时通过CoordinatorLayout.Behavior实现的,AppBarLayout使用的Behavior继承了HeaderBehavior
    问题就在这里。
    HeaderBehavior的onTouchEvent中使用Scroller实现了fling操作,但是没有通过NestedScrolling API对外开放,也就说一旦HeaderBehavior的fling动作形成,无法由外部主动中断。
@Override
    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
       switch (ev.getActionMasked()) {
            //剔除了多余部分
            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;
    }

final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset,
            int maxOffset, float velocityY) {
        if (mFlingRunnable != null) {
            layout.removeCallbacks(mFlingRunnable);
            mFlingRunnable = null;
        }

        if (mScroller == null) {
            mScroller = new OverScroller(layout.getContext());
        }

        mScroller.fling(
                0, getTopAndBottomOffset(), // curr
                0, Math.round(velocityY), // velocity.
                0, 0, // x
                minOffset, maxOffset); // y

        if (mScroller.computeScrollOffset()) {
            mFlingRunnable = new FlingRunnable(coordinatorLayout, layout);
            ViewCompat.postOnAnimation(layout, mFlingRunnable);
            return true;
        } else {
            onFlingFinished(coordinatorLayout, layout);
            return false;
        }
    }
  • Round 2
    与AppBarLayout同层级的RecyclerView可以通过升级过的NestedScrolling API对AppBarLayout产生影响,比如RecyclerView向下fling时滑动到item 0之后,如果AppBarLayout可以滑动时会给AppBarLayout施加一个同样向下的fling动作,以此形成一个连贯的下滑fling。
    那么问题来了。
    当HeaderBehavior产生的向上的fling没有结束时,RecyclerView又送来向下的fling,抖动就产生了。


    AppBarLayout v26+抖动BugFix_第3张图片
    我可是讲道理的人.png
AppBarLayout v26+抖动BugFix_第4张图片
那我们继续了.png
  • Round 3
    既然知道了问题所在,那么我们就来解决问题。
    看一哈HeaderBehavior的代码发现mScroller没有提供对外的调用接口而且使用默认的修饰符,但是有一个好消息就是mScroller是在fling()方法中通过判空来初始化的并且没有重置mScroller的操作。
    所以我们可以通过子类继承之后在构建时就为mScroller注入实例,这样我们就可以获得mScroller的引用了。
    然后在RecyclerView产生fling时主动中断mScroller.fling。
    /**
     * 反射注入Scroller以获取其引用
     *
     * @param context
     */
    private void bindScrollerValue(Context context) {
        if (mScroller1 != null) return;
        mScroller1 = new OverScroller(context);
        try {
            //android.support.design.widget.HeaderBehavior
            Class clzHeaderBehavior = getClass().getSuperclass().getSuperclass();
            Field fieldScroller = clzHeaderBehavior.getDeclaredField("mScroller");
            fieldScroller.setAccessible(true);
            fieldScroller.set(this, mScroller1);
        } catch (Exception e) {}
    }
    
    /**
      * 来自RecyclerView的fling会触发NestedScrolling API
      */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
        if (type == ViewCompat.TYPE_NON_TOUCH) {
            //fling上滑appbar然后迅速fling下滑list时, HeaderBehavior的mScroller并未停止, 会导致上下来回晃动
            if (mScroller1.computeScrollOffset()) {
                mScroller1.abortAnimation();
            }
            //当target滚动到边界时主动停止target fling,与下一次滑动产生冲突
            if (getTopAndBottomOffset() == 0) {
                ViewCompat.stopNestedScroll(target, type);
            }
        }
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
    }

最后把写好的Behavior应用到AppBarLayout上就OK了。


代码很少,实现方式也很简单,主要是分享下解决此类问题的一种思路,希望能对大家有所帮助。

Demo传送阵

你可能感兴趣的:(AppBarLayout v26+抖动BugFix)