一:前言

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. 深入分析 Java 中的中文编码问题 (文章来自网络)

    许令波,developerWorks 中国网站最佳作者,现就职于淘宝网,是一名 Java 开发工程师.对大型互联网架构设计颇感兴趣,喜欢钻研开源框架的设计原理.有时间将学到的知识整理成文章,也喜欢记录 ...

  2. 关于ligerUI中ligerTree代码中的一个bug,造成该控件无法通过url的POST方式加载数据

    该bug造成ligerTree参数中的method无论你怎么设置都只能用get方式提交 由于本人水平有限,只是找到原因,但无法修正 ligerUI v1.1.9 版本中的ligerui.all.js文 ...

  3. (二)Android 基本控件

    第一节:View 视图组件 Andorid 中的View 视图组件,实现类是android.view.View 类,是绝大多数图形显示类的父类,提供了大量的方法和属性.在View 类下,有很多子类,如 ...

  4. react native android 开发,基础配置笔记。

    一.React-native-device-info https://github.com/rebeccahughes/react-native-device-info 二.修改App名称 三.定位权 ...

  5. (转)UIColor 的使用

    os开发-UIColor的使用. 在ios开发中,经常遇到对UIColor的相关操作. 比如这样 self.backgroundColor = [UIColorredColor]; 这里的redCol ...

  6. foreach的一点理解

    首先什么样的数据才能实现foreach 1 实现IEnumerable这个接口 2 有GetEnumerable()这个方法 然后为啥实现这个接口或者有这个方法就可以实现foreach遍历 首先我先用 ...

  7. CentOS 6.4 64位 安装 jdk 6u45

    准备: 1.下载历史版本jdk 地址: http://java.sun.com/products/archive/ 下载的版本 jdk-6u45-linux-x64-rpm.bin  Linux x6 ...

  8. 浅谈页面无刷新技术ajax

    现在一般网站都是用ajax来实现页面无刷新操作的. 什么是无刷新:ajax可以实现页面与后台的数据交互,用户完全感觉不出页面有任何的刷新,这就是AJAX的无刷新. ajax方法实现: 可以对ajax进 ...

  9. php精粹-编写高效的php代码 --- php设计模式

    1.选择一个最合适的设计模式 没有任何事物是完美的,也没有人说过设计模式一个严格的放之四海而皆准的解决方法.因此你可以改变这些模式,使它们更适合手头的工作.对于某些设计模式而言,他们就是所属程序固有的 ...

  10. jquery mobile页面跳转后,必须重新刷新页面js方可有效

    最近在做个项目,用到jquery mobile,很陌生对他,问题一个个的来,那就要一个个解决,找了一天这个问题,放可明白:首先明白jqm里面页面跳转默认都是通过ajax请求的,必须重新刷新页面js方可 ...