一:前言

View是Android中最基本的UI单元.

当一个View接收到了触碰事件时,会调用其onTouchEvent方法.方法声明如下:

1
2
3
4
5
6
7
/**
 * 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) {

了解下View怎么处理onTouchEvent方法是很有必要的.

在具体的看View是怎么处理触碰事件之前,从用户交互上,我们先需要对View处理的事件有一些期望:

(1)能够区分将用户的触碰事件是点击还是滑动区别开来.

(2)能够将点击与长按区别开来.

二: 处理流程分析

View#onTouchEvent方法主要做了如下处理:

 (1) 如果此view被禁用了. (如果是触碰完成事件则设置按下状态),然后返回是否可点击.

(中间的注释的意思为:一个可点击的View虽然禁用了,但是还是要把事件消耗掉,只是不响应它们而已.

1
2
3
4
5
6
7
8
9
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));
}

(2) 如果此View有触碰事件处理代理,那么将此事件交给代理处理:

1
2
3
4
5
if (mTouchDelegate != null) {
         if (mTouchDelegate.onTouchEvent(event)) {
             return true;
         }
}

(3)如果不可点击(既不能单击,也不能长按)则直接返回.false

(4)可点击时,处理触控事件.根据,按下,移动,取消,抬起,这些基本触摸事件来分别处理.

 它们其中又有很强的关联性.

 

三:触摸事件分析:

(3.1)当触控开始时:即处理 case MotionEvent.ACTION_DOWN:分支.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;

上面分支代码的第一个调用,performButtonActionOnTouchDown(event) 一般的设备都是返回false.

因为目前的实现中,它是处理如鼠标的右键的.(如果此View响应或者其父View响应右键菜单,那么就此事件就被消耗掉了.)

可以看下这个方法的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
     * 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;
    }

对于MotionEvent的BUTTON_SECONDARY常量,对于鼠标中的按键来说,是指右键.

第二个方法调用:isInScrollingContainer(),

   它的注释已经写得很明白了,就是遍历整个View树来判断当前的View是不是在一个滚动的容器中.

因为对于触碰事件的处理,我符合我们讲的,不能把滑动当前点击.所以先判断是不是在一个可滑动的容器中.

下面是此方法的实现代码:

1
2
3
4
5
6
7
8
9
10
public boolean isInScrollingContainer() {
       ViewParent p = getParent();
       while (p != null && p instanceof ViewGroup) {
           if (((ViewGroup) p).shouldDelayChildPressedState()) {
               return true;
           }
           p = p.getParent();
       }
       return false;
   }

检查结果有两种情况: 

 (1)如果不是在一个可滚动的容器中:

调用setPressed(true) 设置按下状态.,setPressed 主要是设置PFLAG_PRESSED标志位.后面会具体分析此方法.

检查长按.

 (2)如果是在一个可滚动的容器中:

  先设置用户准备点击这么一个标志位:PFLAG_PREPRESSED.

  然后则发送一个延迟消息来确定用户到底是要滚动还是点击.

1
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

在给定的tapTimeout时间之内,用户的触摸没有移动,就当作用户是想点击,而不是滑动.

具体的做法是,将 CheckForTap的实例mPendingCheckForTap添加时消息队例中,延迟执行.

如果在这tagTimeout之间用户触摸移动了,则删除此消息.否则:执行按下状态.然后检查长按.

CheckForTap消息方法如下:

1
2
3
4
5
6
7
private final class CheckForTap implements Runnable {
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            setPressed(true);
            checkForLongClick(ViewConfiguration.getTapTimeout());
        }
}

检查长按也是大概类似的思路:等等再决定.

checkForLongClick方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
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);
        }
}

CheckForLongPress消息类要单独说一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
       }
   }

因为等待形成长按的过程中,界面可能发生变化如Activity的pause及restart,这个时候,长按应当失效.

View中提供了mWindowAttachCount来记录View的attach次数.当检查长按时的attach次数与长按到形成时.

的attach一样则处理,否则就不应该再当前长按. 所以在将检查长按的消息添加时队伍的时候,要记录下当前的windowAttachCount.

 (3.2)当手指在屏幕移动时: case MotionEvent.ACTION_MOVE:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<strong> 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;</strong>

第一句注释讲的是,对于触碰是否超出边界宽容一些.所以在判断触摸中间点是否在此View中时,先将上下左右增大mTouchSlop个像素,再判断.

如果在View的外面,将处理点击消息移除.如果是已经准备长按了,则将长按的消息移除.并将View的按下状态设置为false.

看看上面调用的pointInView的实现,如下:

1
2
3
4
5
6
7
8
9
10
/**
 * Utility method to determine whether the given point, in local coordinates,
 * is inside the view, where the area of the view is expanded by the slop factor.
 * This method is called while processing touch-move events to determine if the event
 * is still within the view.
 */
private boolean pointInView(float localX, float localY, float slop) {
    return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
            localY < ((mBottom - mTop) + slop);
}

