Android5.0开始提供嵌套滑动机制,用于给子view与父view滑动互动提供更好的交互。
因为在原来的事件分发机制中,如果让子view开始处理事件后,父view有需要在某一个条件下处理事件,只能把子view的事件拦截,在接下来的一个完整的时间系类中,父view就无法继续给子view分发事件了,除非重写dispatchTouchEvent
方法,但是我们知道重写这个方法还是比较有难度的。
在最新的V4包等兼容库中Android都对嵌套滑动提供了支持,主要类如下:
接下来对上述的一些类进行介绍
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实现。
一个嵌套滑动的完成流程应该是这样的。
NestedScrollingParentHelper和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;
}
//......省略一些方法
}
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;//父亲消耗
}
}
}
}