Android NestedScrolling 解析

产生原因

由于在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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
 @CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
  • 1
  • 2

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

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

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent
  • 1

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

  • 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);  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

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

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
// 省略
}
  • 1
  • 2
  • 3

所以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);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

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

 private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mScrollingChildHelper == null) {
            mScrollingChildHelper = new NestedScrollingChildHelper(this);
        }
        return mScrollingChildHelper;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

以上方法主要做了:

  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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

其实和子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

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