Android从零开搞系列:自定义View(7)ScrollTo+ScrollBy+Scroller+NestedScrolling机制(下)

转载请注意:
http://blog.csdn.net/wjzj000/article/details/53894449
本菜开源的一个自己写的Demo,希望能给Androider们有所帮助,水平有限,见谅见谅…
https://github.com/zhiaixinyang/PersonalCollect (拆解GitHub上的优秀框架于一体,全部拆离不含任何额外的库导入)
https://github.com/zhiaixinyang/MyFirstApp(Retrofit+RxJava+MVP)


写在前面

本篇博客记录我个人对NestedScrolling机制学习的过程以及记录。
关于scrollTo,scrollBy以及Scroller相关的内容,请参考我的上一篇博客:
http://blog.csdn.net/wjzj000/article/details/53874285
我相信大家在看到这篇博客的时候,应该已经看了不少的博客。但是我猜大家依然没有搞懂,不然不会费尽心思的继续找NestedScrolling相关的博客内容。(PS:看官如果懂这个过程,就没必要看了继续往下看了。因为这里我只是记录的自己学习的过程,没有啥高深的用法。)


让我们先看一个效果:

这里写图片描述

接下来的内容将依托于这个效果,来梳理NestedScrolling机制。


什么是NestedScrolling?

让我们先翻译一下这个词:嵌套滑动。我们知道我们的正常操作都是一个事件,有一套自己的事件分发机制。一个View只要选择处理这个事件,那么正常情况下谁也拿不走这个事件了,就是一条路走到黑。

也正是因为此,并不利于我们做一些效果。比如我们想俩个控件共享这此的事件。那么我们就可以想到NestedScrolling机制。而这个NestedScrolling机制可以干什么呢?
简单来说,当我们俩个控件时嵌套关系正准备处理一个滚动事件时:那么子View在想要滑动的时候会想问问它的父View:爹,你滚吗?此时,父View会根据自己是否需要滚动而对子View说:儿砸,我滚。那么子View得到这个消息,就会在自己的滑动事件中源源不断的把自己的滚动的数据回调给父View,供其使用。所以父View在此时的滚动,其实是子View提供的!


何以见得?

首先,各位看官应该都或多或少的见过下面这四个妖艳贱货:
NestedScrollingChild
NestedScrollingParent
NestedScrollingChildHelper
NestedScrollingParentHelper
直到昨天我才恍然大悟明白了它们之间的关系。上文中我们提到父View的滚动其实是子View进行提供的。为什么?因为源码就是如此。


走进源码

在开始源码之前,我们先树立一个概念。这套机制的流程是这样的:由父View实现NestedScrollingParent根据需要重写一些方法。然后子View实现NestedScrollingChild在onTouchEvent中对对应的方法进行回调。而xxxHelper的作用是:封装了一些方法,方便我们去处理一系列情况。
那么接下来让我们看一步步开始看源码:

首先是这个效果的布局文件:
<com.example.mbenben.studydemo.view.nestedscroll.MyNestedScrollLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/one"
        android:text="我是来卖萌的"
        android:textColor="@color/white"
        android:textSize="16sp"
        android:background="@color/black"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="60dp" />
    <android.support.v7.widget.RecyclerView
        android:id="@+id/rlv_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</com.example.mbenben.studydemo.view.nestedscroll.MyNestedScrollLayout>
  • 效果很简单,只有一个自定义的父View。没错就是它实现了NestedScrollingParent。
