AppBarLayout v26+抖动BugFix

作者: Hello丶Kay | 来源:发表于2018-02-06 14:29 被阅读260次

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

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

    期望.gif

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

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

    现实.gif

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


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


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

    交出代码.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);
        }
    }
    

    这就完了?
    是的。


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


    我就知道你是喜欢我的.png
    • Round 1
      CoordinatorLayout和子View的联动时通过CoordinatorLayout.Behavior实现的,AppBarLayout使用的Behavior继承了HeaderBehavior<AppBarLayout>。
      问题就在这里。
      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,抖动就产生了。


      我可是讲道理的人.png
    那我们继续了.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了。

    <android.support.design.widget.AppBarLayout
            android:id="@+id/layout_appbar"
            android:layout_width="match_parent"
            app:layout_behavior="hello.kay.chestnut.fix.FixBounceV26Behavior"
            android:layout_height="wrap_content">
    

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

    Demo传送阵

    相关文章

      网友评论

      本文标题:AppBarLayout v26+抖动BugFix

      本文链接:https://www.haomeiwen.com/subject/sbawzxtx.html