View 是 Android 中所有控件的基类,ViewGroup 也继承了 View。
Android 中,x 轴和 y 轴的正方向分别为右和下。
(left, top): View 左上角原始坐标
(right, bottom): View 右下角原始坐标(margin 会影响 left/top/right/bottom)
(x, y): View 左上角最终坐标
translationX: View 左上角横向偏移量
translationY: View 左上角纵向偏移量
x = left + translationX
y = top + translationY (setX/Y() 时其实就是改变 translationX/Y 的值)
width = right - left
height = bottom - top
典型事件:ACTION_DOWN, ACTION_MOVE,ACTION_UP
意思也很容易理解,分别是落,动,起
一次触摸会触发一系列事件:
- 点击屏幕后离开松开:DOWN -> UP
- 点击屏幕滑动再松开:DOWN -> MOVE ->…-> MOVE -> UP
通过 MotionEvent 获得点击事件的坐标:
- getX / getY : 相对于当前 View 左上角的 x 和 y 坐标
- getRawX / getRawY:相对于手机屏幕左上角的 x 和 y 坐标
- (View.getX / getY 获得的是相对于父容器的 x 和 y 坐标)
滑动的最小距离,若没达到,则不认为是滑动,默认 8dp。
速度追踪,用于追踪手指在滑动过程中的速度。
在 View 的 onTouchEvent 方法中:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);// 1000ms内划过的像素数
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
回收:
velocityTracker.clear();
velocityTracker.recycle();
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
一般监听滑动相关,在 onTouchEvent 中自己实现,若是监听双击,则使用 GestureDetector。
弹性滑动对象,用于实现 View 的弹性滑动,即有过过渡效果的滑动,与 View 的 computeScroll 方法配合使用。(下面会详细介绍)
- scrollTo/scrollBy
- 操作简单,适合对 View 内容的滑动
- 典型应用:ScrollView 的滑动- 动画
- 操作简单,适合没有交互的 View 和实现复杂的动画效果- 改变 LayoutParams
- 操作稍复杂,适合有交互的 View
典型用法:
private Scroller mScroller = new Scroller(context);
public void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.forceFinished(true);
mScroller.startScroll(scrollX, 0, delta, 0, 5000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollBy(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
当我们调用 startScroll() 时,Scroller 内部其实什么也没做,只是保存了我们传递的几个参数。所以仅仅调用 startScroll() 是不会产生滑动的,因为它内部没有做与滑动相关的事,Scroller 产生滑动,其实是因为 invalidate()。
invalidate() 会导致 View 重绘,在 View 的 Draw 方法中会调用 computeScroll(),computeScroll() 在 View 中是一个空实现,所以我们要自己实现,正如上所示。
过程:
invalidate() -->
View 重绘, draw() -->
computeScroll() ==>
scrollTo() -->
postInvalidate() 再次重绘 -->
…
computeScrollOffset() 会根据时间流逝算出当前的 ScrollX/Y,返回 true 即表示滑动还没有结束,继续滑动。
动画本身就是一种渐进的过程,所以利用动画天然就具有弹性效果。
另外通过动画,也可以实现类似 Scroller 对 View 的弹性滑动。
核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用 Handler 或 View 的 postDelayed 方法,也可以使用线程的 sleep 方法。
"main@11086" prio=5 tid=0x2 nid=NA runnable
java.lang.Thread.State: RUNNABLE
// ViewGroup
at com.gdeer.gdtesthub.touchevent.deliver.MyTextView.dispatchTouchEvent(MyTextView.java:25)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
// DecorView.superDispatchTouchEvent
at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:440)
at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1830)
// Activity
at android.app.Activity.dispatchTouchEvent(Activity.java:3400)
at com.gdeer.gdtesthub.touchevent.deliver.TouchDeliverActivity.dispatchTouchEvent(TouchDeliverActivity.kt:18)
// DecorView.dispatchTouchEvent
at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:398)
at android.view.View.dispatchPointerEvent(View.java:12752)
// ViewRootImpl
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5106)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4909)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4585)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4642)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7092)
at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7061)
at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7022)
// InputEventReceiver
at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7195)
at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:186)
// Looper
at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-1)
at android.os.MessageQueue.next(MessageQueue.java:326)
at android.os.Looper.loop(Looper.java:160)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Method.java:-1)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Looper -->
InputEventReceiver -->
ViewRootImpl -->
DecorView.dispatchTouchEvent -->
Activity -->
DecorView.superDispatchTouchEvent -->
ViewGroup
InputEventReceiver 收到事件后转给 ViewRootImpl 处理;ViewRootImpl 传给 DecorView;DecorView 通过 dispatchTouchEvent 将事件传给 Activity(dispatchTouchEvent 会调用 window.callback,Activity 就是一个 window.callback);Activity 再传给 DecorView,接着 DecorView 就按照事件分发机制去分发事件(superDispatchTouchEvent,即 ViewGroup 的事件分发机制)。
如果一个 View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 就会被调用,若全部 View 都不处理这个事件,那这个事件将最终传递给 Activity 来处理,即 Activity 的 onTouchEvent 会被调用。
如果设置了 OnTouchListener,onTouch 会先于 onTouchEvent 执行:
onTouchListener > onTouchEvent > onClickListener
处理 down 事件:
boolean dispatchTouchEvent(MotionEvent ev) {
// 决定自己是否拦截
boolean handled = false;
boolean intercepted = onInterceptTouchEvent(ev);
// 传递给子view
if (!intercepted) {
handled = child.dispatchTouchEvent();
}
// 自己收尾
if (!handled) {
handled = onTouchEvent();
}
return handled;
}
处理 move 事件:
boolean dispatchTouchEvent(MotionEvent ev) {
// 决定自己是否拦截
boolean handled = false;
boolean intercepted = false;
if (mFirstTouchTarget == null) {
intercepted = true;
} else if (disallowIntercept) {
intercepted = false;
} else {
intercepted = onInterceptTouchEvent(ev);
}
// 传递给子view
if (!intercepted) {
handled = child.dispatchTouchEvent();
}
// 自己不收尾
return handled;
}
一些概念:
- 是否允许拦截:FLAG_DISALLOW_INTERCEPT 标记位是否为 true
- 是否拦截:onInterceptTouchEvent 返回结果
- 是否处理:onTouchEvent 是否调用
- 是否处理成功:onTouchEvent 返回结果
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
......
}
mFirstTouchTarget
会在 ViewGroup 的子 view 处理触摸事件成功后赋值,mFirstTouchTarget != null
说明自己的子 view 处理成功过事件(onTouchEvent 返回了 true)。FLAG_DISALLOW_INTERCEPT
为 0x80000。FLAG_DISALLOW_INTERCEPT
会在执行过 requestDisallowInterceptTouchEvent(true)
后,被添加到 mGroupFlags 中。(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0
即表示 mGroupFlags 拥有 FLAG_DISALLOW_INTERCEPT
这个 FLAG。则上述代码的含义为,当要处理的事件是 down 事件,或自己的子 view 处理成功过之前的事件,则会去判断是否允许拦截,如果允许拦截,则去判断是否拦截。
如果不是 down 事件,且自己的 子 view 没有处理成功过之前的事件,那就不用判断了,直接决定自己拦截。
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
......
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
......
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
......
}
......
}
这段代码,将事件传递给子 view。
dispatchTransformedTouchEvent
中会执行 child.dispatchTouchEvent
。
如果 dispatchTransformedTouchEvent
返回了 true,说明子 view 成功处理了该事件,在 addTouchTarget
中,会给 mFirstTouchTarget
赋值。
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
......
}
......
return handled;
}
如果子 view 没有处理成功,即 mFirstTouchTarget == null
,那会在 dispatchTransformedTouchEvent
中调用自己的 onTouchEvent
方法。
view 没有 onInterceptTouchEvent 方法,dispatchTouchEvent 里会直接调用 onTouchEvent 方法。
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
......
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
......
return result;
}
由上可以看出,在 view enabled
的时候,OnTouchListener.onTouch 会先于 onTouchEvent 调用,如果 onTouch 返回了 true,那 onTouchEvent 就不会调用。
在 view disabled
的时候,OnTouchListener.onTouch 不会调用,但 onTouchEvent 还是会调用。
常见的三种滑动冲突场景:
- 外部滑动与内部滑动的方向不一致
- 外部滑动与内部滑动的方向一致
- 以上两种的嵌套
对场景一:根据滑动是水平滑动还是竖直滑动来判断到底是由谁来拦截事件。由滑动轨迹的起终点的坐标即可判断为水平还是竖直(距离差、角度、速度差等)。
对场景二:根据业务需求具体分析。
对场景三:以上两者的混合。
指事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截。需重写父容器的 onInterceptTouchEvent 方法。
伪代码:
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要当前点击事件){
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
return intercepted;
}
- 对于 ACTION_DOWN 事件,父容器必须返回 false,因为一旦拦截了 ACTION_DOWN,后续的 ACTION_MOVE,ACTION_UP 都会交给父容器处理。
- 对于 ACTION_UP 事件,父容器也返回 false,一旦拦截,子元素的 onClick 事件便无法触发。父容器(ViewGroup)比较特殊,一旦开始拦截(ACTION_DOWN 的 onTouchEvent 返回 true),那么后续事件也都会交给它处理(并且 onInterceptTouchEvent 不会再被调用),而 ACTION_UP 作为最后一个事件也一定能够传到父容器(因为 onInterceptTouchEvent 不会被调用,所以返回 false 也就不起作用)。
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,需要配合 requestDisallowInterceptTouchEvent 方法来设置(控制 FLAG_DISALLOW_INTERCEPT 标志位,设置后 ViewGroup 将无法拦截除了 ACTION_DOWN 以外的点击事件,ACTION_DOWN 不受此标志位约束),需重写子元素的 dispatchTouchEvent 方法(子元素,view 无 onInterceptTouchEvent 方法)。
子元素:
public boolean dispatchTouchEvent(MotionEvent event){
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
case MotionEvent.ACTION_MOVE:
if(父容器需要当前点击事件){
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
因为 ACTION_DOWN 不受 requestDisallowInterceptTouchEvent 方法影响,父元素也要做相应处理:
public boolean onInterceptTouchEvent(MotionEvent event){
int action = event.getAction();
if (action == ACTION_DOWN){
return false;
} else {
return true;
}
}