Android在发布 5.0(Lollipop)版本之后,Google为我们提供了嵌套滑动的特性。下面,我们从源码角度去分析Android嵌套滑动的实现机制。
首先,我们先来看一下以下嵌套滑动相关的4个核心类的实现:
NestedScrollingChild
package android.support.v4.view;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewParent;
public interface NestedScrollingChild {
/**
* 设置是否允许嵌套滑动
*
* @param enabled
*/
public void setNestedScrollingEnabled(boolean enabled);
/**
* 判断是否允许嵌套滑动
*
* @return true if nested scrolling is enabled
*/
public boolean isNestedScrollingEnabled();
/**
* 开始嵌套滑动
*
* @param axes 表示滑动方向(3个可能值)
* ViewCompat#SCROLL_AXIS_HORIZONTAL
* ViewCompat#SCROLL_AXIS_VERTICAL
* ViewCompat#SCROLL_AXIS_HORIZONTAL | ViewCompat#SCROLL_AXIS_VERTICAL
* @return true if a cooperative parent was found and nested scrolling has been enabled for the current gesture.
*/
public boolean startNestedScroll(int axes);
/**
* 停止嵌套滑动
*/
public void stopNestedScroll();
/**
* 判断是否有父类支持嵌套滑动
*/
public boolean hasNestedScrollingParent();
/**
* ChildView执行完scroll后调用,通知ParentView其实际的滑动距离和未消耗的滑动距离
*
* @param dxConsumed ChildView在x轴实际滑动的距离
* @param dyConsumed ChildView在y轴实际滑动的距离
* @param dxUnconsumed ChildView在x轴未消耗的距离(一般ParentView根据此数据处理自身的x轴滑动)
* @param dyUnconsumed ChildView在y轴未消耗的距离(一般ParentView根据此数据处理自身的y轴滑动)
* @param offsetInWindow ChildView的窗体偏移量
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
/**
* ChildView执行scroll前,用于通知ParentView意图滑动的距离(onInterceptTouchEvent或者onTouch中调用)
*
* @param dx ChildView在x轴上意图滑动的距离
* @param dy ChildView在y轴上意图滑动的距离
* @param consumed 输出参数,记录ParentView消费的滑动距离(x轴:consumed[0],y轴:consumed[1])
* @param offsetInWindow ChildView的窗体偏移量
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
/**
* ChildView进行fling滑动时调用,通知ParentView
*
* @param velocityX ChildView在x轴的fling速率
* @param velocityY ChildView在y轴的fling速率
* @param consumed ParentView是否消费本次fling操作
* @return true if the nested scrolling parent consumed or otherwise reacted to the fling
*/
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* ChildView进行fling操作前调用,通知ParentView其fling的速率
*
* @param velocityX ChildView在x轴的fling速率
* @param velocityY ChildView在y轴的fling速率
* @return true if a nested scrolling parent consumed the fling
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
以上NestedScrollingChild接口的方法描述已经比较详细,实现嵌套滑动时,ChildView必须实现该接口。通过查看RecycleView源码,我们可以知道NestedScrollingChild的接口实现完全由NestedScrollingChildHelper代理实现。因此,我们接下来分析以下NestedScrollingChildHelper的代码实现!
NestedScrollingChildHelper
package android.support.v4.view;
import android.view.View;
import android.view.ViewParent;
public class NestedScrollingChildHelper {
/**
* Helper对应的ChildView
*/
private final View mView;
/**
* A nested scrolling parent view currently receiving events for a nested scroll in progress.
*/
private ViewParent mNestedScrollingParent;
/**
* 是否允许嵌套滑动
*/
private boolean mIsNestedScrollingEnabled;
/**
* 记录上次scroll event中ParentView消耗的滑动距离
*/
private int[] mTempNestedScrollConsumed;
public NestedScrollingChildHelper(View view) {
mView = view;
}
/**
* 设置是否允许嵌套滑动
*
* @param enabled true to enable nested scrolling dispatch from this view, false otherwise
*/
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
/**
* 判断是否允许嵌套滑动
*
* @return true if nested scrolling is enabled for this view
*/
public boolean isNestedScrollingEnabled() {
return mIsNestedScrollingEnabled;
}
/**
* 判断ChildView此时是否存在一个接收嵌套滑动事件的ParentView
*
* @return true if this view has a nested scrolling parent, false otherwise
*/
public boolean hasNestedScrollingParent() {
return mNestedScrollingParent != null;
}
/**
* ChildView接收到滑动请求时调用
*(查找符合要求的Nested Scrolling ParentView)
*
* @param axes ChildView的滑动方向
* @return true if a cooperating parent view was found and nested scrolling started successfully
*/
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// 目前处于嵌套滑动处理阶段
return true;
}
if (isNestedScrollingEnabled()) {
// 接受嵌套滑动的ParentView
ViewParent p = mView.getParent();
// ParentView的直接子控件:ChildView本身或包含ChildView
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
// 找到符合要求的Nested Scrolling ParentView时,通知ParentView准备
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
/**
* 停止当前的嵌套滑动
*/
public void stopNestedScroll() {
if (mNestedScrollingParent != null) {
// 通知ParentView停止嵌套滑动
ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
mNestedScrollingParent = null;
}
}
/**
* ChildView执行完scroll操作后,通知ParentView
*(参考NestedScrollingChild#dispatchNestedScroll函数的注释说明)
*/
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) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
// 通知ParentView,ChildView的实际滑动情况(ParentView一般根据dxUnconsumed| dyUnconsumed处理自身滑动)
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
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;
}
/**
* ChildView执行scroll前,通知Parent其意图滑动的距离
*(参考NestedScrollingChild#dispatchNestedPreScroll函数注释说明)
*/
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;
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;
}
/**
* ChildView执行fling滑动操作后,通知ParentView
* (参考NestedScrollingChild#dispatchNestedFling函数)
*/
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
velocityY, consumed);
}
return false;
}
/**
* ChildView执行fling滑动操作前,通知ParentView,由ParentView确定是否消费fling滑动事件
* (参考NestedScrollingChild#dispatchNestedPreFling函数)
*
* @return arentView是否消费fling滑动事件
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
velocityY);
}
return false;
}
/**
* 在ChildView的onDetachedFromWindow回调函数中调用,结束嵌套滑动
*/
public void onDetachedFromWindow() {
ViewCompat.stopNestedScroll(mView);
}
/**
* 停止嵌套滑动
*(nested scroll ChildView停止本次嵌套滑动时,处理ChildView自身的状态)
*/
public void onStopNestedScroll(View child) {
ViewCompat.stopNestedScroll(mView);
}
}
NestedScrollingChildHelper可以说是嵌套滑动机制中最重要的类,ChildView实现NestedScrollingChild接口时,所有NestedScrollingChild的函数实现都交给NestedScrollingChildHelper代理实现,包括:查找接受嵌套滑动的ParentView,通知ParentView嵌套滑动的滑动距离、方向,通知ParentView结束嵌套滑动等。
NestedScrollingParent
package android.support.v4.view;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
/**
* 实现嵌套滑动的ParentView必须实现该接口
*/
public interface NestedScrollingParent {
/**
* ParentView判断是否进行嵌套滑动
*
* @param child ParentView的直接子控件(包含target)
* @param target 嵌套滑动的ChildView
* @param nestedScrollAxes ChildView的嵌套滑动方向
* @return true if this ViewParent accepts the nested scroll operation
*/
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
/**
* 当onStartNestedScroll返回true时调用
*(一般直接调用NestedScrollingParentHelper#onNestedScrollAccepted,记录嵌套滑动方向)
*/
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
/**
* ParentView结束嵌套滑动
*(一般直接调用NestedScrollingParentHelper#onStopNestedScroll,重置嵌套滑动方向flag为0)
*/
public void onStopNestedScroll(View target);
/**
* 接收ChildView的滑动通知(ChildView scroll后)
*(当ChildView调用dispatchNestedScroll函数时间接调用,一般ParentView在函数中实现滑动操作)
*/
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
/**
* 接收ChildView的滑动通知(ChildView scroll前)
*(当ChildView调用 dispatchNestedPreScroll函数时间接调用,由ParentView决定自身消耗的滑动距离)
*/
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
/**
* 类似onNestedScroll,只是针对fling操作
*/
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
/**
* 类似onNestedPreScroll,只是针对fling操作
*/
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
/**
* 返回嵌套滑动的方向
* (直接调用NestedScrollingParentHelper#getNestedScrollAxes)
*/
public int getNestedScrollAxes();
}
实现嵌套滑动的ParentView必须实现NestedScrollingParent接口类,用于判断是否接受嵌套滑动、计算ParentView消耗的滑动距离以及接收到ChildView的滑动通知后,ParentView本身进行滑动等逻辑。以上函数都不是ParentView自身直接调用,而是由ChildView实现NestedScrollingChild接口的方法间接调用!
NestedScrollingParentHelper
public class NestedScrollingParentHelper {
/**
* 实现嵌套滑动的ParentView
*/
private final ViewGroup mViewGroup;
/**
* 记录嵌套滑动的方向flag
*/
private int mNestedScrollAxes;
public NestedScrollingParentHelper(ViewGroup viewGroup) {
mViewGroup = viewGroup;
}
/**
* 返回嵌套滑动的方向flag
*/
public int getNestedScrollAxes() {
return mNestedScrollAxes;
}
/**
* 当ParentView接收嵌套滑动操作时调用
* (在NestedScrollingParent#onNestedScrollAccepted接口实现中调用)
*/
public void onNestedScrollAccepted(View child, View target, int axes) {
// 记录嵌套滑动方向
mNestedScrollAxes = axes;
}
/**
* 当嵌套滑动结束时调用
* (在NestedScrollingParent#onStopNestedScroll接口实现中调用)
*/
public void onStopNestedScroll(View target) {
// 重置嵌套滑动方向
mNestedScrollAxes = 0;
}
}
以上NestedScrollingParentHelper的实现比较简单,基本都是在NestedScrollingParent的对应接口调用,只是在ParentView接受嵌套滑动和结束嵌套滑动时,改变记录嵌套滑动方向flag的值mNestedScrollAxes。
建议熟悉以上4个类后,查看一下RecycleView中NestedScrollingChild接口的实现,以及调用NestedScrollingChild接口的时机;查看一下NestedScrollView中NestedScrollingParent接口的实现,即可对嵌套滑动机制大致掌握!
下一篇,我将通过简单分析RecycleView和NestedScrollView源码,以及实现简单的demo,帮助大家更深刻地了解嵌套滑动机制!
总结一下,嵌套滑动的工作流程如下:
(1)ChildView接收到TouchEvent(down类型)后,先查找符合嵌套滑动的ParentView :
NestedScrollingChild#startNestedScroll(int axes) -》
NestedScrollingChildHelper#startNestedScroll(int axes) -》
递归查找符合嵌套滑动要求的ParentView(即NestedScrollingParent#startNestedScroll(int axes)返回true的父控件)
(2)ChildView(实现NestedScrollingChild接口)接收到滑动请求时,先通知ParentView(实现NestedScrollingParent接口)其意图滑动的方向nestedScrollAxes和距离dx & dy:
// consumed作为输出参数,获取ParentView的消耗滑动距离
NestedScrollingChild#dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)
(3)ParentView可以根据自定义规则,计算需要消耗滑动距离int[] consumed,再通知ChildView;
//consumed作为输出参数,记录ParentView的消耗滑动距离
NestedScrollingParent#onNestedPreScroll(View target, int dx, int dy, int[] consumed)
(4)ChildView根据ParentView返回的数据int[] consumed,重新计算需要滑动的距离dxConsumed & dyConsumed以及不消耗的滑动距离dxUnconsumed & dyUnconsumed;
(5)ChildView根据步骤4计算得到实际的滑动距离后,先自身调用scroll函数进行滑动,然后通知ParentView其滑动情况;
NestedScrollingChild#dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
(6)ParentView接收到ChildView的实际滑动情况时,可以根据dxUnconsumed & dyUnconsumed,确定ParentView自身的滑动。
NestedScrollingParent#onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
注意:
仅当步骤(1)中找到接受嵌套滑动的parentView,才会进行后面的步骤;
以上步骤(2)~(6)为循环步骤,在同一系列的Touch Events事件流中,每一个move类型的Touch Event都会触发一次(2)~(6)的组合步骤,从而可以根据手指在屏幕的触摸动态处理ChildView和ParentView的滑动!
直至,ChildView调用NestedScrollingChild#stopNestedPreScroll函数(一般为接收到类型up或cancel的Touch Event时调用),则嵌套滑动结束。
参考链接:
NestedScrolling事件机制源码解析