Android笔记:触摸事件的分析与总结----MotionEvent对象
一、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对象处理过程 范例视图如下: 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 <RelativeLayoutxmlns: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; /** * *@authorzeng */ public class BooleanButton extends Button { public BooleanButton(Contextcontext,AttributeSetattrs) { super (context,attrs); } protected boolean myValue() { return false ; } @Override public boolean onTouchEvent(MotionEventevent) { StringmyTag= 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, "该buttontouch=" +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. *@authorzeng */ public class TrueButton extends BooleanButton { public TrueButton(Contextcontext,AttributeSetattrs) { 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. *@authorzeng */ public class FalseButton extends BooleanButton { public FalseButton(Contextcontext,AttributeSetattrs) { 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 *@authorzeng */ public class MainActivity extends Activity implements OnTouchListener { @Override protected void onCreate(BundlesavedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Viewlayout1=findViewById(R.id.layout1); layout1.setOnTouchListener( this ); ButtontrueButton1=(Button)findViewById(R.id.trueButton1); trueButton1.setOnTouchListener( this ); ButtonfalseButton1=(Button)findViewById(R.id.falseButton1); falseButton1.setOnTouchListener( this ); Viewlayout2=findViewById(R.id.layout2); layout2.setOnTouchListener( this ); ButtontrueButton2=(Button)findViewById(R.id.trueButton2); trueButton2.setOnTouchListener( this ); ButtonfalseButton2=(Button)findViewById(R.id.falseButton2); falseButton2.setOnTouchListener( this ); } @Override public boolean onTouch(Viewv,MotionEventevent) { StringmyTag=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(MotionEventevent) { return super .onTouchEvent(event); } protected static StringdescribeEvent(Viewview,MotionEventevent) { StringBuildersb= 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( "Edgeflags:" ).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( "Downtime:" ).append(event.getDownTime()).append( "ms" ); sb.append( "Eventtime:" ).append(event.getEventTime()).append( "ms" ); sb.append( "Elapsed:" ).append(event.getEventTime()-event.getDownTime()).append( "ms\n" ); return sb.toString(); } } 点击过程分析: 1.点击上部的【true_Btn_上】,运行日志如下: 此时执行的是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_上】,运行日志如下: 点击后,false_btn按钮会处于常亮状态。效果如图: 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_下】按钮,运行日志如下: 原理与前几点相同。此时若按住按钮的手指离开前,其他手指继续触摸屏幕,则将不再继续输出日志,因为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 /** *Passthetouchscreenmotioneventdowntothetargetview,orthisview *ifitisthetarget. * *@paramevent *Themotioneventtobedispatched. *@returnTrueiftheeventwashandledbytheview,falseotherwise. */ public boolean dispatchTouchEvent(MotionEventevent) { //用于测试目,直接忽略 if (mInputEventConsistencyVerifier!= null ) { mInputEventConsistencyVerifier.onTouchEvent(event, 0 ); } //上面代码的第2个标注。未被其他窗口遮盖 if (onFilterTouchEventForSecurity(event)) { //noinspectionSimplifiableIfStatement 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,如需转载请自行联系原作者