Android笔记:触摸事件的分析与总结----MotionEvent对象

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

一、MotionEvent对象

    当用户触摸屏幕时,将创建一个MontionEvent对象。MotionEvent包含了关于发生触摸的位置和时间的信息,以及触摸事件的其他细节。

    获取MontionEvent对象的方法有:

    1.重载Activity中的onTouchEvent(MotionEvent event)方法;

    2.View对象调用View.setOnTouchListener接口实现onTouch(View v, MotionEvent event)方法;


    获得MontionEvent对象后,可以通过以下常用方法进一步获取触控事件的具体信息:

1
2
3
4
5
6
7
8
9
     event.getAction()  //获取触控动作比如ACTION_DOWN
     event.getPointerCount();  //获取触控点的数量,比如2则可能是两个手指同时按压屏幕
     event.getPointerId(nID);  //对于每个触控的点的细节,我们可以通过一个循环执行getPointerId方法获取索引
     event.getX(nID);  //获取第nID个触控点的x位置
     event.getY(nID);  //获取第nID个点触控的y位置
     event.getPressure(nID);  //LCD可以感应出用户的手指压力,当然具体的级别由驱动和物理硬件决定的
     event.getDownTime()  //按下开始时间
     event.getEventTime()  // 事件结束时间
     event.getEventTime()-event.getDownTime());  //总共按下时花费时间


    触控对象中的主要相关常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
     /**
      * 用于多点触控进行操作 
      */
     public  static  final  int  ACTION_MASK =  0xff ;
     
     /**
      * 手指按下时
      */
     public  static  final  int  ACTION_DOWN =  0 ;
     
     /**
      * 手指放开时
      */
     public  static  final  int  ACTION_UP =  1 ;
     
     /**
      * 移动操作时
      */
     public  static  final  int  ACTION_MOVE =  2 ;
     
     /**
      * 用户无规则的操作时可能触发. 此操作用于表明,一个触摸序列在未发生任何实际操作的情况下结束.
      */
     public  static  final  int  ACTION_CANCEL =  3 ;
     
     /**
      * 触摸操作发生在窗口之外,但仍然能够找到该操作的特殊情况下设置.
      */
     public  static  final  int  ACTION_OUTSIDE =  4 ;
     
     /**
     
      */
     public  static  final  int  ACTION_POINTER_DOWN =  5 ;
     
     /**
     
      */
     public  static  final  int  ACTION_POINTER_UP =  6 ;
     
     /**
     
      */
     public  static  final  int  ACTION_HOVER_MOVE =  7 ;
     
     /**
      * Android3.1开始引入的常量,来自于输入设备(如鼠标),而非触摸屏.
      */
     public  static  final  int  ACTION_SCROLL =  8 ;

    


二、MotionEvent对象处理过程

    范例视图如下:

    wKiom1QlAaOy17zsAADq23QwuSQ833.jpg

    

    activity_main.xml代码如下: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<RelativeLayout xmlns:android= "http://schemas.android.com/apk/res/android"
     xmlns:tools= "http://schemas.android.com/tools"
     android:layout_width= "match_parent"
     android:layout_height= "match_parent"
     tools:context= ".MainActivity"  >
 
     <LinearLayout
         android:layout_width= "match_parent"
         android:layout_height= "match_parent"
         android:layout_alignParentLeft= "true"
         android:layout_alignParentTop= "true"
         android:orientation= "vertical"  >
 
         <LinearLayout
             android:id= "@+id/layout1"
             android:layout_width= "match_parent"
             android:layout_height= "0dp"
             android:layout_weight= "1"
             android:orientation= "vertical"
             android:tag= "true_Layout"  >
 
             <com.example.d_motionevent.TrueButton
                 android:id= "@+id/trueButton1"
                 android:layout_width= "wrap_content"
                 android:layout_height= "wrap_content"
                 android:tag= "true_Btn_上"
                 android:text= "true_Btn_上"  />
 
             <com.example.d_motionevent.FalseButton
                 android:id= "@+id/falseButton1"
                 android:layout_width= "wrap_content"
                 android:layout_height= "wrap_content"
                 android:tag= "false_Btn_上"
                 android:text= "false_Btn_上"  />
 
         </LinearLayout>
 
         <LinearLayout
             android:id= "@+id/layout2"
             android:layout_width= "match_parent"
             android:layout_height= "0dp"
             android:layout_weight= "1"
             android:background= "#ff00ff"
             android:orientation= "vertical"
             android:tag= "false_Layout"  >
 
             <com.example.d_motionevent.TrueButton
                 android:id= "@+id/trueButton2"
                 android:layout_width= "wrap_content"
                 android:layout_height= "wrap_content"
                 android:tag= "true_Btn_下"
                 android:text= "true_Btn_下"  />
 
             <com.example.d_motionevent.FalseButton
                 android:id= "@+id/falseButton2"
                 android:layout_width= "wrap_content"
                 android:layout_height= "wrap_content"
                 android:tag= "false_Btn_下"
                 android:text= "false_Btn_下"  />
 
         </LinearLayout>
     </LinearLayout>
 
