一、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
|
<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_上】,运行日志如下:
此时执行的是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
|
/**
* 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对象但却又无法找到。