Android NestedScrolling 解析

来源:互联网 发布:阿里云备案要多长时间 编辑:程序博客网 时间:2024/06/03 20:54

产生原因

由于在Android上进行滑动的控件在手机性能越来越好的情况下,人们已经习惯于常用的手势进行操作,出现许多控件滑动时需要去协调同一个界面的滑动的情况。例如在同一个方向内外(上下)的嵌套,不同方向(上下与左右方向)的嵌套等。解决这类嵌套可以通过Android开发艺术书上讲的内部拦截法外部拦截法去解决,但是,在处理多个View的协调时使用外部拦截法,特别是一些第三方库,在使用时就必须去修改源码里的onTouch()等方法。并且在处理如下图所示,当滑动到一定距离又需要拦截的View去响应滑动,这种情况是需要自己去手动处理事件分发,相对就复杂不少。于是Google在5.0后推出了NestedScrolling解决方式。

NestedScrolling

这是Google官方从5.0后引入的滑动嵌套解决方案,同时能够向后兼容。最有代表性的就是Handling Scrolls with CoordinatorLayout中的效果, 下面的效果就是从5.0后常见的效果。那我们需要去了解背后的原因。再举一反三去扩展使用到自己想要的效果。

Expanding or contracting the Toolbar or header space to make room for the main content

<android.support.design.widget.AppBarLayout        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:fitsSystemWindows="true"        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">    <android.support.design.widget.CollapsingToolbarLayout            android:id="@+id/collapsing_toolbar"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:fitsSystemWindows="true"            app:contentScrim="?attr/colorPrimary"            app:expandedTitleMarginEnd="64dp"            app:expandedTitleMarginStart="48dp"            app:layout_scrollFlags="scroll|exitUntilCollapsed">            <android.support.v7.widget.Toolbar                android:id="@+id/toolbar"                android:layout_width="match_parent"                android:layout_height="?attr/actionBarSize"                app:layout_scrollFlags="scroll|enterAlways"></android.support.v7.widget.Toolbar>    </android.support.design.widget.CollapsingToolbarLayout></android.support.design.widget.AppBarLayout>
 @CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)public class AppBarLayout extends LinearLayout {

上面知道了AppBarLayout是LinearLayout并且注解说明使用AppBarLayout.Behavior.class来处理协调滑动。
具体 看看这个Behavior,可以看到几个关键的NestedScroll相关方法,后面会具体说明。
这里写图片描述

再了解一下CoordinatorLayout,实现了NestedScrollingParent接口

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent

从上面了解的一些,我们接下来就全面来了解以下几个类。

  • NestedScrollingChild
  • NestedScrollingChildHelper

  • NestedScrollingParent

  • NestedScrollingParentHelper

NestedScrollingChild

先看看NestedScrollingChild的细节。

public interface NestedScrollingChild {      /**      * 设置嵌套滑动是否能用     *      *  @param enabled true to enable nested scrolling, false to disable     */      public void setNestedScrollingEnabled(boolean enabled);      /**      * 判断嵌套滑动是否可用      *      * @return true if nested scrolling is enabled     */      public boolean isNestedScrollingEnabled();      /**      * 开始嵌套滑动     *      * @param axes 表示方向轴,有横向和竖向     */      public boolean startNestedScroll(int axes);      /**      * 停止嵌套滑动      */      public void stopNestedScroll();      /**      * 判断是否有父View 支持嵌套滑动      * @return whether this view has a nested scrolling parent     */      public boolean hasNestedScrollingParent();      /**      * 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离     *     * @param dx  x轴上滑动的距离     * @param dy  y轴上滑动的距离     * @param consumed 父view消费掉的scroll长度     * @param offsetInWindow   子View的窗体偏移量     * @return 支持的嵌套的父View 是否处理了 滑动事件      */      public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);      /**      * 子view处理scroll后调用     *     * @param dxConsumed x轴上被消费的距离(横向)      * @param dyConsumed y轴上被消费的距离(竖向)     * @param dxUnconsumed x轴上未被消费的距离      * @param dyUnconsumed y轴上未被消费的距离      * @param offsetInWindow 子View的窗体偏移量     * @return  true if the event was dispatched, false if it could not be dispatched.     */      public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);      /**      * 滑行时调用      *     * @param velocityX x 轴上的滑动速率     * @param velocityY y 轴上的滑动速率     * @param consumed 是否被消费      * @return  true if the nested scrolling parent consumed or otherwise reacted to the fling     */      public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);      /**      * 进行滑行前调用     *     * @param velocityX x 轴上的滑动速率     * @param velocityY y 轴上的滑动速率      * @return true if a nested scrolling parent consumed the fling     */      public boolean dispatchNestedPreFling(float velocityX, float velocityY);  }