</RelativeLayout>


    自定义Button类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package  com.example.d_motionevent;
 
import  android.content.Context;
import  android.util.AttributeSet;
import  android.util.Log;
import  android.view.MotionEvent;
import  android.widget.Button;
 
/**
 
  * @author zeng
  */
public  class  BooleanButton  extends  Button
{
     public  BooleanButton(Context context, AttributeSet attrs)
     {
         super (context, attrs);
     }
     
     protected  boolean  myValue()
     {
         return  false ;
     }
     
     @Override
     public  boolean  onTouchEvent(MotionEvent event)
     {
         String myTag =  this .getTag().toString();
         
         Log.e(myTag,  "==========="  "Button 的 onTouchEvent 方法"  "===========" );
         Log.e(myTag, MainActivity.describeEvent( this , event));
         Log.e(myTag,  "父类 onTouchEvent() = "  super .onTouchEvent(event));
         Log.e(myTag,  "该button touch = "  + myValue());
         return  myValue();
     }
}


    TrueButton类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package  com.example.d_motionevent;
 
import  android.content.Context;
import  android.util.AttributeSet;
 
/**
  * onTouchEvent返回true的Button.
  * @author zeng
  */
public  class  TrueButton  extends  BooleanButton
{
     public  TrueButton(Context context, AttributeSet attrs)
     {
         super (context, attrs);
     }
     
     @Override
     protected  boolean  myValue()
     {
         return  true ;
     }
}


    FalseButton类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package  com.example.d_motionevent;
 
import  android.content.Context;
import  android.util.AttributeSet;
 
/**
  * onTouchEvent返回false的Button.
  * @author zeng
  */
public  class  FalseButton  extends  BooleanButton
{
     public  FalseButton(Context context, AttributeSet attrs)
     {
         super (context, attrs);
     }
}


    MainActivity.class代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package  com.example.d_motionevent;
 
import  android.os.Bundle;
import  android.app.Activity;
import  android.util.Log;
import  android.view.MotionEvent;
import  android.view.View;
import  android.view.View.OnTouchListener;
import  android.widget.Button;
 
/**
  * 地址:http://glblong.blog.51cto.com/3058613/1557683
  * @author zeng
  */
public  class  MainActivity  extends  Activity  implements  OnTouchListener
{
     @Override
     protected  void  onCreate(Bundle savedInstanceState)
     {
         super .onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         
         View layout1 = findViewById(R.id.layout1);
         layout1.setOnTouchListener( this );
         
         Button trueButton1 = (Button) findViewById(R.id.trueButton1);
         trueButton1.setOnTouchListener( this );
         Button falseButton1 = (Button) findViewById(R.id.falseButton1);
         falseButton1.setOnTouchListener( this );
         
         View layout2 = findViewById(R.id.layout2);
         layout2.setOnTouchListener( this );
         
         Button trueButton2 = (Button) findViewById(R.id.trueButton2);
         trueButton2.setOnTouchListener( this );
         Button falseButton2 = (Button) findViewById(R.id.falseButton2);
         falseButton2.setOnTouchListener( this );
     }
     