public class MyNestedScrollLayout extends LinearLayout implements NestedScrollingParent{
    //暂时省略内部代码
}

  • 到这里肯定有看官有疑惑,那实现NestedScrollingChild是谁?还能有谁?RecyclerView呗!
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
    //省略内部代码
}
  • 所以现在啥都有了,实现NestedScrollingChild的RecyclerView以及实现NestedScrollingParent的自定义View。
  • 那么接下来就是让我们看一下,内部原理。刚才提到嵌套滑动是由子View进行控制的。所以让我们看看这个例子中作为子View的RecyclerView。直接把目光定位到它的onTouchEvent中
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        //省略部分代码
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                //省略部分代码
                //nestedScrollAxis 就是对应是水平滚动还是垂直滚动
                int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
                }
                //此方法用于最开始去询问父View是否需要获取这个滚动事件。PS:关于它的作用请往下看即可。
                startNestedScroll(nestedScrollAxis);
            } break;
            //省略部分代码,直接看ACTION_MOVE情况
            case MotionEvent.ACTION_MOVE: {
                //省略部分代码
                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
                //dispatchNestedPreScroll()此方法就是NestedScrollingChild中需要实现的方法。这里我们的RecyclerView将自己的滑动参数,传递进了这个方法。
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }
                //省略部分代码
            }
        }
    }
  • 让我们重点看这俩个方法:
    • 一个是DOWN中的startNestedScroll
    • 一个是MOVE中的dispatchNestedPreScroll
    @Override
    public boolean startNestedScroll(int axes) {
        return getScrollingChildHelper().startNestedScroll(axes);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }
  • 在这里,我们可以看到它是直接通过Helper类完成这个事件的分发。
  • 接下来让我们直接看一看Helper的内部实现。

    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
                //最终会通过父View的onStartNestedScroll()的返回值来决定回调什么方法。
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    //如果父View返回true,那么将调用这个方法。
                    /**
                     * 这里是官方关于这个方法的解释:
                     *  此方法将在{@link #onStartNestedScroll(View,View,int)
                     * onStartNestedScroll}返回true后调用。
                     * 这个方法的实现应该总是调用他们的超类的这个方法的实现(如果存在)。
                     */
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }
  • 简单可以这么理解,在我的子View中的onTouchEvent中DOWN的时候去询问父View是否需要这个事件,如果父View需要,那么进行一系列的赋值,然后在MOVE的时候调用dispatchNestedPreScroll()不断的将滑动的值传递给父View,知道父View不想处理了为止。
//请注意,内部注释的那一行。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    if (mTempNestedScrollConsumed == null) {
                        mTempNestedScrollConsumed = new int[2];
                    }
                    consumed = mTempNestedScrollConsumed;
                }
                consumed[0] = 0;
                consumed[1] = 0;
                //OK,在这里就是对实现NestedScrollingParent的父View进行回调。
                //接下来我们简单看一下ViewParentCompat,其实就是封装了不同安卓版本实现。
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }
    static final ViewParentCompatImpl IMPL;
    static {
        final int version = Build.VERSION.SDK_INT;
        if (version >= 21) {
            IMPL = new ViewParentCompatLollipopImpl();
        } else if (version >= 19) {
            IMPL = new ViewParentCompatKitKatImpl();
        } else if (version >= 14) {
            IMPL = new ViewParentCompatICSImpl();
        } else {
            IMPL = new ViewParentCompatStubImpl();
        }
    }

到这,整套NestedScrolling机制的流程就梳理完了。不知道各位看官看懂了没有。
那么接下来就是我们该怎么去使用?
上文代码中我们也可以看到,onTouchEvent事件中最终回调了onNestedPreScroll()方法,因此在父View对事件进行消费的核心也就是它!

  • 让我们走进自定义的这个父View中去瞅一瞅:
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return true;
    }
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        boolean hiddenTop = dy > 0 && getScrollY() < topHeight;
        boolean showTop = dy < 0 && getScrollY() >= 0 && !ViewCompat.canScrollVertically(target, -1);
        if (hiddenTop||showTop) {
            consumed[1] = dy;
            scrollBy(0, dy);
        }
    }
    //其他方法怎么处理?我们可以直接交给Helper去处理。
    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        nestedScrollingParentHelper.onNestedScrollAccepted(child,target,nestedScrollAxes);
    }
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > topHeight) {
            y = topHeight;
        }
        if (y != getScrollY()) {
            super.scrollTo(x, y);
        }
    }
  • 这里我们就可以呼应前边所说的内容,onStartNestedScroll()返回true,告诉子View我希望处理并消耗一定的事件,然后onNestedScrollAccepted()方法被回调。紧接着子View将所获得的事件坐标通过回调onNestedPreScroll()传递进来。
  • 最终父View根据自己的需要对事件进行消耗。
  • consumed[1]=dy;就是在告诉子View你传递的事件坐标我已经消耗了。1代表消耗y;0代表消耗x。
  • 这里我们进行了判断,在满足条件的时候进行对事件消费,否则并不做处理。

最后梳理一遍:

梳理一下过程:
如果我们想进行嵌套滑动,那么我们的外层View就要实现NestedScrollingParent,内层View实现NestedScrollingChild,并且在onTouchEvent中在特定的条件下对startNestedScroll(),dispatchNestedScroll()等方法进行合适的回调。

而外层View则需要重写onStartNestedScroll()返回true告诉内层View,我要进行消费事件,并重写onNestedPreScroll()通过自己的需求进行相应的消费。

当然其他的方法如果没有特殊需求,直接交付给Helper去处理也不失为一种好方法。


PS:相关源码基本都存放于我的这个开源项目之中:
https://github.com/zhiaixinyang/PersonalCollect


尾声

OK,到此关于NestedScrolling机制的简单流程就梳理完毕,希望各位看官能够浪费这么多时间有所收获。

最后希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值