ScrollView 继承自 FrameLayout,是什么让 FrameLayout 变成了可以滚动,可 fling 的呢?
是因为 onInterceptTouchEvent 和 onTouchEvent 的重写。
ScrollView 只能包含一个子视图,常常是一个线性布局。ScrollView 所关心的是,子视图是否消费了 Down 事件。不关心的是,子视图内部又经历了多少次事件分发。
当子视图不消费 DOWN 事件
onInterceptTouchEvent 函数会被调用。
(先无视掉无关紧要的代码:诸如简单判断,确保滚动结束,确保父视图不拦截事件,边缘效果,多点触控,内部滚动,越界滚动,回收工作等等。)
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
}
// DOWN 事件产生时,一般 mIsBeingDragged 已经被重置为 false 了
return mIsBeingDragged;
DOWN 事件不会被拦截,在子视图中寻找能够接收到事件的是视图,让它处理。
根据假设,子视图不消费 DOWN 事件,最后由 ScrollView 的 onTouchEvent 处理。
case MotionEvent.ACTION_DOWN: {
mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
break;
}
return true;
ScrollView 会消费掉 Down 事件(否则将收不到后续事件),并记录位置。
MOVE 事件
由于子视图没有消费 DOWN 事件,即 touchTarget 为 null,后续事件将被拦截并由 ScrollView 的 onTouchEvent 处理。
case MotionEvent.ACTION_MOVE: {
final int y = (int) ev.getY(activePointerIndex);
// 相对坐标,绝对距离
int deltaY = mLastMotionY - y;
// 如果移动距离大于最小距离,则标识为正在拖拽,并消耗掉这段距离
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
mIsBeingDragged = true;
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}}
// 如果正在拖拽
if (mIsBeingDragged) {
final int range = getScrollRange();
if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)) {}
break;
// !!! ---- MOVE 和 UP 的返回值有何意义? ---- !!!
return true;
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
int newScrollY = scrollY + deltaY;
onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
}
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
super.scrollTo(scrollX, scrollY);
}
如果移动距离小于 TouchSlop,则不做任何响应。
如果移动距离大于 TouchSlop,则确认正在拖拽,并消耗这段距离。
如果正在拖拽,则更新 mScrollY 并重画。
UP 事件
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
// 计算滑动速度,并 fling
// 下拉向上滚,所以速度是取负值
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
flingWithNestedDispatch(-initialVelocity); }}
// 重置
mActivePointerId = INVALID_POINTER;
endDrag();
}
private void flingWithNestedDispatch(int velocityY) {
if (canFling) { fling(velocityY); }
}
public void fling(int velocityY) {
int height = getHeight() - mPaddingBottom - mPaddingTop;
int bottom = getChildAt(0).getHeight();
mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,Math.max(0, bottom - height), 0, height/2);
}
滑动日志
4-16 13:10:15.497 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 0
04-16 13:10:15.497 5038-5038/ivolianer.viewgroup E/result: LinearLayout dispatch event 0
04-16 13:10:15.497 5038-5038/ivolianer.viewgroup E/result: ScrollView onTouch dispatch event 0
04-16 13:10:15.532 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 2
04-16 13:10:15.532 5038-5038/ivolianer.viewgroup E/result: ScrollView onTouch dispatch event 2
04-16 13:10:15.549 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 2
04-16 13:10:15.549 5038-5038/ivolianer.viewgroup E/result: ScrollView onTouch dispatch event 2
04-16 13:10:15.560 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 2
04-16 13:10:15.561 5038-5038/ivolianer.viewgroup E/result: ScrollView onTouch dispatch event 2
04-16 13:10:15.561 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 1
04-16 13:10:15.561 5038-5038/ivolianer.viewgroup E/result: ScrollView onTouch dispatch event 1
当子视图消费 DOWN 事件(比上面的情况复杂)
interceptTouchEvent 被调用
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
}
不拦截 DOWN 事件,寻找能接收到事件的子视图,让它处理。
根据假设,子视图消费 DOWN 事件,ScrollView 本身的 onTouchEvent 不会被调用。
MOVE 事件
因为 target 不为 null,intereceptTouchEvent 被调用。
case MotionEvent.ACTION_MOVE: {
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0){
mIsBeingDragged = true;
mLastMotionY = y;
}
break;
}
return mIsBeingDragged;
如果滑动距离始终小于 TouchSlop
事件不会被拦截。
所有 MOVE 事件都会 TouchTarget 处理。
最后的 UP 事件也会传递给 TouchTarget。
日志
04-16 13:12:03.104 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 0
04-16 13:12:03.104 5038-5038/ivolianer.viewgroup E/result: LinearLayout dispatch event 0
04-16 13:12:03.126 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 2
04-16 13:12:03.126 5038-5038/ivolianer.viewgroup E/result: LinearLayout dispatch event 2
04-16 13:12:03.176 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 2
04-16 13:12:03.176 5038-5038/ivolianer.viewgroup E/result: LinearLayout dispatch event 2
04-16 13:12:04.101 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 1
04-16 13:12:04.101 5038-5038/ivolianer.viewgroup E/result: LinearLayout dispatch event 1
如果滑动距离大于 TouchSlop
mIsBeingDragged = true,并拦截事件。
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
后续的 MOVE 事件也会被拦截。
拦截事件的实质
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child,target.pointerIdBits)) {
handled = true;
}
把事件传递给 TouchTarget 时。
如果不拦截,正常分发事件。
如果拦截了,事件会被替换成 CANCEL 事件。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL){
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
return handled;
}
并且会重置 TouchTarget ,这会导致后续 UP 事件也无法收到。
if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked ==MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
}
日志
04-16 13:01:47.191 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 0
04-16 13:01:47.195 17678-17678/ivolianer.viewgroup E/result: LinearLayout receive event 0
04-16 13:01:47.231 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 2
04-16 13:01:47.231 17678-17678/ivolianer.viewgroup E/result: LinearLayout receive event 2
04-16 13:01:47.247 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 2
04-16 13:01:47.247 17678-17678/ivolianer.viewgroup E/result: LinearLayout receive event 3
04-16 13:01:47.264 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 2
04-16 13:01:47.264 17678-17678/ivolianer.viewgroup E/result: ScrollView onTouch receive event 2
04-16 13:01:47.328 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 2
04-16 13:01:47.328 17678-17678/ivolianer.viewgroup E/result: ScrollView onTouch receive event 2
04-16 13:01:47.328 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 1
04-16 13:01:47.328 17678-17678/ivolianer.viewgroup E/result: ScrollView onTouch receive event 1
BB了这多,总结下:
ScrollView 给了你传递 DOWN 和 UP 事件的机会,为了让子视图有机会实现点击或长按。但当你企图传递 MOVE 事件时(滑动距离大于 TouchSlop),他会无情拦截下来,并重置 TouchTarget。
网友评论