写在前面
最近遇到了一个问题,在SwipeRefreshLayout
中,有时候下拉,圆球不会下来,等松开手指的时候,球会突然闪一下,不明所以。想到这个应该是滑动相关的问题,而且跟嵌套滑动似乎很有关联,我们看,public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingParent,NestedScrollingChild
,可以看出SwipeRefreshLayout
即实现了NestedScrollingParent
也实现了NestedScrollingChild
,那先从这个角度着手,看看NestedScroll
是个什么玩意儿。
学习一个
先来看看这两篇文章
- [Android 嵌套滑动机制(NestedScrolling)- Gemini](Android 嵌套滑动机制(NestedScrolling))
- Android NestedScrolling 实战 - Android
这里摘抄几句关于NestedScrollingChild
比较重要的:
需要做的就是,如果要准备开始滑动了,需要告诉 Parent,你要准备进入滑动状态了,调用
startNestedScroll()
。你在滑动之前,先问一下你的 Parent 是否需要滑动,也就是调用dispatchNestedPreScroll()
。如果父类滑动了一定距离,你需要重新计算一下父类滑动后剩下给你的滑动距离余量。然后,你自己进行余下的滑动。最后,如果滑动距离还有剩余,你就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用dispatchNestedScroll()
。
关于NestedScrollingParent
的:
从上面的 Child 分析可知,滑动开始的调用
startNestedScroll()
,Parent 收到onStartNestedScroll()
回调,决定是否需要配合 Child 一起进行处理滑动,如果需要配合,还会回调onNestedScrollAccepted()
。
每次滑动前,Child 先询问 Parent 是否需要滑动,即dispatchNestedPreScroll()
,这就回调到 Parent 的onNestedPreScroll()
,Parent 可以在这个回调中“劫持”掉 Child 的滑动,也就是先于 Child 滑动。
Child 滑动以后,会调用onNestedScroll()
,回调到 Parent 的 onNestedScroll(),这里就是 Child 滑动后,剩下的给 Parent 处理,也就是 后于 Child 滑动。
最后,滑动结束,调用onStopNestedScroll()
表示本次处理结束。
下面的内容是假定大家已经把上面两篇文章看完了。
我的例子
其实上面两篇文章已经写明白了,但有点不足的是,没有一个通俗易懂的例子来演示。所以如果各位还不是太清楚的话,可以通过下面的例子来理解。
先来看一个图。
这是一整次的滑动,橙色的为子View,蓝色的为父View。我们将子View往上滑的时候,先是父View带着子View一起向上滑动,等父View到了顶之后,子View开始滑动。
大概的原理是,滑动事件在子View中的时候,先让父View进行滑动的处理,然后子View去处理未被父View消费的距离。
在代码中是这么处理的。
1. 首先,子View是肯定需要实现NestedScrollingChild
的,然后重写onTouchEvent
方法,。。。
2.
得,不解释了。Talk is plain. Show you the codes.
下面是子View的实现。
public class NestedChildView extends View implements NestedScrollingChild {
public static final String TAG = "NestedChildView";
private final NestedScrollingChildHelper childHelper = new NestedScrollingChildHelper(this);
private float downY;
private int[] consumed = new int[2];
private int[] offsetInWindow = new int[2];
public NestedChildView(Context context) {
super(context);
init();
}
public NestedChildView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setNestedScrollingEnabled(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int actionMasked = MotionEventCompat.getActionMasked(event);
// 取第一个接触屏幕的手指Id
final int pointerId = MotionEventCompat.getPointerId(event, 0);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
// 取得当前的Y,并赋值给lastY变量
downY = getPointerY(event, pointerId);
// 找不到手指,放弃掉这个触摸事件流
if (downY == -1) {
return false;
}
// 通知父View,开始滑动
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
case MotionEvent.ACTION_MOVE:
// 获得当前手指的Y
final float pointerY = getPointerY(event, pointerId);
// 找不到手指,放弃掉这个触摸事件流
if (pointerY == -1) {
return false;
}
// 计算出滑动的偏移量
float deltaY = pointerY - downY;
Log.d(TAG, String.format("downY = %f",deltaY));
Log.d(TAG, String.format("before dispatchNestedPreScroll, deltaY = %f", deltaY));
// 通知父View, 子View想滑动 deltaY 个偏移量,父View要不要先滑一下,然后把父View滑了多少,告诉子View一下
// 下面这个方法的前两个参数为在x,y方向上想要滑动的偏移量
// 第三个参数为一个长度为2的整型数组,父View将消费掉的距离放置在这个数组里面
// 第四个参数为一个长度为2的整型数组,父View在屏幕里面的偏移量放置在这个数组里面
// 返回值为 true,代表父View有消费任何的滑动.
if (dispatchNestedPreScroll(0, (int) deltaY, consumed, offsetInWindow)) {
// 偏移量需要减掉被父View消费掉的
deltaY -= consumed[1];
Log.d(TAG, String.format("after dispatchNestedPreScroll , deltaY = %f", deltaY));
}
// 上面的 (int)deltaY 会造成精度丢失,这里把精度给舍弃掉
if(Math.floor(Math.abs(deltaY)) == 0) {
deltaY = 0;
}
// 这里移动子View,下面的min,max是为了控制边界,避免子View越界
setY(Math.min(Math.max(getY() + deltaY, 0), ((View) getParent()).getHeight() - getHeight()));
break;
}
return true;
}
/**
* 这个方法通过pointerId获取pointerIndex,然后获取Y
*
*/
private float getPointerY(MotionEvent event, int pointerId) {
final int pointerIndex = MotionEventCompat.findPointerIndex(event, pointerId);
if (pointerIndex < 0) {
return -1;
}
return MotionEventCompat.getY(event, pointerIndex);
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
Log.d(TAG, String.format("setNestedScrollingEnabled , enabled = %b", enabled));
childHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
Log.d(TAG, "isNestedScrollingEnabled");
return childHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
Log.d(TAG, String.format("startNestedScroll , axes = %d", axes));
return childHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
Log.d(TAG, "stopNestedScroll");
childHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
Log.d(TAG, "hasNestedScrollingParent");
return childHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
final boolean b = childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
Log.d(TAG, String.format("dispatchNestedScroll , dxConsumed = %d, dyConsumed = %d, dxUnconsumed = %d, dyUnconsumed = %d, offsetInWindow = %s", dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, Arrays.toString(offsetInWindow)));
return b;
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
final boolean b = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
Log.d(TAG, String.format("dispatchNestedPreScroll , dx = %d, dy = %d, consumed = %s, offsetInWindow = %s", dx, dy, Arrays.toString(consumed), Arrays.toString(offsetInWindow)));
return b;
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
Log.d(TAG, String.format("dispatchNestedFling , velocityX = %f, velocityY = %f, consumed = %b", velocityX, velocityY, consumed));
return childHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
Log.d(TAG, String.format("dispatchNestedPreFling , velocityX = %f, velocityY = %f", velocityX, velocityY));
return childHelper.dispatchNestedPreFling(velocityX, velocityY);
}
}
可以看到,NestedScrollingChild
接口中的方法,都委托给NestedScrollingChildHelper
去实现了,根本就不用我们来做。其实在Lollipop
版本以上,View中是有这些方法的,只是我们要兼容Lollipop
以下的版本,所以要自己来实现这个接口。
主要的逻辑,就在onTouchEvent
方法中了。如果之前有重写过这个方法的经验,其实一点都不复杂。
- 在
ACTION_DOWN
中,记录了一个按下的位置。 - 在
ACTION_MOVE
中,计算出偏移量,然后将这个偏移量,通过dispatchNestedPreScroll
方法,传递给父View(当然,是需要实现NestedScrollingParent
的父View),稍后会贴出父View中,在收到通知后,是怎么处理的。 - 如果被有被父View消费,那么偏移量需要减去被父View消费掉的。
- 根据偏移量移动子View。
下面看父View是怎么实现的。
public class NestedParentView extends FrameLayout implements NestedScrollingParent {
public static final String TAG = NestedParentView.class.getSimpleName();
private NestedScrollingParentHelper parentHelper;
public NestedParentView(Context context) {
super(context);
}
public NestedParentView(Context context, AttributeSet attrs) {
super(context, attrs);
}
{
parentHelper = new NestedScrollingParentHelper(this);
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
Log.d(TAG, String.format("onStartNestedScroll, child = %s, target = %s, nestedScrollAxes = %d", child, target, nestedScrollAxes));
return true;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
Log.d(TAG, String.format("onNestedScrollAccepted, child = %s, target = %s, nestedScrollAxes = %d", child, target, nestedScrollAxes));
parentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(View target) {
Log.d(TAG, "onStopNestedScroll");
parentHelper.onStopNestedScroll(target);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
Log.d(TAG, String.format("onNestedScroll, dxConsumed = %d, dyConsumed = %d, dxUnconsumed = %d, dyUnconsumed = %d", dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed));
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// 应该移动的Y距离
final float shouldMoveY = getY() + dy;
// 获取到父View的容器的引用,这里假定父View容器是View
final View parent = (View) getParent();
int consumedY;
// 如果超过了父View的上边界,只消费子View到父View上边的距离
if (shouldMoveY <= 0) {
consumedY = - (int) getY();
} else if (shouldMoveY >= parent.getHeight() - getHeight()) {
// 如果超过了父View的下边界,只消费子View到父View
consumedY = (int) (parent.getHeight() - getHeight() - getY());
} else {
// 其他情况下全部消费
consumedY = dy;
}
// 对父View进行移动
setY(getY() + consumedY);
// 将父View消费掉的放入consumed数组中
consumed[1] = consumedY;
Log.d(TAG, String.format("onNestedPreScroll, dx = %d, dy = %d, consumed = %s", dx, dy, Arrays.toString(consumed)));
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
Log.d(TAG, String.format("onNestedFling, velocityX = %f, velocityY = %f, consumed = %b", velocityX, velocityY, consumed));
return true;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
Log.d(TAG, String.format("onNestedPreFling, velocityX = %f, velocityY = %f", velocityX, velocityY));
return true;
}
@Override
public int getNestedScrollAxes() {
Log.d(TAG, "getNestedScrollAxes");
return parentHelper.getNestedScrollAxes();
}
}
其实也很清晰,接口NestedScrollingParent
部分委托给NestedScrollingParentHelper
实现,在本例中,我们重点关注onNestedPreScroll
这个方法。这个方法就是在子View中调用dispatchNestedPreScroll
之后被调用,除了参数offsetInWindow
由Helper类控制,其他的参数都是一样的。
父View获取到子View给的dy
之后,看要消费多少,把消费的量设置到consumed
数组中即可,很简单。
至此这个小例子就写完了,希望能让大家有所启发。