CoordinatorLayout里嵌套着RecyclerViewToolbar,我们上下滑动RecyclerView的时候,Toolbar会随之显现隐藏,这是典型的嵌套滑动机制情景。这里,RecyclerView作为嵌套的子View,我们猜测,它一定实现了NestedScrollingChild 接口。

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {// 省略}

所以RecyclerView 实现了NestedScrollingChild 接口里的方法,我们在跟进去看看各个方法是怎么实现的?

  @Override    public void setNestedScrollingEnabled(boolean enabled) {        getScrollingChildHelper().setNestedScrollingEnabled(enabled);    }    @Override    public boolean isNestedScrollingEnabled() {        return getScrollingChildHelper().isNestedScrollingEnabled();    }    @Override    public boolean startNestedScroll(int axes) {        return getScrollingChildHelper().startNestedScroll(axes);    }    @Override    public void stopNestedScroll() {        getScrollingChildHelper().stopNestedScroll();    }    @Override    public boolean hasNestedScrollingParent() {        return getScrollingChildHelper().hasNestedScrollingParent();    }    @Override    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,            int dyUnconsumed, int[] offsetInWindow) {        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,                dxUnconsumed, dyUnconsumed, offsetInWindow);    }    @Override    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);    }    @Override    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {        return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);    }    @Override    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {        return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);    }

从上面的代码可以看出,全部都交给getScrollingChildHelper()这个方法的返回对象处理了,看看这个方法是怎么实现的。

 private NestedScrollingChildHelper getScrollingChildHelper() {        if (mScrollingChildHelper == null) {            mScrollingChildHelper = new NestedScrollingChildHelper(this);        }        return mScrollingChildHelper;    }

NestedScrollingChild 接口的方法都交给NestedScrollingChildHelper这个代理对象处理了。现在我们继续深入,随意挑个,分析下NestedScrollingChildHelper中开始嵌套滑动startNestedScroll(int axes)方法是怎么实现的。

NestedScrollingChildHelper#startNestedScroll

 public boolean startNestedScroll(int axes) {        if (hasNestedScrollingParent()) {             return true;        }        if (isNestedScrollingEnabled()) {//判断是否可以滑动            ViewParent p = mView.getParent();            View child = mView;            while (p != null) {                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {//回调了父View的onStartNestedScroll方法                    mNestedScrollingParent = p;                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);                    return true;                }                if (p instanceof View) {                    child = (View) p;                }                p = p.getParent();            }        }        return false;    }

以上方法主要做了:

  1. 判断是否有嵌套滑动的父View,返回值 true 表示找到了嵌套滑动的父View和同意一起处理 Scroll 事件。

  2. 用While的方式寻找最近嵌套滑动的父View ,如果找到调用父view的onNestedScrollAccepted.

从这里至少可以得出 子view在调用某个方法都会回调嵌套父view相应的方法,比如子view开始了startNestedScroll,如果嵌套父view存在,就会回调父view的onStartNestedScroll、onNestedScrollAccepted方法。

NestedScrollingChildHelper#dispatchNestedPreScroll
NestedScrollingChildHelper#dispatchNestedScroll
NestedScrollingChildHelper#stopNestedScroll
以上Helper的实现也是这个思路。

NestedScrollingParent

public interface NestedScrollingParent {    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);    public void onStopNestedScroll(View target);    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,            int dxUnconsumed, int dyUnconsumed);    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);    public boolean onNestedFling(View target, float velocityX,                                                       float velocityY,boolean consumed);    public boolean onNestedPreFling(View target, float velocityX, float velocityY);    public int getNestedScrollAxes();}

其实和子view差不多的方法,大致一一对应关系,而且它的具体实现也交给了NestedScrollingParentHelper这个代理类,这和我们上文的方式是一样的,就不再重复了

调用流程

  1. 当 NestedScrollingChild(下文用Child代替) 要开始滑动的时候会调用 onStartNestedScroll ,然后交给代理类NestedScrollingChildHelper(下文ChildHelper代替)的onStartNestedScroll请求给最近的NestedScrollingParent(下文Parent代替).

  2. 当ChildHelper的onStartNestedScroll方法 返回 true 表示同意一起处理 Scroll 事件的时候时候,ChildHelper会通知Parent回调onNestedScrollAccepted 做一些准备动作

  3. 当Child 要开始滑动的时候,会先发送onNestedPreScroll,交给ChildHelper的onNestedPreScroll 请求给Parent ,告诉它我现在要滑动多少距离,你觉得行不行,这时候Parent 根据实际情况告诉Child 现在只允许你滑动多少距离.然后 ChildHelper根据 onNestedPreScroll 中回调回来的信息对滑动距离做相对应的调整.

  4. 在滑动的过程中 Child 会发送onNestedScroll通知ChildeHelpaer的onNestedScroll告知Parent 当前 Child 的滑动情况.

  5. 当要进行滑行的时候,会先发送onNestedFling 请求给Parent,告诉它 我现在要滑行了,你说行不行, 这时候Parent会根据情况告诉 Child 你是否可以滑行.

  6. Child 通过onNestedFling 返回的 Boolean 值来觉得是否进行滑行.如果要滑行的话,会在滑行的时候发送onNestedFling 通知告知 Parent 滑行情况.

  7. 当滑动事件结束就会child发送onStopNestedScroll通知 Parent 去做相关操作.

我做一个图,做得不好不要见笑。

child-parent

再来个Material Design风格的动态效果
这里写图片描述

参考链接

Handling Scrolls with CoordinatorLayout
NestedScrolling事件机制源码解析
NestedScrollingChild
NestedScrollingChildHelper
NestedScrollingParent
NestedScrollingParentHelper
SwipeRefreshLayout 解析
Android NestedScrolling机制完全解析 带你玩转嵌套滑动
从源码角度分析嵌套滑动机制NestedScrolling


0 0