三探Android嵌套滑动 NestedScrolling机制 本质以及源码解析

来源:互联网 发布:java 线程池 参数 编辑:程序博客网 时间:2024/05/22 06:26

要了解NestedScrolling机制的本质,当然少不了阅读源码。

这里我们先给出结论:NestedScrolling机制本质上就是两个相互关联的接口,当我们调用一个接口中的方法时,另一个接口中与之对应的方法就会被触发,仅此而已。

这就意味着,尽管我们之前介绍NestedScrolling机制时,为其加了很多条条框框和使用规则,但实际上,我们可以按照自己的需求和想法,完全自由的去使用它们————只要知道两个接口中方法的对应关系即可,至于何时调用NestedScrollingChild接口中的方法以及在NestedScrollingParent的方法中要做什么,都随你意。以NestedScrollingParent的onNestedPreScroll()方法为例:你可以使用scrollTo()、scrollBy()来滚动自身的内容(此时自身的布局位置是不变的),也可以通过修改自己的layoutParams来改变自身的布局位置;你甚至可以明明滚动了50px却向回传参数谎报说自己1px都没有滚动……只要能实现你想要的效果就行。

1源码解析

1.1 NestedScrollingChild和NestedScrollingParent

public interface NestedScrollingChild {     public boolean startNestedScroll(int axes);    public void stopNestedScroll();    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);     public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);     public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);     public boolean dispatchNestedPreFling(float velocityX, float velocityY);    public void setNestedScrollingEnabled(boolean enabled);    public boolean isNestedScrollingEnabled();    public boolean hasNestedScrollingParent();}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
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

这两个接口其实没什么好说的,仅仅就是定义了一些抽象方法而已。抽象方法能做什么事取决于它的具体实现。而在第一篇文章中我们已经知道,对于实现这两个接口中的大部分方法,我们只要调用其对应的helper类中的同名方法即可。

下面的表格我们在第一篇文章中也已经见到过了,它描述的是两个接口中方法的触发关系:

NestedScrollingChild中的方法(发起者)NestedScrollingParent中的方法(被回调)startNestedScrollonStartNestedScroll、onNestedScrollAccepteddispatchNestedPreScrollonNestedPreScrolldispatchNestedScrollonNestedScrollstopNestedScrollonStopNestedScroll……

这些方法之间的触发关系是如果建立起来的呢?——通过NestedScrollingChildHelper对象。下面我们将通过阅读NestedScrollingChildHelper的源码来明确这一点。

1.2 NestedScrollingChildHelper

NestedScrollingChildHelper完整源码(中文注释)

在看源码之前先说明一点:为了方便表述以及避免混淆,以下我们都将使用配合者parent发起者child特指通过NestedScrolling机制进行配合动作的一对父子view。

先来看成员变量

private final View mView;//发起者childprivate ViewParent mNestedScrollingParent;//配合者parentprivate boolean mIsNestedScrollingEnabled;public NestedScrollingChildHelper(View view) {    mView = view;}public boolean hasNestedScrollingParent() {    return mNestedScrollingParent != null;}public void setNestedScrollingEnabled(boolean enabled) {    ...    mIsNestedScrollingEnabled = enabled;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • mView:发起者child,在创建NestedScrollingChildHelper对象时,由构造方法传入
  • mNestedScrollingParent:在startNestedScroll(int axes)方法中找到的配合者parent
  • mIsNestedScrollingEnabled:相当于是一个功能开关,如果值为false的话,那么NestedScrolling机制就无法使用

startNestedScroll(int axes)方法

这个方法所做的事情就是自下而上遍历mView的各级父view,看其中是否存在一个实现了NestedScrollingParent接口并且其onStartNestedScroll(…)方法返回true的父view,如果存在,则将这个父view赋值给成员变量mNestedScrollingParent,并返回true(表示找到了能与发起者child进行配合动作的配合者parent)。

具体做法参考代码及注释:

public boolean startNestedScroll(int axes) {    if (hasNestedScrollingParent()) {        return true;    }    if (isNestedScrollingEnabled()) {        ViewParent p = mView.getParent();        View child = mView;        //往上逐层调用每个父view的onStartNestedScroll方法,直到某个父view的onStartNestedScroll返回了ture,        //此时说明找到了配合者parent        while (p != null) {            //这几个参数的含义参考NestedScrollParent接口的onStartNestedScroll            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {//找到了配合者parent                mNestedScrollingParent = p;//保存配合者parent                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);//调用配合者parent的onNestedScrollAccepted方法                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
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 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

stopNestedScroll()方法

就是简单的调用配合者parent的onStopNestedScroll方法而已

public void stopNestedScroll() {    if (mNestedScrollingParent != null) {        //调用配合者parent的onStopNestedScroll方法        ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);        mNestedScrollingParent = null;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

dispatchNestedPreScroll(…)方法

就做了两件事:

  • 1.调用配合者parent的onNestedPreScroll方法
  • 2.根据配合者parent的起止位置计算offsetInWindow
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {        if (dx != 0 || dy != 0) {            //获得配合者parent的起始位置            int startX = 0;            int startY = 0;            if (offsetInWindow != null) {                mView.getLocationInWindow(offsetInWindow);                startX = offsetInWindow[0];                startY = offsetInWindow[1];            }            //将comsumed清空            if (consumed == null) {                if (mTempNestedScrollConsumed == null) {                    mTempNestedScrollConsumed = new int[2];                }                consumed = mTempNestedScrollConsumed;            }            consumed[0] = 0;            consumed[1] = 0;            //调用配合者parent的onNestedPreScroll方法            ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);            //根据配合者parent的起始位置和终止位置计算窗体偏移量(其实就是配合者parent的偏移量)            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;}
  • 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
  • 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

dispatchNestedScroll(…)方法

这个与上面的dispatchNestedPreScroll()方法如出一辙,不用解释了吧

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {        if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {            //获得配合者parent的起始位置            int startX = 0;            int startY = 0;            if (offsetInWindow != null) {                mView.getLocationInWindow(offsetInWindow);                startX = offsetInWindow[0];                startY = offsetInWindow[1];            }            //调用配合者parent的onNestedScroll方法            ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);            //根据配合者parent的起始位置和终止位置计算窗体偏移量(其实就是配合者parent的偏移量)            if (offsetInWindow != null) {                mView.getLocationInWindow(offsetInWindow);                offsetInWindow[0] -= startX;                offsetInWindow[1] -= startY;            }            return true;        } else if (offsetInWindow != null) {            // No motion, no dispatch. Keep offsetInWindow up to date.            offsetInWindow[0] = 0;            offsetInWindow[1] = 0;        }    }    return false;}
  • 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
  • 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

dispatchNestedPreFling(…)和dispatchNestedFling(…)方法

这两个方法可以类比于dispatchNestedPreScroll(…)和dispatchNestedScroll(…),但是更简单,只做了“调用配合者parent的同名方法”这一件事。

public boolean dispatchNestedPreFling(float velocityX, float velocityY) {    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {        //调用配合者parent的onNestedPreFling方法        return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX, velocityY);    }    return false;}public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {        //调用配合者parent的onNestedFling方法        return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX, velocityY, consumed);    }    return false;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

以上就是NestedScrollingChildHelper的主要代码。

总结一下,NestedScrollingChildHelper主要就是做了下面这两件事

  1. 找到配合者parent
  2. 作为发起者child和配合者parent之间方法调用的桥梁,起一个中介或者说是代理的作用。

    当我们调用NestedScrollingChild中的方法XXX()时,方法XXX()实际会去调用NestedScrollingChildHelper中的方法XXX(),而NestedScrollingChildHelper中的方法XXX()又会去调用NestedScrollingParent中的方法onXXX(),就是这样一个简单的传递流程。方法的返回值则是走相反的传递路径。

1.3 NestedScrollingParentHelper

源码只是寥寥数行,没有什么值得特别注意的地方:

//此Helper类的工作非常简单,就是保存了axes的信息而已public class NestedScrollingParentHelper {    private final ViewGroup mViewGroup;    private int mNestedScrollAxes;    public NestedScrollingParentHelper(ViewGroup viewGroup) {        mViewGroup = viewGroup;    }    public void onNestedScrollAccepted(View child, View target, int axes) {        mNestedScrollAxes = axes;    }    public int getNestedScrollAxes() {        return mNestedScrollAxes;    }    public void onStopNestedScroll(View target) {        mNestedScrollAxes = 0;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

2总结与思考

回想NestedScrolling的工作流程并结合上面helper类的源码,我们会发现整个NestedScrolling机制其实就是两个接口加上一个中介(NestedScrollingChildHelper)而已。在两个helper类中也基本没有涉及到接口的使用方式————这也就是为什么我们会在文章开头时说:你可以在一定范围内“为所欲为”。

google对于NestedScrolling机制的设计也很值得我们在自己的项目中借鉴:

  1. 通过两个接口来解耦需要进行交互的view
  2. 提供封装了接口之间交互逻辑的helper类以方便用户使用接口

下一篇文章中,我们将使用NestedScrolling机制实现系列文章开始时所示的饿了么店铺详情页效果。

1 0
原创粉丝点击