NestedScrolling机制(三)——机制本质以及源码解析

要了解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();
}

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

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

NestedScrollingChild中的方法(发起者)NestedScrollingParent中的方法(被回调)
startNestedScrollonStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScrollonNestedPreScroll
dispatchNestedScrollonNestedScroll
stopNestedScrollonStopNestedScroll

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

1.2 NestedScrollingChildHelper

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

先来看成员变量

private final View mView;//发起者child
private ViewParent mNestedScrollingParent;//配合者parent
private boolean mIsNestedScrollingEnabled;

public NestedScrollingChildHelper(View view) {
    mView = view;
}

public boolean hasNestedScrollingParent() {
    return mNestedScrollingParent != null;
}

public void setNestedScrollingEnabled(boolean enabled) {
	...
    mIsNestedScrollingEnabled = enabled;
}
  • 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;
}

stopNestedScroll()方法

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

public void stopNestedScroll() {
    if (mNestedScrollingParent != null) {
        //调用配合者parent的onStopNestedScroll方法
        ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
        mNestedScrollingParent = null;
    }
}

dispatchNestedPreScroll(…)方法

就做了两件事:

  • 1.调用配合者parent的onNestedPreScroll方法
  • 2.根据发起者child的起止位置计算offsetInWindow
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        if (dx != 0 || dy != 0) {

            //获得发起者child的起始位置
            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);

            //根据发起者child的起始位置和终止位置计算offsetInWindow
            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;
}

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) {

            //获得发起者child的起始位置
            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);

            //根据发起者child的起始位置和终止位置计算offsetInWindow
            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;
}

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;
}

以上就是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;
    }
}

2总结与思考

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

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

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

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值