(3.3)  精简代码再分析,加深理解:

到这里我们已经分析了一个View中的触控事件的.按下,和移动了.如果初次接触可能有点晕.但是让我以一个最简单的情况把这些上面出现过的代码重新组织一下:

我们的情况就是,一个Activity中只有一个正常的Button.

所以我们View处理触控事件的代码应该如下:

当手指按下时:

1
2
setPressed(true);
checkForLongClick(0);

设置按下的效果.派发一个消息侦查用户是否准备长按.

这个时候有两种情况:

一:用户手指按下过了一段时间.也没有到处移动,所以我们认为用户是想长按.触发执行长按消息:

1
2
3
4
5
6
if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick()) {
                    mHasPerformedLongPress = true;
                }
}

这些代码,在上面分析的时候,提及到了一点.

下面再多说几句:

最外面的if判断.

主要是判断,长按是在按下的基础之上出现的.所以要isPressed(),

执行长按时,父View还在(指View层级还没有销毁),WindowAttachCount不变,指此窗口还是当初View按下时的窗口而不是重建的窗口.

最里面的判断,是判断View界面是否执行了长按,然后设置对应标志字段.

二:用户按下之后到处移动:

这个时候就执行了ACTION_MOVE分支的代码了.

if ((mPrivateFlags & PFLAG_PRESSED) != 0) 按照之前的执行流程.

因为,上面调用了setPressed(true).在些方法中,mPrivateFlags字段中的PFLAG_PRESSED标志为被启用了.

setPressed的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * Sets the pressed state for this view.
 *
 * @see #isClickable()
 * @see #setClickable(boolean)
 *
 * @param pressed Pass true to set the View's internal state to "pressed", or false to reverts
 *        the View's internal state from a previously set "pressed" state.
 */
public void setPressed(boolean pressed) {
    final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
 
    if (pressed) {
        mPrivateFlags |= PFLAG_PRESSED;
    } else {
        mPrivateFlags &= ~PFLAG_PRESSED;
    }
 
    if (needsRefresh) {
        refreshDrawableState();
    }
    dispatchSetPressed(pressed);
}

(3.4)触控完成时(即当手指都抬起来时):

这分支的代码加上注释看起来稍微有点长.我们分开来分析:

首先是检查 PFLAG_PREPRESSED 和PFLAG_PRESSED 这两个标志.如果其中一个为真则处理.

根据上面的分析我们知道这两个标志位首先是在开始触控时(即手指按下ACTION_DOWN)时设置时,

PFLAG_PREPRESSED 表示在一个可滚动的容器中,要稍后才能确定是按下还是滚动.

PFLAG_PRESSED 表示不是在一个可滚动的容器中,已经可以确定按下这一操作.

1
2
3
4
5
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED ) != 0;
boolean pressed = (mPrivateFlags & PFLAG_PRESSED) != 0;
if(pressed || prepressed){
 // 处理些事件
}

然后是看是否需要获得焦点及用变量focusTaken设置是否获得了焦点.

如果我们还没有获得焦点,但是我们在触控屏下又可以获得焦点,那么则请求获得焦点.

1
2
3
4
5
// take focus if we don't have it already and we should in touch mode.
 boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
     focusTaken = requestFocus();
}

然后,如果之前是prepressed那么现在就设置按下状态:

虽然用户在我们还没有显示按下状态的效果时就不按了.我们还是得在进行实际的点击操作时,

让用户看到按下的效果.

1
2
3
4
5
6
7
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);
}

然后是判断是否进行了长按:

如果没有,那好,移除长按的延迟消息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  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();
        }
    }
}

下面是判断有没有重新请求获得焦点,如果还没有新获得焦点,说明之前已经是按下的状态了.

派发执行点击操作的消息.这是为了在实际的执行点击操作时,让用户有时间再看看按下的效果.

之后就是派发消息来取消点击状态:

1
2
3
4
5
6
7
8
9
10
11
12
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();

ViewConfiguration.getPressedStateDuration() 获得的是按下效果显示的时间,由PRESSED_STATE_DURATION

常量指定,为64毫秒.

小结:其他的事件处理,基本是设置状态,派发消息.到这里就需要对,当前的状态,做出判断及处理.

(3.5) 接下来就是最简单的,但是也很重要的,当触控事件被系统取消:ACTION_CANCEL:

在这个事件中,只需要setPressed(false),并移除按下,及长按的延迟消息就可以了.

具体代码如下:

1
2
3
4
5
case MotionEvent.ACTION_CANCEL:
    setPressed(false);
    removeTapCallback();
    removeLongPressCallback();
    break;

四: 总结

View#onTouchEvent方法虽然只有几十行代码,但是对于我们理解触控事件的处理方法.

MotionEvent各个事件的处理方法都是有很大的帮助.

值得我们这这个方法的代码打印出来,多多学习.

 

