通过CoordinatorLayout可以实现许多炫酷的效果,大家可以参考我之前一篇博客:
其实CoordinatorLayout就是利用NestedScrolling(嵌套滑动机制)来完成复杂的滑动交互。NestedScrolling是Android 5.0之后为我们提供的新特性,降低了使用传统事件分发机制处理嵌套滑动的难度,用于给子view与父view提供更好的交互。
今天就从源码的角度一起分析NestedScrolling,关于NestedScrolling的实现,有以下几个主要类需要关注:
NestedScrollingParent 嵌套滑动父view接口
NestedScrollingChild 嵌套滑动子view接口
NestedScrollingParentHelper 嵌套滑动父view接口的代理实现
NestedScrollingChildHelper 嵌套滑动子view接口的代理实现
我们先来看看NestedScrollingParent中的几个实现方法:
/**
* 父View是否允许嵌套滑动
*
* @param child 包含嵌套滑动父类的子View
* @param target 实现嵌套滑动的子View
* @param nestedScrollAxes 嵌套滑动方向,水平竖直或都支持
*/
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return super.onStartNestedScroll(child, target, nestedScrollAxes);
}
/**
* onStartNestedScroll()方法返回true会调用该函数
* 参数与onStartNestedScroll一致
*/
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
super.onNestedScrollAccepted(child, target, axes);
}
/**
* 嵌套滑动结束时调用
*
* @param target 实现嵌套滑动的子View
*/
@Override
public void onStopNestedScroll(View target) {
super.onStopNestedScroll(target);
}
/**
* 嵌套滑动子View的滑动情况(进度)
*
* @param target 实现嵌套滑动的子View
* @param dxConsumed 水平方向上嵌套滑动的子View消耗(滑动)的距离
* @param dyConsumed 竖直方向上嵌套滑动的子View消耗(滑动)的距离
* @param dxUnconsumed 水平方向上嵌套滑动的子View未消耗(未滑动)的距离
* @param dyUnconsumed 竖直方向上嵌套滑动的子View未消耗(未滑动)的距离
*/
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}
/**
* 嵌套滑动子View滑动之前的准备工作
*
* @param target 实现嵌套滑动的子View
* @param dx 水平方向上嵌套滑动的子View滑动的总距离
* @param dy 竖直方向上嵌套滑动的子View滑动的总距离
* @param consumed consumed[0]水平方向与consumed[1]竖直方向上父View消耗(滑动)的距离
*/
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(target, dx, dy, consumed);
}
/**
* 嵌套滑动子View的fling(滑行)情况
*
* @param target 实现嵌套滑动的子View
* @param velocityX 水平方向上的速度
* @param velocityY 竖直方向上的速度
* @param consumed 子View是否消耗fling
* @return true 父View是否消耗了fling
*/
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return super.onNestedFling(target, velocityX, velocityY, consumed);
}
/**
* 嵌套滑动子View fling(滑行)前的准备工作
*
* @param target 实现嵌套滑动的子View
* @param velocityX 水平方向上的速度
* @param velocityY 竖直方向上的速度
* @return true 父View是否消耗了fling
*/
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return super.onNestedPreFling(target, velocityX, velocityY);
}
/**
* 嵌套滑动方向
*
* @return 水平竖直或都支持
*/
@Override
public int getNestedScrollAxes() {
return super.getNestedScrollAxes();
}
接下来看看NestedScrollingChild中的实现方法:
/**
* 设置是否支持嵌套滑动
*
* @param enabled true与false表示支持与不支持
*/
@Override
public void setNestedScrollingEnabled(boolean enabled) {
super.setNestedScrollingEnabled(enabled);
}
/**
* 判断嵌套滑动是否可用
*
* @return true表示支持嵌套滑动
*/
@Override
public boolean isNestedScrollingEnabled() {
return super.isNestedScrollingEnabled();
}
/**
* 开始嵌套滑动
*
* @param axes 方向轴,水平方向与竖直方向
* @return
*/
@Override
public boolean startNestedScroll(int axes) {
return super.startNestedScroll(axes);
}
/**
* 停止嵌套滑动
*/
@Override
public void stopNestedScroll() {
super.stopNestedScroll();
}
/**
* 判断父View是否支持嵌套滑动
*
* @return true与false表示支持与不支持
*/
@Override
public boolean hasNestedScrollingParent() {
return super.hasNestedScrollingParent();
}
/**
* 处理滑动事件
*
* @param dxConsumed 水平方向上消耗(滑动)的距离
* @param dyConsumed 竖直方向上消耗(滑动)的距离
* @param dxUnconsumed 水平方向上未消耗(未滑动)的距离
* @param dyUnconsumed 竖直方向上未消耗(未滑动)的距离
* @param offsetInWindow 窗体偏移量
* @return true表示事件已经分发,false表示没有分发
*/
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return super.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
/**
* 处理滑动事件前的准备工作
*
* @param dx 水平方向上滑动的距离
* @param dy 竖直方向上滑动的距离
* @param consumed 父view消耗的距离
* @param offsetInWindow 窗体偏移量
* @return 父View是否处理了嵌套滑动
*/
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
/**
* fling(滑行)前的准备工作
*
* @param velocityX 水平方向上的速度
* @param velocityY 竖直方向上的速度
* @param consumed 是否被消耗
* @return true表示被消耗,false反之
*/
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return super.dispatchNestedFling(velocityX, velocityY, consumed);
}
/**
* fling(滑行)时调用
*
* @param velocityX 水平方向上的速度
* @param velocityY 竖直方向上的速度
* @return true表示被消耗,false反之
*/
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return super.dispatchNestedPreFling(velocityX, velocityY);
}
实际应用中,嵌套滑动中的父view实现NestedScrollingParent接口,嵌套滑动中的子view实现NestedScrollingChild接口。NestedScrollingParentHelper和NestedScrollingChildHelper是两个辅助类,我们只需要在对应的接口方法中调用这些辅助类的实现即可。
OK,准备工作到此结束。参考网上资料写了一个简单的例子,先看最终的效果图:
最终实现的效果如上所示,通过这个实例来分析完整的嵌套滑动流程以及它们之间的分工合作。
1.子view是嵌套滑动的发起者,父view是嵌套滑动的处理者。首先在子view中允许设置嵌套滑动:
private void init() {
nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
2.调用startNestedScroll()方法开始嵌套滑动,并设置滑动方向:
case MotionEvent.ACTION_DOWN: {
mDownX = x;
mDownY = y;
//通知父View开始嵌套滑动,并设置滑动方向(水平竖直方向都支持)
startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
这时候父view的onStartNestedScroll方法将会被回调,返回true表示允许此次嵌套滑动:
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return true;
}
3.view开始滑动之前,会调用dispatchNestedPreScroll方法确定父view是否需要滑动。如果父view需要滑动,会消耗的距离放在consumed中,返回给子view,子view根据父view消耗的距离重新计算自己需要滑动的距离,进行滑动;如果父view不需要滑动,则子View自身处理滑动事件:
case MotionEvent.ACTION_MOVE: {
int dx = x - mDownX;
int dy = y - mDownY;
//如果父View处理滑动事件
if (dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)) {
//减去父View消耗的距离
dx -= consumed[0];
dy -= consumed[1];
}
offsetLeftAndRight(dx);
offsetTopAndBottom(dy);
break;
}
这时候父view的onNestedPreScroll方法将会被回调,协同处理滑动事件:
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(target, dx, dy, consumed);
//向右滑动
if (dx > 0) {
//滑动到边界
if (target.getRight() + dx > getWidth()) {
dx = target.getRight() + dx - getWidth();
//父View消耗
offsetLeftAndRight(dx);
consumed[0] += dx;
}
}
//向左滑动
else {
if (target.getLeft() + dx < 0) {
dx = dx + target.getLeft();
//父View消耗
offsetLeftAndRight(dx);
consumed[0] += dx;
}
}
//向下滑动
if (dy > 0) {
if (target.getBottom() + dy > getHeight()) {
dy = target.getBottom() + dy - getHeight();
//父View消耗
offsetTopAndBottom(dy);
consumed[1] += dy;
}
}
//向上滑动
else {
if (target.getTop() + dy < 0) {
dy = dy + target.getTop();
//父View消耗
offsetTopAndBottom(dy);
consumed[1] += dy;
}
}
}
4.子view计算完自己的滑动距离进行滑动之后,调用dispatchNestedScroll方法进行滑动:
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return nestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
5.如果需要停止嵌套滑动,子view调用stopNestedScroll方法,父view的onStopNestedScroll方法被回调结束滑动:
case MotionEvent.ACTION_UP: {
//结束嵌套滑动
stopNestedScroll();
break;
}
至此,我们已经经历了一次完整的嵌套滑动流程,实际上内部都是通过NestedScrollingChildHelper实现的,我们只需要在恰当的地方传入参数调用方法即可。
关于NestedScrollingParentHelper源码解析可以参考下面的博客:
NestedScrollingParent,NestedScrollingParentHelper 详解
希望能对你有所帮助,源码已经同步上传到github上:
https://github.com/18722527635/AndroidArtStudy
欢迎star,fork,提issues,一起进步,下一篇再见~