     @Override
     public  boolean  onTouch(View v, MotionEvent event)
     {
         String myTag = v.getTag().toString();
         
         Log.e(myTag,  "==========="  "Activity 的 onTouch 方法"  "===========" );
         Log.e(myTag,  "被点击的View = "  + myTag);
         Log.e(myTag, describeEvent(v, event));
         
         if  ( "true" .equals(myTag.substring( 0 4 )))
         {
             Log.e(myTag,  " == true" );
             return  true ;
         }
         else
         {
             Log.e(myTag,  " == false" );
             return  false ;
         }
     }
     
     @Override
     public  boolean  onTouchEvent(MotionEvent event)
     {
         return  super .onTouchEvent(event);
     }
     
     protected  static  String describeEvent(View view, MotionEvent event)
     {
         StringBuilder sb =  new  StringBuilder( 300 );
         
         sb.append( "Action: " ).append(event.getAction()).append( "\n" ); // 获取触控动作比如ACTION_DOWN
         sb.append( "相对坐标: " ).append(event.getX()).append( "  *  " ).append(event.getY()).append( "   " );
         sb.append( "绝对坐标: " ).append(event.getRawX()).append( "  *  " ).append(event.getRawY()).append( "\n" );
         
         if  (event.getX() <  0  || event.getX() > view.getWidth() || event.getY() <  0  || event.getY() > view.getHeight())
         {
             sb.append( "未点击在View范围内" );
         }
         
         sb.append( "Edge flags: " ).append(event.getEdgeFlags()).append( "  " ); // 边缘标记,但是看设备情况,很可能始终返回0
         sb.append( "Pressure: " ).append(event.getPressure()).append( "  " ); // 压力值,0-1之间,看情况,很可能始终返回1
         sb.append( "Size: " ).append(event.getSize()).append( "\n" ); // 指压范围
         sb.append( "Down time: " ).append(event.getDownTime()).append( "ms   " );
         sb.append( "Event time: " ).append(event.getEventTime()).append( "ms   " );
         sb.append( "Elapsed: " ).append(event.getEventTime() - event.getDownTime()).append( "ms\n" );
         
         return  sb.toString();
     }
}


    点击过程分析:


1.点击上部的【true_Btn_上】,运行日志如下:

wKioL1QfvF6zXGbRAAfjcv460KQ430.jpg

    此时执行的是Activity上的onTouch方法并返回了True。

    onTouch()方法返回true,因为编码TrueButton的目的是为返回true。返回true会告诉Android,MotionEvent对象已经被使用,不能将它提供给其他方法。

    它还告诉Android,继续将此触摸序列的触摸事件发送到此方法。这就是为什么我们在ACTION_DOWN事件后还会看到ACTION_UP或ACTION_MOVE等其他事件。

    当触摸【true_Btn_上】时,按钮并没有高亮颜色变化。这是因为,onTouch()是在调用任何按钮方法之前调用的,而且onTouch()方法返回了true,所以Android就不会再调用【true_Btn_上】按钮的onTouchEvent()方法了。

    如果在onTouch()方法中,在返回true的行之前添加一行"v.onTouchEvent(event);",那么将会看到触摸时按钮会有颜色变化了。



2.点击上部的【false_Btn_上】,运行日志如下:

wKiom1QfmRKzSA05AAn2vtE-E5o910.jpg


    点击后,false_btn按钮会处于常亮状态。效果如图:

wKioL1QfnXvC-z0IAAG57335zho319.jpg


    Android接收到MotionEvent对象中的ACTION_DOWN事件,将它传递给MainActivity类中的onTouch()方法。onTouch()执行后返回false。

    这个过程告诉Android,onTouch()方法未使用该事件,所以Android寻找要调用的下一个方法,也就是范例中的FalseButton类中重写的onTouchEvent()方法。参见BooleanButton.java中的onTouchEvent()方法,先执行父类的onTouchEvent()方法,然后再返回了false。此时打印出来的Logcat日志与之前的完全一样,因为我们仍然在同一个View对象FalseButton中。

    根据日志可以看到,父类希望从onTouchEvent()返回true,但是FalseButton的该方法返回了false。通过返回false,再次告诉Android我们未使用此事件,所以Android不会将ACTION_UP事件发送到我们的按钮,以至于该按钮不知道手指是否已离开了触摸屏。这也是为什么FalseButton在被按下时一直停留在被按下的颜色状态。

    简而言之,每次从收到MotionEvent对象的UI对象返回false时,Android就会停止将MotionEvent对象继续发送到该UI对象,同时还会不断的查找另一个UI对象来使用MotionEvent对象。


    接着看日志,Android两次尝试找到ACTION_DOWN事件的使用者但都失败了,现在它前进道应用程序中下一个可能接收该事件的View,也就是按钮底层的布局true_Layout。

    此时,Android再次调用了MainActivity类中的onTouch()方法,但是现在接收事件的对象变成了true_Layout。接收到的MotionEvent对象的所有信息也与之前相同,只有Y坐标除外,因为点击位置的Y坐标相对于布局左上角要比相对于按钮的左上角的距离来得大。因为true_Layout的onTouch()方法返回true,所以Android将触摸事件的剩余信息发送到了布局。


3.点击上部的【true_Btn_上】按钮不放,同时触摸其他区域。

    触摸【true_Btn_上】按钮,在离开按钮之前,其他手指在屏幕上其他区域移动。此时,Logcat将显示接收触摸事件的对象都是【true_Btn_上】按钮。甚至手指离开【true_Btn_上】按钮,而另一手指仍然在屏幕上移动时,接收到触摸事件的仍然还是【true_Btn_上】按钮。

    当从onTouch()返回true时,触摸事件序列将与【true_Btn_上】按钮相关联。这告诉了Android,它可以停止查找用来接收MotionEvent对象的UI对象了,只需将此触摸序列的所有未来MotionEvent对象发送给我们。即使在拖动手指时遇到了另一个视图,我们仍然会绑定到此序列的原始视图。


4.点击下部的【false_Btn_下】按钮,运行日志如下:

wKioL1Qf6Lbi0sWQAAdADS-H8rs285.jpg

    原理与前几点相同。此时若按住按钮的手指离开前,其他手指继续触摸屏幕,则将不再继续输出日志,因为Android找不到接收MotionEvent对象并返回true结果的UI对象,直到开始下一个新触摸序列。


    范例源码见附件。地址:http://glblong.blog.51cto.com/3058613/1557683


三、MotionEvent回收

    MotionEvent类中有个recycle()方法,但是不要通过此方法对MotionEvent对象进行回收。如果回调方法没有使用MotionEvent对象,并且返回了false,MotionEvent对象可能会被传递到其他某个方法、视图或我们的活动。

    MotionEvent类中还有个obtain()方法,通过此方法可以创建一个MotionEvent的副本或者全新的MotionEvent,这个副本或全新的事件对象是在完成之后应该回收的对象。




四、小结

1.onTouch()方法与View类中的onTouchEvent()方法的区别

onTouch()是View.setOnTouchListener后调用的方法,如果一个View对象setOnTouchListener后,同时又重写了View类自身的onTouchEvent()方法,那么当屏幕触摸时会先调用哪个方法呢?


每当事件产生时,都要先经由dispatchTouchEvent方法来分发。看下View类中的dispatchTouchEvent()源码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
     /**
      * Pass the touch screen motion event down to the target view, or this view
      * if it is the target.
     
      * @param event
      *            The motion event to be dispatched.
      * @return True if the event was handled by the view, false otherwise.
      */
     public  boolean  dispatchTouchEvent(MotionEvent event)
     {
         // 用于测试目,直接忽略
         if  (mInputEventConsistencyVerifier !=  null )
         {
             mInputEventConsistencyVerifier.onTouchEvent(event,  0 );
         }
          
         // 上面代码的第2个标注。 未被其他窗口遮盖
         if  (onFilterTouchEventForSecurity(event))
         {
             // noinspection SimplifiableIfStatement
             if  (mOnTouchListener !=  null  && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch( this , event))
             {
                 // 如果有监听器,执行监听器的,不在执行当前视图的onTouchEvent方法
                 return  true ;
             }
             // 执行当前视图的onTouchEvent方法
             if  (onTouchEvent(event))
             {
                 return  true ;
             }
         }
          
         // 用于测试目,直接忽略
         if  (mInputEventConsistencyVerifier !=  null )
         {
             mInputEventConsistencyVerifier.onUnhandledEvent(event,  0 );
         }
         return  false ;
     }