深入Android开发之--理解View#onTouchEvent的更多相关文章

  1. Android开发进阶——自定义View的使用及其原理探索

    在Android开发中,系统提供给我们的UI控件是有限的,当我们需要使用一些特殊的控件的时候,只靠系统提供的控件,可能无法达到我们想要的效果,这时,就需要我们自定义一些控件,来完成我们想要的效果了.下 ...

  2. android开发架构理解

    1. android 开发和普通的PC程序开发的,我觉得还是不要过度设计,因为手机开发,项目相对传统软件开发就小很多,而且手机的性能有限,过度设计代码mapping需要消耗的能相对就高,而且手机开发的 ...

  3. Android 自定义View修炼-Android开发之自定义View开发及实例详解

    在开发Android应用的过程中,难免需要自定义View,其实自定义View不难,只要了解原理,实现起来就没有那么难. 其主要原理就是继承View,重写构造方法.onDraw,(onMeasure)等 ...

  4. Android 开发中的View事件监听机制

    在开发过程中,我们常常根据实际的需要绘制自己的应用组件,那么定制自己的监听事件,及相应的处理方法是必要的.我们都知道Android中,事件的监听是基于回调机制的,比如常用的OnClick事件,你了解它 ...

  5. Android 开发 -------- 自己定义View 画 五子棋

    自己定义View  实现 五子棋 配图: watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbG92ZV9KYXZjX3lvdQ==/font/5a6L5L2T ...

  6. android开发学习 ------- 自定义View 圆 ,其点击事件 及 确定当前view的层级关系

    我需要实现下面的效果:   参考文章:https://blog.csdn.net/halaoda/article/details/78177069 涉及的View事件分发机制 https://www. ...

  7. android开发之自定义View 详解 资料整理 小冰原创整理,原创作品。

    2019独角兽企业重金招聘Python工程师标准>>> /** * 作者:David Zheng on 2015/11/7 15:38 * * 网站:http://www.93sec ...

  8. android开发_view和view属性

    一.view视图的宽度和高度属性,属性值:固定和浮动两种状态 1属性为固定值 <View android:layout_width="30dp" android:layout ...

  9. Android 开发 深入理解Handler、Looper、Messagequeue 转载

    转载请注明出处:http://blog.csdn.net/vnanyesheshou/article/details/73484527 本文已授权微信公众号 fanfan程序媛 独家发布 扫一扫文章底 ...

随机推荐

  1. UML 结构图之包图 总结

    [注] 本文不是包图的基础教程, 只是包图的图形总结. 学习UML图形 推荐阅读<UML参考手册>第2版. http://www.umlchina.com/ 推荐微软的开发软件设计模型 h ...

  2. ASP.NET CompareValidator 控件在VS2012中出错的问题

    CompareValidator 控件用于将由用户输入到输入控件的值与输入到其他输入控件的值或常数值进行比较. -------如果输入控件为空,则不会调用任何验证函数,并且验证将成功.使用 Requi ...

  3. KP 佛学禅语

    1.人之所以痛苦,在于追求错误的东西. 2.如果你不给自己烦恼,别人也永远不可能给你烦恼.因为你自己的内心,你放不下. 3.你永远要感谢给你逆境的众生. 4.你永远要宽恕众生,不论他有多坏,甚至他伤害 ...

  4. 学习笔记_Java_day12_设计模式MVC(13).JavaWeb的三层框架(14)

    MVC 1. 什么是MVC MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model).视图(View)和控制器(Contr ...

  5. cognos 10.2.2 Framework manager使用”数据源”新建查询主题

    又做了一个简单的报表,就是在Framework Manager中写个sum()的sql出个报表,可以使用使用"数据源"新建查询主题 配置查询主题后修改SQL,注意全部都是大写,要和 ...

  6. Autolayout的在storyboard警告和错误

    警告 控件的frame不匹配所添加的约束, 比如比如约束控件的宽度为100, 而控件现在的宽度是110 错误 缺乏必要的约束, 比如只约束了宽度和高度, 没有约束具体的位置 两个约束冲突, 比如 1个 ...

  7. Angular2中的host

    要将Angular组件渲染成DOM中的某种东西,你需要在Angular组件中结合一个DOM元素,我们称这些叫host元素. 一个组件可以用以下方式于其host DOM元素进行交互 它可以监听其事件. ...

  8. Codevs 1690 开关灯 USACO

    1690 开关灯 USACO 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 钻石 Diamond 传送门 题目描述 Description YYX家门前的街上有N(2<=N& ...

  9. MongoDB源码分析——mongo与JavaScript交互

    mongo与JavaScript交互 源码版本为MongoDB 2.6分支     之前已经说过mongo是MongoDB提供的一个执行JavaScript脚本的客户端工具,执行js其实就是一个js和 ...

  10. javascript 事件多次绑定和删除

    同一个事件绑定多个事件处理程序(适合自己写)IE: 添加: 对象.attachEvent("on事件名","处理程序/函数名"); 执行顺序从后向前 删除: 对 ...