今天准备完结View事件分发了,码蛋的,果然自己去认真过一遍才是王道!晚上做梦都TM能想起这三个方法的先后顺序。。
好了,我们简单看看View事件分发流程的第二个方法,绑定OnTouchListener重写的onTouch()方法。这个方法是我们自己重写的方法,也没什么好讲的,我们只需要记住一点,如果我们的onTouch()方法返回了true,那么就不会执行View的onTouchEvent()方法了,有疑惑的可以去看看上一篇笔记中介绍dispatchTouchEvent(MotionEvent event)返回true的两个条件判断。如果第一个条件判断成立了直接返回true,也就不会往后继续执行onTouchEvent()的条件判断了。
继续看最后一个方法--onTouchEvent(),我们先修改我们自定义View的onTouchEvent()中的代码,如下:
在这个方法中,在ACTIOM_DOWN中我们返回了false,运行程序看下打印的值:
以上是我点击按钮2次所打印的值,你会发现,ACTION_MOVE和ACTION_UP都没有打印出来,也就是说后续的事件我们的View都没有得到,这个是为什么呢?仔细回忆一下,还是dispatchTouchEvent()中的那个第二个返回true的条件判断,如果onTouchEvent()返回了true,那么它也返回true,否则返回false,表明我们这个View并不需要处理这个事件,自然之后的Move和Up事件都不再给我们的View了。也就是说,如果你希望自己的View处理Move和Up事件,你的Down事件处理的返回值必须是true(还有点迷糊的话就去看看上一篇笔记中最后那部分的说明吧)。
继续修改onTouchEvent()代码,如下:
将Down事件的返回值改为true,Move事件的返回值改为false,结果会怎么样呢?这个时候我心里都想鄙视自己:逗比,Down返回true,Move返回false,结果肯定是只接收Down和Move事件,然后Up事件接收不到嘛!我们看打印结果:
我擦,这尼玛是为什么?明明在Move事件返回了false,为什么还会接收到Up事件,这和之前说的不是冲突了么?其实不然,一个View的onTouchEvent()是否接收touch事件是由其ACTION_DOWN事件返回值决定的,如果在Down事件返回false,那么之后的Move和Up事件都不再接收,反之,如果Down中返回了true,表示这个View要处理这些事件,不管Move和Up返回值如何,都会接收这些事件。(有疑问的朋友也可以自己试一试给move和up事件的返回值改成false看看)
好了,继续来研究下onTouchEvent()这个方法的源码吧。。。 源码如下:
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
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);
}
if (!mHasPerformedLongPress) {
// 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)) {
performClick();
}
}
}
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();
}
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();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// 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;
}
额,看方法注释:实现此方法用来处理屏幕触摸事件(这次绝对没翻译错。。)
首先看第10-18行,如果当前的View是Disable的(被禁用),进入if语句内,如果当前的事件是触摸抬起事件,那么就设置按下状态,然后返回是否可点击或者长按的状态。只要它可点击或者可长按,那么也会消耗掉触摸事件,只是不会对这些事件作出相应(也就是光吃不干活?)。
再看20-24行,如果当前的View有设置触摸代理,那么就将事件交给触摸代理处理。
好了,看26行,如果当前的View是可点击或者可长按,返回true,否则返回false;
我们看看当View可点击或者长按时候,事件的处理是怎么样的。
首先看ACTION_DOWN:
if (performButtonActionOnTouchDown(event)) {
break;
}
这个方法一般都是返回false,为什么呢?我们去看看它的源码:
/**
* Performs button-related actions during a touch down event.
*
* @param event The event.
* @return True if the down was consumed.
*
* @hide
*/
protected boolean performButtonActionOnTouchDown(MotionEvent event) {
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) {
return true;
}
}
return false;
}
这个方法的含义就是如果你按下了右键,那么这个ACTION_DOWN事件就会被消费掉(码蛋,,想了半天也不知道我要怎么右键按下这个View。。境界不够,知道的朋友可以教我下,感激不尽)
看下一行代码boolean isInScrollingContainer = isInScrollingContainer();这个地方注释写得很清楚,遍历View当前的View结构树来判断当前View是否在一个可滑动的容器中。
如果是在一个可滑动的容器内,那么首先给当前的标志设置为PFLAG_PREPRESSED表示用户准备点击,然后发送一个延迟的消息加入到队列中,用来判断这个按下的意图是滑动还是点击,这个延迟时间为ViewConfiguration.getTapTimeout()(这个值在各个版本下好像不大一样,有些是150ms,有些是180ms),如果在这个延迟的时间段内用户触发了ACTION_Move事件,则从消息队列中删除该条消息,进入ACTION_MOVE事件(视为滑动事件)。否则执行按下状态(视为点击事件),并且触发长按事件的检查。具体源码如下:
private final class CheckForTap implements Runnable {
public void run() {
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true);
checkForLongClick(ViewConfiguration.getTapTimeout());
}
}
再来看看长按事件检查的源码:
private void checkForLongClick(int delayOffset) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
这个思路和检查按下事件差不多,都是一个延时,只是这里面有这样一行代码是不一样的:
mPendingCheckForLongPress.rememberWindowAttachCount();
这行代码的作用是什么呢?我们去看看CheckForLongPress的源码:
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}
这个记录了mWindowAttachCount这个属性的值,它是记录当前View的attach次数,要知道长按触发事件比较长,如果在这段时间内,Activity发生了变化,调用了onPause()或者onReStart()那么我们的长按事件应该失效才对,这也就有必要记录这个mWindowAttachCount属性值了,只有当形成长按事件时候当前的mWindowAttachCount等于之前记录的mOriginalWindowAttachCount值,才执行长按操作performLongClick(),如果用户有绑定onLongClickListener,返回true,然后将长按标志mHasPerformedLongPress置为true。
如果当前的View不在滑动视图内,直接执行设置按下状态并检查长按操作。
好了。继续看ACTION_MOVE的代码:
final int x = (int) event.getX();
final int y = (int) event.getY();
// 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;
这块代码就好看多了。。。先记录用户触摸的坐标位置,然后判断用户触摸的当前位置是不是在View里面,这个方法注释是这样的:在判断是否移动到View外面时宽容一些,也就是在View的外面再加mTouchSlop个像素大小,(就好像你去买肉多给你点一样的。。。这就是宽容)。如果当前位置滑动到外面去了,那么移除之前的Down事件中的点击的消息,如果是按下状态(也就是说在检查长按操作了),移除掉长按事件的消息,并且将按下的状态给移除,置为false。
继续看ACTION_UP的代码:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed)
首先看这个条件,只要PFLAG_PREPRESSED(在Down事件中,如果当前View是在一个滚动的容器内,那么此值被设置)或者PFLAG_PRESSED(在Down事件中,如果View不是在一个滚动的容器内,此值被设置)其中一个为真,就进入条件判断内。
// 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);
}
先将获取焦点的标志focusTaken设置为false,判断如果当前View可以获得焦点而且当前没有取得焦点,人那么请求获取焦点。如果之前是prepressed状态(在滚动容器内),直接setPressed(true)设置按下状态(虽然这个时候用户已经将手给抬起来了,但是我们在进行实际的点击操作的时候还是应该让用户看到这个按下的状态)。
继续向下看
if (!mHasPerformedLongPress) {
// 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)) {
performClick();
}
}
}
如果长按标志为false,也就是Up事件发生时,还没达到长按操作的要求,那么视为点击事件,移除长按检查的消息。如果focusTaken为false,也就是说View没有去重新请求焦点(在这之前就是按下状态),那么进入条件判断内,发送一个延迟的消息来执行点击效果,这块注释说的很清楚:发送一个延迟的消息而不是立即执行点击操作,是为了让那些可视化的状态更新(比如:按下状态)。。之后的代码就没什么需要深究的了,就是去发送一个消息取消按下的状态(这里也是一个延迟消息,这个延迟的事件就是按下状态显示的时间),然后移除掉这个点击事件的消息。
额。。ACTION_CANCEL的话就没必要看了,就是一个取消按下状态,移除点击检查和长按检查的消息。
这篇笔记的长度有点那啥了。。。下一篇再看看这个东西在开发中到底有什么用吧
最后整理了一下这个触摸事件的流程(画图太操蛋了。。。直接写出来了。。 原谅我吧 阿门~~~):