dispatchTouchEvent()方法里先是判断了是否有监听器,然后才会去判断onTouchEvent(event)。因此会先执行onTouch()方法,如果监听器的onTouch()返回false,则继续执行View的onTouchEvent()。



2.触摸事件的传递过程

1).触摸事件最先被最顶层的UI对象(比如范例中的Btn)接收,如果该View对象接收后返回false,则继续传向其上一层UI对象(比如范例中的layout),依此类推,直到最底层的Activity。

2).同一个View对象,接收到触摸事件时,如果有设置监听器,先执行View.setOnTouchListener里的onTouch(),然后执行View类中的onTouchEvent(event)方法。

3).当一个UI对象接收到触摸事件并返回true时,同时多指触摸的其他事件序列都会被该UI对象接收,直至下一个新的触摸事件序列产生。

4).当各层的UI对象接收触摸事件都返回false时,此刻同时多指触摸,由此产生的其他事件序列将找不到接收的UI对象。因为当前原始的接收UI对象返回false,系统会一直查找下一个可能接收事件并返回true的UI对象但却又无法找到。






本文转自 glblong 51CTO博客,原文链接:http://blog.51cto.com/glblong/1557683,如需转载请自行联系原作者

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
21天前
|
安全 Android开发 数据安全/隐私保护
深入探讨iOS与Android系统安全性对比分析
在移动操作系统领域,iOS和Android无疑是两大巨头。本文从技术角度出发,对这两个系统的架构、安全机制以及用户隐私保护等方面进行了详细的比较分析。通过深入探讨,我们旨在揭示两个系统在安全性方面的差异,并为用户提供一些实用的安全建议。
|
29天前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
54 15
Android 系统缓存扫描与清理方法分析
|
1月前
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
76 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
1月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
93 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
1月前
|
存储 Linux Android开发
Android底层:通熟易懂分析binder:1.binder准备工作
本文详细介绍了Android Binder机制的准备工作,包括打开Binder驱动、内存映射(mmap)、启动Binder主线程等内容。通过分析系统调用和进程与驱动层的通信,解释了Binder如何实现进程间通信。文章还探讨了Binder主线程的启动流程及其在进程通信中的作用,最后总结了Binder准备工作的调用时机和重要性。
Android底层:通熟易懂分析binder:1.binder准备工作
|
2月前
|
安全 Android开发 数据安全/隐私保护
探索安卓与iOS的安全性差异:技术深度分析与实践建议
本文旨在深入探讨并比较Android和iOS两大移动操作系统在安全性方面的不同之处。通过详细的技术分析,揭示两者在架构设计、权限管理、应用生态及更新机制等方面的安全特性。同时,针对这些差异提出针对性的实践建议,旨在为开发者和用户提供增强移动设备安全性的参考。
142 3
|
1月前
|
开发工具 Android开发 Swift
安卓与iOS开发环境的差异性分析
【10月更文挑战第8天】 本文旨在探讨Android和iOS两大移动操作系统在开发环境上的不同,包括开发语言、工具、平台特性等方面。通过对这些差异性的分析,帮助开发者更好地理解两大平台,以便在项目开发中做出更合适的技术选择。
|
JavaScript Java Android开发
《Android游戏开发详解》一2.10 使用对象
我们现在开始真正地使用对象。创建一个名为BasicObjects的新的Java对象。然后,创建一个名为World的新类,并且给它一个简单的“Hello, world!” 的main方法,如程序清单2.9所示。
1403 0
下一篇
无影云桌面