美文网首页Android开发Android知识Android技术知识
通俗易懂的小例子来演示如何使用NestedScroll

通俗易懂的小例子来演示如何使用NestedScroll

作者: Jason__Ding | 来源:发表于2016-10-27 18:17 被阅读703次

    写在前面

    最近遇到了一个问题,在SwipeRefreshLayout中,有时候下拉,圆球不会下来,等松开手指的时候,球会突然闪一下,不明所以。想到这个应该是滑动相关的问题,而且跟嵌套滑动似乎很有关联,我们看,public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingParent,NestedScrollingChild,可以看出SwipeRefreshLayout 即实现了NestedScrollingParent也实现了NestedScrollingChild,那先从这个角度着手,看看NestedScroll是个什么玩意儿。

    学习一个

    先来看看这两篇文章

    这里摘抄几句关于NestedScrollingChild比较重要的:

    需要做的就是,如果要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用 startNestedScroll()。你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用 dispatchNestedPreScroll()。如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用 dispatchNestedScroll()

    关于NestedScrollingParent的:

    从上面的 Child 分析可知,滑动开始的调用 startNestedScroll(),Parent 收到 onStartNestedScroll()回调,决定是否需要配合 Child 一起进行处理滑动,如果需要配合,还会回调 onNestedScrollAccepted()
    每次滑动前,Child 先询问 Parent 是否需要滑动,即 dispatchNestedPreScroll(),这就回调到 Parent 的 onNestedPreScroll(),Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。
    Child 滑动以后,会调用 onNestedScroll(),回调到 Parent 的 onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。
    最后,滑动结束,调用 onStopNestedScroll()表示本次处理结束。

    下面的内容是假定大家已经把上面两篇文章看完了。

    我的例子

    其实上面两篇文章已经写明白了,但有点不足的是,没有一个通俗易懂的例子来演示。所以如果各位还不是太清楚的话,可以通过下面的例子来理解。

    先来看一个图。


    嵌套滑动效果

    这是一整次的滑动,橙色的为子View,蓝色的为父View。我们将子View往上滑的时候,先是父View带着子View一起向上滑动,等父View到了顶之后,子View开始滑动。

    大概的原理是,滑动事件在子View中的时候,先让父View进行滑动的处理,然后子View去处理未被父View消费的距离。

    在代码中是这么处理的。
    1. 首先,子View是肯定需要实现NestedScrollingChild的,然后重写onTouchEvent方法,。。。
    2.

    得,不解释了。Talk is plain. Show you the codes.

    下面是子View的实现。

    
    public class NestedChildView extends View implements NestedScrollingChild {
    
        public static final String TAG = "NestedChildView";
    
        private final NestedScrollingChildHelper childHelper = new NestedScrollingChildHelper(this);
        private float downY;
    
        private int[] consumed = new int[2];
        private int[] offsetInWindow = new int[2];
    
    
        public NestedChildView(Context context) {
            super(context);
            init();
        }
    
    
        public NestedChildView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
    
        }
    
        private void init() {
            setNestedScrollingEnabled(true);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            final int actionMasked = MotionEventCompat.getActionMasked(event);
    
            // 取第一个接触屏幕的手指Id
            final int pointerId = MotionEventCompat.getPointerId(event, 0);
            switch (actionMasked) {
                case MotionEvent.ACTION_DOWN:
    
                    // 取得当前的Y,并赋值给lastY变量
                    downY = getPointerY(event, pointerId);
                    // 找不到手指,放弃掉这个触摸事件流
                    if (downY == -1) {
                        return false;
                    }
    
                    // 通知父View,开始滑动
                    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
                    break;
                case MotionEvent.ACTION_MOVE:
    
                    // 获得当前手指的Y
                    final float pointerY = getPointerY(event, pointerId);
    
                    // 找不到手指,放弃掉这个触摸事件流
                    if (pointerY == -1) {
                        return false;
                    }
    
                    // 计算出滑动的偏移量
                    float deltaY = pointerY - downY;
    
                    Log.d(TAG, String.format("downY = %f",deltaY));
    
                    Log.d(TAG, String.format("before dispatchNestedPreScroll, deltaY = %f", deltaY));
    
                    // 通知父View, 子View想滑动 deltaY 个偏移量,父View要不要先滑一下,然后把父View滑了多少,告诉子View一下
                    // 下面这个方法的前两个参数为在x,y方向上想要滑动的偏移量
                    // 第三个参数为一个长度为2的整型数组,父View将消费掉的距离放置在这个数组里面
                    // 第四个参数为一个长度为2的整型数组,父View在屏幕里面的偏移量放置在这个数组里面
                    // 返回值为 true,代表父View有消费任何的滑动.
                    if (dispatchNestedPreScroll(0, (int) deltaY, consumed, offsetInWindow)) {
    
                        // 偏移量需要减掉被父View消费掉的
                        deltaY -= consumed[1];
                        Log.d(TAG, String.format("after dispatchNestedPreScroll , deltaY = %f", deltaY));
    
                    }
    
                    // 上面的 (int)deltaY 会造成精度丢失,这里把精度给舍弃掉
                    if(Math.floor(Math.abs(deltaY)) == 0) {
                        deltaY = 0;
                    }
    
                    // 这里移动子View,下面的min,max是为了控制边界,避免子View越界
                    setY(Math.min(Math.max(getY() + deltaY, 0), ((View) getParent()).getHeight() - getHeight()));
    
    
                    break;
            }
            return true;
        }
    
        /**
         * 这个方法通过pointerId获取pointerIndex,然后获取Y
         *
         */
        private float getPointerY(MotionEvent event, int pointerId) {
            final int pointerIndex = MotionEventCompat.findPointerIndex(event, pointerId);
            if (pointerIndex < 0) {
                return -1;
            }
            return MotionEventCompat.getY(event, pointerIndex);
        }
    
        @Override
        public void setNestedScrollingEnabled(boolean enabled) {
            Log.d(TAG, String.format("setNestedScrollingEnabled , enabled = %b", enabled));
            childHelper.setNestedScrollingEnabled(enabled);
        }
    
        @Override
        public boolean isNestedScrollingEnabled() {
            Log.d(TAG, "isNestedScrollingEnabled");
            return childHelper.isNestedScrollingEnabled();
        }
    
        @Override
        public boolean startNestedScroll(int axes) {
            Log.d(TAG, String.format("startNestedScroll , axes = %d", axes));
            return childHelper.startNestedScroll(axes);
        }
    
        @Override
        public void stopNestedScroll() {
            Log.d(TAG, "stopNestedScroll");
            childHelper.stopNestedScroll();
        }
    
        @Override
        public boolean hasNestedScrollingParent() {
            Log.d(TAG, "hasNestedScrollingParent");
            return childHelper.hasNestedScrollingParent();
        }
    
        @Override
        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
            final boolean b = childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
            Log.d(TAG, String.format("dispatchNestedScroll , dxConsumed = %d, dyConsumed = %d, dxUnconsumed = %d, dyUnconsumed = %d, offsetInWindow = %s", dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, Arrays.toString(offsetInWindow)));
            return b;
        }
    
        @Override
        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
            final boolean b = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
            Log.d(TAG, String.format("dispatchNestedPreScroll , dx = %d, dy = %d, consumed = %s, offsetInWindow = %s", dx, dy, Arrays.toString(consumed), Arrays.toString(offsetInWindow)));
            return b;
        }
    
        @Override
        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
            Log.d(TAG, String.format("dispatchNestedFling , velocityX = %f, velocityY = %f, consumed = %b", velocityX, velocityY, consumed));
            return childHelper.dispatchNestedFling(velocityX, velocityY, consumed);
        }
    
        @Override
        public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
            Log.d(TAG, String.format("dispatchNestedPreFling , velocityX = %f, velocityY = %f", velocityX, velocityY));
            return childHelper.dispatchNestedPreFling(velocityX, velocityY);
        }
    }
    
    

    可以看到,NestedScrollingChild接口中的方法,都委托给NestedScrollingChildHelper去实现了,根本就不用我们来做。其实在Lollipop版本以上,View中是有这些方法的,只是我们要兼容Lollipop以下的版本,所以要自己来实现这个接口。

    主要的逻辑,就在onTouchEvent方法中了。如果之前有重写过这个方法的经验,其实一点都不复杂。

    1. ACTION_DOWN中,记录了一个按下的位置。
    2. ACTION_MOVE中,计算出偏移量,然后将这个偏移量,通过dispatchNestedPreScroll方法,传递给父View(当然,是需要实现NestedScrollingParent的父View),稍后会贴出父View中,在收到通知后,是怎么处理的。
    3. 如果被有被父View消费,那么偏移量需要减去被父View消费掉的。
    4. 根据偏移量移动子View。

    下面看父View是怎么实现的。

    public class NestedParentView extends FrameLayout implements NestedScrollingParent {
    
        public static final String TAG = NestedParentView.class.getSimpleName();
    
        private NestedScrollingParentHelper parentHelper;
    
        public NestedParentView(Context context) {
            super(context);
        }
    
        public NestedParentView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        {
            parentHelper = new NestedScrollingParentHelper(this);
    
        }
    
        @Override
        public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
            Log.d(TAG, String.format("onStartNestedScroll, child = %s, target = %s, nestedScrollAxes = %d", child, target, nestedScrollAxes));
            return true;
        }
    
        @Override
        public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
            Log.d(TAG, String.format("onNestedScrollAccepted, child = %s, target = %s, nestedScrollAxes = %d", child, target, nestedScrollAxes));
            parentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        }
    
        @Override
        public void onStopNestedScroll(View target) {
            Log.d(TAG, "onStopNestedScroll");
            parentHelper.onStopNestedScroll(target);
        }
    
        @Override
        public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            Log.d(TAG, String.format("onNestedScroll, dxConsumed = %d, dyConsumed = %d, dxUnconsumed = %d, dyUnconsumed = %d", dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed));
        }
    
        @Override
        public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
            // 应该移动的Y距离
            final float shouldMoveY = getY() + dy;
    
            // 获取到父View的容器的引用,这里假定父View容器是View
            final View parent = (View) getParent();
    
            int consumedY;
            // 如果超过了父View的上边界,只消费子View到父View上边的距离
            if (shouldMoveY <= 0) {
                consumedY = - (int) getY();
            } else if (shouldMoveY >= parent.getHeight() - getHeight()) {
                // 如果超过了父View的下边界,只消费子View到父View
                consumedY = (int) (parent.getHeight() - getHeight() - getY());
            } else {
                // 其他情况下全部消费
                consumedY = dy;
            }
    
            // 对父View进行移动
            setY(getY() + consumedY);
    
            // 将父View消费掉的放入consumed数组中
            consumed[1] = consumedY;
    
            Log.d(TAG, String.format("onNestedPreScroll, dx = %d, dy = %d, consumed = %s", dx, dy, Arrays.toString(consumed)));
        }
    
        @Override
        public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
            Log.d(TAG, String.format("onNestedFling, velocityX = %f, velocityY = %f, consumed = %b", velocityX, velocityY, consumed));
            return true;
        }
    
        @Override
        public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
            Log.d(TAG, String.format("onNestedPreFling, velocityX = %f, velocityY = %f", velocityX, velocityY));
            return true;
        }
    
        @Override
        public int getNestedScrollAxes() {
            Log.d(TAG, "getNestedScrollAxes");
            return parentHelper.getNestedScrollAxes();
        }
    }
    
    

    其实也很清晰,接口NestedScrollingParent部分委托给NestedScrollingParentHelper实现,在本例中,我们重点关注onNestedPreScroll这个方法。这个方法就是在子View中调用dispatchNestedPreScroll之后被调用,除了参数offsetInWindow由Helper类控制,其他的参数都是一样的。

    父View获取到子View给的dy之后,看要消费多少,把消费的量设置到consumed数组中即可,很简单。


    至此这个小例子就写完了,希望能让大家有所启发。

    相关文章

      网友评论

        本文标题:通俗易懂的小例子来演示如何使用NestedScroll

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