Android中的嵌套滑动机制
Android5.0开始提供嵌套滑动机制,用于给子view与父view滑动互动提供更好的交互。
因为在原来的事件分发机制中,如果让子view开始处理事件后,父view有需要在某一个条件下处理事件,只能把子view的事件拦截,在接下来的一个完整的时间系类中,父view就无法继续给子view分发事件了,除非重写dispatchTouchEvent
方法,但是我们知道重写这个方法还是比较有难度的。
在最新的V4包等兼容库中Android都对嵌套滑动提供了支持,主要类如下:
V4
- NestedScrollingParent 嵌套滑动中父view接口
- NestedScrollingParentHelper 嵌套滑动中父view接口的代理实现
- NestedScrollingChild 嵌套滑动中子view接口
- NestedScrollingChildHelper 嵌套滑动中子view接口的代理实现
- NestedScrollView 支持嵌套滑动的ScrollView
design
- CoordinatorLayout 协调器布局
- CoordinatorLayout.Behavior
注意点
- 在嵌套滑动中的一些规则:子view是嵌套滑动的发起者,父view是嵌套滑动的处理者
- 在使用调用嵌套滑动相关的方法时,应该总是使用:ViewCompat,ViewGroupCompat, ViewParentCompat的静态方法来实现兼容
- 应该使用NestedScrollingParentHelper或NestedScrollingChildHelper的对应方法来实现NestedScrollingParent或NestedScrollingChild接口中的方法。
学习嵌套滑动机制
接下来对上述的一些类进行介绍
NestedScrollingChild与NestedScrollingParent
public interface NestedScrollingChild {
public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public boolean hasNestedScrollingParent();
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public void stopNestedScroll();
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();
两个接口都有对应的方法,一个需要被嵌套滑动中的父view实现,一个需要被嵌套滑动中的子view实现。
一个嵌套滑动的完成流程应该是这样的。
- 在一个可以滑动的子view中开启嵌套滑动setNestedScrollingEnabled
- 如果要开始一次嵌套滑动,首先应该调用startNestedScroll方法(比如在ACTION_DOWN中),通知父view开始一次嵌套滑动,方法的参数应该是ViewCompatSCROLL_AXIS_HORIZONTAL(横向)或ViewCompatSCROLL_AXIS_VERTICAL(竖向)或者他们的and/or值。这时父view的onStartNestedScroll方法将会被回调,如果父view返回true表示配合此次嵌套滑动,并且父view的onNestedScrollAccepted被调用
- 在子view开始滑动之前,应该先问父view许否需要先滑动,也就是调用dispatchNestedPreScroll方法,这个方法接收三个四个参数:
- dxConsumed 表示子view此次滑动期间将要消耗的水平方法的距离
- dyConsumed 表示子view此次滑动期间将要消耗的垂直方法的距离
- consumed 一个两个长度的数组,这个数组传递给父view,如果父view要先行滑动,将会把消耗的距离通过此数据返回给子view
- offsetInWindow 父view先完成一个滑动后子view在窗口中的偏移值。
- 上面参数可以理解为:dxConsumed和dyConsumed是总的滑动值,传给父view,如果父view需要滑动有消耗掉一些距离,然后把消耗的距离放在consumed中,返回给子view,返回子view根据父view消耗的距离重新计算自己需要滑动的距离,进行滑动。这个过程发送在父view的onNestedPreScroll方法中。
- 子view在根据dispatchNestedPreScroll的返回值,然后计算被父view消耗的距离,根据需要位置
- 子view重新计算自己的滑动距离进行滑动之后,需要调用dispatchNestedScroll方法,此方法接收五个参数
- int dxConsumed 子view在滑动中水平方向消耗的距离
- int dyConsumed 子view在滑动中垂直方向消耗的距离
- int dxUnconsumed 子view在滑动中水平方向没有消耗的距离
- int dyUnconsumed 子view在滑动中垂直方向没有消耗的距离
- int[] offsetInWindow 返回值。父view完成一个滑动后子view在窗口中的偏移值。
- 在完成一系列滑动后,如果需要停止滑动,则子view调用stopNestedScroll然后父view的onStopNestedScroll方法被回调
NestedScrollingParentHelper和NestedScrollingChildHelper分析
NestedScrollingParentHelper和NestedScrollingChildHelper是两个辅助类,分别对象上面分析的两个接口。系统已经给我们封装好了,我们只需要在对应的接口的方法中调用这些辅助类的实现即可。
NestedScrollingChildHelper
public class NestedScrollingChildHelper {
private final View mView;//嵌套滑动中的子view
private ViewParent mNestedScrollingParent;//嵌套滑动中的父view接口
private boolean mIsNestedScrollingEnabled;//嵌套滑动是否可用
private int[] mTempNestedScrollConsumed;
public NestedScrollingChildHelper(View view) {
mView = view;
}
//......省略一部分方法
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {//如果正在进行嵌套滑动,无需处理
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {//否则如果嵌套滑动时开启的,遍历查找可以配合嵌套滑动的父view
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {//这里调用了父view的onStartNestedScroll询问是否配合嵌套滑动
//如果配合的话,给mNestedScrollingParent赋值,再调用父view的onNestedScrollAccepted。
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;//找到了就返回
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
//停止嵌套滑动,就是调用父view的onStopNestedScroll,然后mNestedScrollingParent置为null
public void stopNestedScroll() {
if (mNestedScrollingParent != null) {
ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
mNestedScrollingParent = null;
}
}
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) {//判断输入值
/*记录子view滑动前在窗口中的位置*/
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
//子view滑动后,告诉父view滑动的距离
ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);
if (offsetInWindow != null) {
//计算父view滑动后,子view在窗口中的偏移值
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;
}
//分发嵌套滑动,在子view开始滑动之前
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {//判断 dx 与 dy
/*记录子view滑动前在窗口中的位置*/
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {//处理==null的情况
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
//让父view先滑动。
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
//计算父view滑动后,子view在窗口中的偏移值
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;//如果父view消耗了一部分距离就返回ture
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
velocityY, consumed);
}
return false;
}
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
velocityY);
}
return false;
}
//......省略一些方法
}
NestedScrollingParentHelper
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;
}
}
NestedScrollingParentHelper就是记录NestedScrollAxes。
实战
开始实战之前,我也不知道到底那些消耗值改如何写,但是v4包中有个NestedScrollView可以拿来参考。
然后我们可以可以根据嵌套滑动写一个简单的demo,效果如下:
代码实现很简单:
嵌套滑动中的子view:
public class NestChildView extends View implements NestedScrollingChild {
private static final String TAG = NestChildView.class.getSimpleName();
private float mLastX;//手指在屏幕上最后的x位置
private float mLastY;//手指在屏幕上最后的y位置
private float mDownX;//手指第一次落下时的x位置(忽略)
private float mDownY;//手指第一次落下时的y位置
private int[] consumed = new int[2];//消耗的距离
private int[] offsetInWindow = new int[2];//窗口偏移
private NestedScrollingChildHelper mScrollingChildHelper;
public NestChildView(Context context) {
this(context, null);
}
public NestChildView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NestChildView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN: {
mDownX = x;
mDownY = y;
mLastX = x;
mLastY = y;
//当开始滑动的时候,告诉父view
startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_MOVE: {
/*
mDownY:293.0
mDownX:215.0
*/
int dy = (int) (y - mDownY);
int dx = (int) (x - mDownX);
//分发触屏事件给父类处理
if (dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)) {
//减掉父类消耗的距离
dx -= consumed[0];
dy -= consumed[1];
Log.d(TAG, Arrays.toString(offsetInWindow));
}
offsetTopAndBottom(dy);
offsetLeftAndRight(dx);
break;
}
case MotionEvent.ACTION_UP: {
stopNestedScroll();
break;
}
}
mLastX = x;
mLastY = y;
return true;
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mScrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mScrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mScrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
/**
* @param dx 水平滑动距离
* @param dy 垂直滑动距离
* @param consumed 父类消耗掉的距离
* @return
*/
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
}
嵌套滑动中的父view:
public class NestParentLayout extends FrameLayout implements NestedScrollingParent {
private static final String TAG = NestParentLayout.class.getSimpleName();
private NestedScrollingParentHelper mScrollingParentHelper;
public NestParentLayout(Context context) {
this(context, null);
}
public NestParentLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NestParentLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScrollingParentHelper = new NestedScrollingParentHelper(this);
}
/*
子类开始请求滑动
*/
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
Log.d(TAG, "onStartNestedScroll() called with: " + "child = [" + child + "], target = [" + target + "], nestedScrollAxes = [" + nestedScrollAxes + "]");
return true;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
mScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
}
@Override
public int getNestedScrollAxes() {
return mScrollingParentHelper.getNestedScrollAxes();
}
@Override
public void onStopNestedScroll(View child) {
mScrollingParentHelper.onStopNestedScroll(child);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
Log.d(TAG, "onNestedPreScroll() called with: " + "dx = [" + dx + "], dy = [" + dy + "], consumed = [" + Arrays.toString(consumed) + "]");
final View child = target;
if (dx > 0) {
if (child.getRight() + dx > getWidth()) {
dx = child.getRight() + dx - getWidth();//多出来的
offsetLeftAndRight(dx);
consumed[0] += dx;//父亲消耗
}
} else {
if (child.getLeft() + dx < 0) {
dx = dx + child.getLeft();
offsetLeftAndRight(dx);
Log.d(TAG, "dx:" + dx);
consumed[0] += dx;//父亲消耗
}
}
if (dy > 0) {
if (child.getBottom() + dy > getHeight()) {
dy = child.getBottom() + dy - getHeight();
offsetTopAndBottom(dy);
consumed[1] += dy;
}
} else {
if (child.getTop() + dy < 0) {
dy = dy + child.getTop();
offsetTopAndBottom(dy);
Log.d(TAG, "dy:" + dy);
consumed[1] += dy;//父亲消耗
}
}
}
}