挥舞着指尖,谱写指尖的艺术
由于自己的能力功底有限,就把我所理解的,所知道的记录下来;
1.Toch事件被封装成了MotionEvent 对象,包括Tonch的位置,时间,历史记录,多点触控;
2.事件类型分为:ACTION_DOWN(按下),ACTION_UP(抬起),ACTION_MOVE(移动)等,这三个较为常用;
3.dispatchTouchEvent(),表示对事件进行分发传递;
4.InterceptTouchEvent(),表示对某个事件进行拦截,就不在往下传递;
5.onTouchEvent(),表示消费事件;
首先事件从当前Activity.dispatchTouchEvent()开始传递,只要当前事件没有被拦截,就会沿着最上层的 ViewGroup 传递,交给ViewGroup来处理,ViewGroup默认不拦截事件,就会向下(子view)传递事件,父类的ViewGroup可以通过InterceptTouchEvent()返回true进行拦截事件,则不能继续往下传递事件,消耗了当前事件,如果没有被拦截,则继续往下传递,到达某个子view时,可通过onTonchEvent()返回true处理消费事件,如果当前view的Touch监听不为null,当前view可用,当前view的onTouch方法返回true,则默认消费事件,不在调用onTonchEvent方法。如果事件从上至下未被拦截,并且子view也没有消费事件,事件就会向上传递,父容器ViewGroup就可以消耗处理掉这个时间,如果ViewGroup还不处理事件,则继续往上传递给Activty的onTonchEvent()进行处理。onTonchEvent()中的MOVE,UP,DOWN里,但凡有一个返回的是false,则不会继续往下执行,例如down中返回了false,那么up,move不会得道执行。
btn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
return true;
}
});
可以看到这里的返回值为true,当前btn是可用状态,也设置了监听事件,onTouch也返回了true,那么他会向上传递由ViewGroup来决定是否拦截,不拦截再往下传递,传递到当前view时,机会自己消耗事件,不再往下传递。
来看看部分源码#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
// ...
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;
}
看到第一个if语句,就跟刚才说的一模一样了,
li.mOnTouchListener.onTouch(this, event)
1.由于刚刚btn事件里的Touch返回了true,这里是true,三个条件成立,则result = true;第二个if语句(false && onTouchEvent)直接不会执行该语句,待会再分析onTouchEvent方法;所以这里返回了true。那么就会调用它本身的move,up,click的事件,不在往下传递。
2.如果touch返回的是false,那么就会执行到第二个if语句中,那么就要判断onTouchEvent()是否返回true,来决定result的值。
先上代码,再来分析
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 省略...
//判断当前view是否有click事件,单击/长按,进入Touch事件
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
//手势:抬起
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//处理点击事件
perfoTrmClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
//手势:按下
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
//手势:异常退出
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
//手势:移动
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
如果进入了if语句,要执行move等手势时,如果这些手势有一个实现了false,则进行拦截,后序手势不在触发,只有全为true时,后序手势会触发。
当我们实现onTouch监听时,手势都为true的返回,点击View滑动时,会触发
ACTION_DOWN->ACTION_MOVE->ACTION_UP
可以看到ACTION_UP中实现了perfoTrmClick()方法,这里面就是处理了view的点击事件。
所以得出结论:点击事件是在up中执行,up是最后触发的,所以点击事件为touch监听中最后触发。
dispatchTouchEvent执行流程
onTouchEvent()处理流程
本文中没提到View代理,想深入了解的可移步其他文章。
上面提到的都是关于view事件是否消耗的过程,现在来看看拦截的过程
public class DiyViewGroup extends ViewGroup implements View.OnTouchListener{
private Context mContext;
//拦截事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
}
我这里自定义了ViweGroup,重写了onInterceptTouchEvent方法,返回了false,则默认不拦截,继续向下传递事件,当返回true时,ViweGroup下的子view的各种手势事件将失效,因为收不到事件的传递。这时会调用ViewGroup自身的MOVE,UP,DOWN;
回到开头的问题上
最后鸣谢各大平台上的博客提供参考