Android按钮单击事件的四种常用写法总结
这篇文章主要介绍了Android按钮单击事件的四种常用写法总结,比较了常见的四种写法的优劣,有不错的
参考借鉴价值,需要的朋友可以参考下
..很多学习Android程序设计的人都会发现每个人对代码的写法都有不同的偏好,比较明显的就是对控件
响应事件的写法的不同。因此本文就把这些写法总结一下,比较下各种写法的优劣,希望对大家灵活地
选择编码方式可以有一定的参考借鉴价值。
xml文件代码如下:
?1234567891011 <Button android:id="@+id/button1" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="Button1" /> <Button
android:id="@+id/button2" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="Button2" />
四种方法分述如下:
匿名内部类:
public class
TestButtonActivity extends Activity { Button btn1, btn2; Toast tst; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate
(savedInstanceState); setContentView(R.layout.activity_test_button); btn1 =
(Button) findViewById(R.id.button1); btn2 = (Button) findViewById(R.id.button2);
btn1.setOnClickListener(new OnClickListener() { @Override public void onClick
(View v) { // TODO Auto-generated method stub Toast tst = Toast.makeText
(TestButtonActivity.this, "111111111", Toast.LENGTH_SHORT); tst.show(); }
}); btn2.setOnClickListener(new OnClickListener() { @Override public
void onClick(View v) { // TODO Auto-generated method stub Toast tst =
Toast.makeText(TestButtonActivity.this, "222222222", Toast.LENGTH_SHORT); tst.show
(); } }); } }
自定义单击事件监听类:
public class
TestButtonActivity extends Activity { Button btn1, btn2; Toast tst; class
MyClickListener implements OnClickListener { @Override public void onClick(View v)
{ // TODO Auto-generated method stub switch (v.getId()) { case
R.id.button1: tst = Toast.makeText(TestButtonActivity.this, "111111111",
Toast.LENGTH_SHORT); tst.show(); break; case R.id.button2:
tst = Toast.makeText(TestButtonActivity.this, "222222222", Toast.LENGTH_SHORT);
tst.show(); break; default: break; } } } @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate
(savedInstanceState); setContentView(R.layout.activity_test_button); btn1 =
(Button) findViewById(R.id.button1); btn2 = (Button) findViewById(R.id.button2);
btn1.setOnClickListener(new MyClickListener()); btn2.setOnClickListener(new
MyClickListener()); } }
Activity继承View.OnClickListener,由Activity实现OnClick(View view)方法,在OnClick(View
view)方法中用switch-case对不同id代表的button进行相应的处理
?12345678910111213141516171819202122232425262728293031323334 public class
TestButtonActivity extends Activity implements OnClickListener { Button btn1, btn2;
Toast tst; @Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); setContentView(R.layout.activity_test_button);
btn1 = (Button) findViewById(R.id.button1); btn2 = (Button) findViewById
(R.id.button2); btn1.setOnClickListener(this); btn2.setOnClickListener(this); }
@Override public void onClick(View v) { // TODO Auto-generated method stub
switch (v.getId()) { case R.id.button1: tst = Toast.makeText(this, "111111111",
Toast.LENGTH_SHORT); tst.show(); break; case R.id.button2: tst =
Toast.makeText(this, "222222222", Toast.LENGTH_SHORT); tst.show(); break;
default: break; } } }
最后一种是我今天看到的一种写法,在XML文件中“显示指定按钮的onClick属性,这样点击按钮时会利
用反射的方式调用对应Activity中的click()方法”
?12345678910111213 <Button android:id="@+id/button1"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="onClick" android:text="Button1" /> <Button android:id="@
+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="onClick" android:text="Button2" />
这里在输完android:的时候按下 Alt+/ 会有 onClick 属性的提示, 但输入到 android:onClick=“ 的
地方按下 Alt+/ 并没有提示 onClick 选项,让我突然觉得这里好像有点问题。
?12345678910111213141516171819202122232425262728 public class TestButtonActivity extends
Activity { Button btn1, btn2; Toast tst; @Override protected void onCreate
(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView
(R.layout.activity_test_button); } // 注意 这里没有 @Override 标签 public void
onClick(View v) { // TODO Auto-generated method stub switch (v.getId()) { case
R.id.button1: tst = Toast.makeText(this, "111111111", Toast.LENGTH_SHORT);
tst.show(); break; case R.id.button2: tst = Toast.makeText(this,
"222222222", Toast.LENGTH_SHORT); tst.show(); break; default: break;
} } }
这种写法整个代码中都不用声明button就可以实现button的单击事件。
以上就是四种实现按钮单击事件的方法。
粗略总结一下,就是按钮少的时候用匿名内部类会比较快,比如写demo测试的时候或者登陆界面之类的
。
按钮多的情况我还是选择第三种方法,方便。
关于第四种方法,我感觉最方便,但看了很多代码还是觉得写法不够大众化,感兴趣的朋友可以对此研
究研究。相信会有不少收获。
========
Android—事件处理
处理用户界面事件Handling UI Events
在Android上,不止一个途径来侦听用户和应用程序之间交互的事件。对于用户界面里的事件,侦听方法
就是从与用户交互的特定视图对象截获这些事件。视图类提供了相应的手段。
在各种用来组建布局的视图类里面,你可能会注意到一些公共的回调方法看起来对用户界面事件有用。
这些方法在该对象的相关动作发生时被Android框架调用。比如,当一个视图(如一个按钮)被触摸时,
该对象上的onTouchEvent()方法会被调用。不过,为了侦听这个事件,你必须扩展这个类并重写该方法
。很明显,扩展每个你想使用的视图对象(只是处理一个事件)是荒唐的。这就是为什么视图类也包含
了一个嵌套接口的集合,这些接口含有实现起来简单得多的回调函数。这些接口叫做事件侦听器event
listeners,是用来截获用户和你的界面交互动作的“门票”。
当你更为普遍的使用事件侦听器来侦听用户动作时,总有那么一次你可能得为了创建一个自定义组件而
扩展一个视图类。也许你想扩展按钮Button类来使某些事更花哨。在这种情况下,你将能够使事件处理
器event handlers类来为你的类定义缺省事件行为。
事件侦听器Event Listeners
事件侦听器是视图View类的接口,包含一个单独的回调方法。这些方法将在视图中注册的侦听器被用户
界面操作触发时由Android框架调用。下面这些回调方法被包含在事件侦听器接口中:
onClick()
包含于View.OnClickListener。当用户触摸这个item(在触摸模式下),或者通过浏览键或跟踪球聚焦
在这个item上,然后按下“确认”键或者按下跟踪球时被调用。
onLongClick()
包含于View.OnLongClickListener。当用户触摸并控制住这个item(在触摸模式下),或者通过浏览键
或跟踪球聚焦在这个item上,然后保持按下“确认”键或者按下跟踪球(一秒钟)时被调用。
onFocusChange()
包含于View.OnFocusChangeListener。当用户使用浏览键或跟踪球浏览进入或离开这个item时被调用。
onKey()
包含于View.OnKeyListener。当用户聚焦在这个item上并按下或释放设备上的一个按键时被调用。
onTouch()
包含于View.OnTouchListener。当用户执行的动作被当做一个触摸事件时被调用,包括按下,释放,或
者屏幕上任何的移动手势(在这个item的边界内)。
onCreateContextMenu()
包含于View.OnCreateContextMenuListener。当正在创建一个上下文菜单的时候被调用(作为持续的“
长点击”动作的结果)。参阅创建菜单Creating Menus章节以获取更多信息。
这些方法是它们相应接口的唯一“住户”。要定义这些方法并处理你的事件,在你的活动中实现这个嵌
套接口或定义它为一个匿名类。然后,传递你的实现的一个实例给各自的View.set...Listener()方法。
(比如,调用setOnClickListener()并传递给它你的OnClickListener实现。)
下面的例子说明了如何为一个按钮注册一个点击侦听器:
// Create an anonymous implementation of OnClickListener
private OnClickListener mCorkyListener = new OnClickListener() {
public void onClick(View v) {
// do something when thebutton is clicked
}
};
protected void onCreate(Bundle savedValues) {
...
// Capture our button fromlayout
Button button =(Button)findViewById(R.id.corky);
// Register the onClicklistener with the implementation above
button.setOnClickListener(mCorkyListener);
...
}
你可能会发现把OnClickListener作为活动的一部分来实现会便利的多。这将避免额外的类加载和对象分
配。比如:
public class ExampleActivity extends Activity implementsOnClickListener {
protected void onCreate(BundlesavedValues) {
...
Button button =(Button)findViewById(R.id.corky);
button.setOnClickListener(this);
}
// Implement theOnClickListener callback
public void onClick(View v) {
// do something when thebutton is clicked
}
...
}
注意上面例子中的onClick()回调没有返回值,但是一些其它事件侦听器必须返回一个布尔值。原因和事
件相关。对于其中一些,原因如下:
· onLongClick() – 返回一个布尔值来指示你是否已经消费了这个事件而不应该再进一步处
理它。也就是说,返回true 表示你已经处理了这个事件而且到此为止;返回false 表示你还没有处理它
和/或这个事件应该继续交给其他on-click侦听器。
· onKey() –返回一个布尔值来指示你是否已经消费了这个事件而不应该再进一步处理它。也
就是说,返回true 表示你已经处理了这个事件而且到此为止;返回false 表示你还没有处理它和/或这
个事件应该继续交给其他on-key侦听器。
· onTouch() - 返回一个布尔值来指示你的侦听器是否已经消费了这个事件。重要的是这个事
件可以有多个彼此跟随的动作。因此,如果当接收到向下动作事件时你返回false,那表明你还没有消费
这个事件而且对后续动作也不感兴趣。那么,你将不会被该事件中的其他动作调用,比如手势或最后出
现向上动作事件。
记住按键事件总是递交给当前焦点所在的视图。它们从视图层次的顶层开始被分发,然后依次向下,直
到到达恰当的目标。如果你的视图(或者一个子视图)当前拥有焦点,那么你可以看到事件经由
dispatchKeyEvent()方法分发。除了从你的视图截获按键事件,还有一个可选方案,你还可以在你的活
动中使用onKeyDown() andonKeyUp()来接收所有的事件。
注意: Android 将首先调用事件处理器,其次是类定义中合适的缺省处理器。这样,从这些事情侦听器
中返回true 将停止事件向其它事件侦听器传播并且也会阻塞视图中的缺事件处理器的回调函数。因此当
你返回true时确认你希望终止这个事件。
事件处理器Event Handlers
如果你从视图创建一个自定义组件,那么你将能够定义一些回调方法被用作缺省的事件处理器。在创建
自定义组件Building CustomComponents的文档中,你将学习到一些用作事件处理的通用回调函数,包括
:
· onKeyDown(int,KeyEvent) - 当一个新的按键事件发生时被调用。
· onKeyUp(int, KeyEvent)- 当一个向上键事件发生时被调用。
· onTrackballEvent(MotionEvent) - 当一个跟踪球运动事件发生时被调用。
· onTouchEvent(MotionEvent) - 当一个触摸屏移动事件发生时调用。
· onFocusChanged(boolean,int, Rect) - 当视图获得或者丢失焦点时被调用。
你应该知道还有一些其它方法,并不属于视图类的一部分,但可以直接影响你处理事件的方式。所以,
当在一个布局里管理更复杂的事件时,考虑一下这些方法:
· Activity.dispatchTouchEvent(MotionEvent) - 这允许你的活动可以在分发给窗口之前捕获
所有的触摸事件。
· ViewGroup.onInterceptTouchEvent(MotionEvent) - 这允许一个视图组ViewGroup 在分发给
子视图时观察这些事件。ViewParent.requestDisallowInterceptTouchEvent(boolean)- 在一个父视图
之上调用这个方法来表示它不应该通过onInterceptTouchEvent(MotionEvent)来捕获触摸事件。
触摸模式Touch Mode
当用户使用方向键或跟踪球浏览用户界面时,有必要给用户可操作的item(比如按钮)设置焦点,这样
用户可以知道哪个item将接受输入。不过,如果这个设备有触摸功能,而且用户通过触摸来和界面交互
,那么就没必要高亮items,或者设定焦点到一个特定的视图。这样,就有一个交互模式叫“触摸模式”
。
对于一个具备触摸功能的设备,一旦用户触摸屏幕,设备将进入触摸模式。自此以后,只有
isFocusableInTouchMode()为真的视图才可以被聚焦,比如文本编辑部件。其他可触摸视图,如按钮,
在被触摸时将不会接受焦点;它们将只是在被按下时简单的触发on-click侦听器。任何时候用户按下方
向键或滚动跟踪球,这个设备将退出触摸模式,然后找一个视图来接受焦点,用户也许不会通过触摸屏
幕的方式来恢复界面交互。
触摸模式状态的维护贯穿整个系统(所有窗口和活动)。为了查询当前状态,你可以调用
isInTouchMode() 来查看这个设备当前是否处于触摸模式中。
处理焦点Handling Focus
框架将根据用户输入处理常规的焦点移动。这包含当视图删除或隐藏,或者新视图出现时改变焦点。视
图通过isFocusable()方法表明它们想获取焦点的意愿。要改变视图是否可以接受焦点,可以调用
setFocusable()。在触摸模式中,你可以通过isFocusableInTouchMode()查询一个视图是否允许接受焦
点。你可以通过setFocusableInTouchMode()方法来改变它。焦点移动基于一个在给定方向查找最近邻居
的算法。少有的情况是,缺省算法可能和开发者的意愿行为不匹配。在这些情况下,你可以通过下面布
局文件中的XML属性提供显式的重写:nextFocusDown, nextFocusLeft,nextFocusRight, 和nextFocusUp
。为失去焦点的视图增加这些属性之一。定义属性值为拥有焦点的视图的ID。比如:
<LinearLayout
android:orientation="vertical"
... >
<Buttonandroid:id="@+id/top"
android:nextFocusUp="@+id/bottom"
... />
<Buttonandroid:id="@+id/bottom"
android:nextFocusDown="@+id/top"
... />
</LinearLayout>
通常,在这个竖向布局中,从第一个按钮向上浏览或者从第二个按钮向下都不会移动到其它地方。现在
这个顶部按钮已经定义了底部按钮为nextFocusUp (反之亦然),浏览焦点将从上到下和从下到上循环移
动。
如果你希望在用户界面中声明一个可聚焦的视图(通常不是这样),可以在你的布局定义中,为这个视
图增加android:focusableXML 属性。把它的值设置成true。你还可以通过
android:focusableInTouchMode在触摸模式下声明一个视图为可聚焦。
想请求一个接受焦点的特定视图,调用requestFocus()。
要侦听焦点事件(当一个视图获得或者失去焦点时被通知到),使用onFocusChange(),如上面事件侦听
器Event Listeners一章所描述的那样
Android—事件处理
方法/步骤
1
package cn.liu.eventhander;
import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class EventActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// 指定布局文件
setContentView(R.layout.listener);
// 获得界面上的组件
Button button=(Button)this.findViewById(R.id.sendbutton);
final EditText input=(EditText)this.findViewById(R.id.input);
final TextView output=(TextView)this.findViewById(R.id.output);
// 注册监听器,实现监听方法
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
Editable content=input.getText();
output.setText("电话号码为:"+content);
input.setText("");
}
});
}
}
2
listener.xml:
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout android:gravity="center_vertical"
android:layout_height="fill_parent"
android:layout_width="fill_parent" android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="@+id/output"
android:layout_height="wrap_content"
android:layout_width="wrap_content" android:text="显示输入的电话号码"/>
<EditText android:background="#FFFFFF" android:digits="0123456789"
android:id="@+id/input" android:layout_height="wrap_content"
android:layout_margin="10dp" android:layout_width="200dp"
android:textColor="#000000"/>
<Button android:id="@+id/sendbutton"
android:layout_height="wrap_content"
android:layout_width="wrap_content" android:text="发送"/>
</LinearLayout>
3
main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello"
/>
</LinearLayout>
========
android开发事件监听
第一种:匿名内部类作为事件监听器类大部分时候,事件处理器都没有什么利用价值(可利用代码通常都被抽象成了业务逻辑方法),因此大
部分事件监听器只是临时使用一次,所以使用匿名内部类形式的事件监听器更合适,实际上,这种形式
是目前是最广泛的事件监听器形式。上面的程序代码就是匿名内部类来创建事件监听器的!!!
对于使用匿名内部类作为监听器的形式来说,唯一的缺点就是匿名内部类的语法有点不易掌握,如果读
者java基础扎实,匿名内部类的语法掌握较好,通常建议使用匿名内部类作为监听器。
第二种:内部类作为监听器
将事件监听器类定义成当前类的内部类。1、使用内部类可以在当前类中复用监听器类,因为监听器类是
外部类的内部类,2、所以可以自由访问外部类的所有界面组件。这也是内部类的两个优势。上面代码就
是内部类的形式!!
第三种:Activity本身作为事件监听器
这种形式使用activity本身作为监听器类,可以直接在activity类中定义事件处理器方法,这种形式非
常简洁。但这种做法有两个缺点:(1)这种形式可能造成程序结构混乱。Activity的主要职责应该是完
成界面初始化;但此时还需包含事件处理器方法,从而引起混乱。(2)如果activity界面类需要实现监
听器接口,让人感觉比较怪异。
上面的程序让Activity类实现了OnClickListener事件监听接口,从而可以在该Activity类中直接定义事
件处理器方法:onClick(view v),当为某个组件添加该事件监听器对象时,直接使用this作为事件监听
器对象即可。
第四种:外部类作为监听器
ButtonTest类
当用户单击button按钮时,程序将会触发MyButtonListener监听器
外部MyButtonListener类
使用顶级类定义事件监听器类的形式比较少见,主要因为如下两个原因:
1、事件监听器通常属于特定的gui界面,定义成外部类不篮球提高程序的内聚性。
2、外部类形式的事件监听器不能自由访问创建gui界面的类中的组件,编程不够简洁。
但如果某个事件监听器确实需要被多个gui界面所共享,而且主要是完成某种业务逻辑的实现,则可以考
虑使用外部类的形式来定义事件监听器类。
第五种:直接绑定到标签
Android还有一种更简单的绑定事件监听器的的方式,直接在界面布局文件中为指定标签绑定事件处理方
法。
对于很多Android标签而言,它们都支持如onClick、onLongClick等属性,这种属性的属性值就是一个形
如xxx
(View source)
的方法的方法名。在布局文件中为button添加属性,如一代码:
为Button按钮绑定一个事件处理方法:clickHanlder,这意味着开发者需要在该界面布局对应的
Activity中定义一个void clickHanler(View source)方法,该方法将会负责处理该按钮上的单击事件。
下面是该界面布局对应的java代码:
原文地址:http://www.apkbus.com/android-18459-1-1.html
========
android事件拦截处理机制详解
目录(?)[+]
前段时间刚接触过Android手机开发,对它的事件传播机制不是很了解,虽然网上也查了相关的资料,但
是总觉得理解模模糊糊,似是而非,于是自己就写个小demo测试了一下。总算搞明白了它的具体机制。
写下自己的结论,分享之,希望对初学android的人有所帮助
布局效果如图所示:
图1
参照上图先说说具体得到的结论:
1) onInterceptTouchEvent负责对touch事件进行拦截,对于嵌套的view最先执行的是事件拦截方法的是
最外层的那个view的onInterceptTouchEvent方法,然后依次执行子视图的onInterceptTouchEvent,然后
在执行子视图的子视图的事件拦截方法(当然在这里假设所有嵌套视图的onInterceptTouchEvent都会得
到执行,让每个视图的onInterceptTouchEvent返回false即可)。参照上图,所以onInterceptTouchEvent
执行顺序就是A--->B--->C--->D.也就是由父视图到子视图传递。总之,事件拦截机制是由父视图开始发
起对事件的拦截(出事了老子先上,儿子稍后)。参照上图当手指触摸事件时,父视图A首先发起对该起事
件的拦截,如果A拦截失败,就交给它的子视图B进行拦截;如果B拦截失败就交给B的子视图C再进行拦截
..直到某一子视图对该次事件拦截成功。
2)某一视图拦截事件成功与否的判断标识是onInterceptTouchEvent方法的返回值,当返回true的时候说
明拦截成功,返回false的时候说明当前视图对事件拦截失败。
3)下面说说拦截成功的情况,假设C视图对当前touch事件拦截成功。拦截成功意味着此次事件不会再传
递到D视图了。所以此时的D视图的onInterceptTouchEvent就得不到运行(事件没法到达了,还拦截谁呢
?)。事件拦截成功后,紧接着就会对事件进行处理,处理的方法教给onTouchEvent方法处理。此时C视
图拦截成功,那么紧接着就会执行C视图的onTouchEvent方法,这是不是就意味着当前touch事件是由C视
图的onTouchEvent方法来处理的呢?这要由C视图的onTouchEvent方法的返回值来决定。当C视图的
onTouchEvent返回true的时候,当前事件就由C全权处理,处理的当然是事件的各种action,什么
MotionEvent.ACTION_MOVE,ACTION_UP都交给了C的onTouchEvent方法进行处理。所以此时就可以在C的
onTouchEvent方法中进行switch(event.getAction)判断执行相关逻辑了。如果返回的false,说明C视图
对此事件不做处理或者处理不了,怎么办呢?儿子不行老爸来,于是事件就交到了B视图的onTouchEvent
方法中。同样B对此事件处理与否还是看B的onTouchEvent返回值,具体的解释就跟C一样了,不复多言。
4)在A B C D的onInterceptTouchEvent和onTouchEvent都返回false的情况下,方法执行的顺序依次为
A.onInterceptTouchEvent-->B.onInterceptTouchEvent-->C.onInterceptTouchEvent-->D.touchEvent(
最深的子视图没重写onInterceptTouchEvent)-->C.touchEvent-->B.touchEvent-->A.touchEvent.也就
是说拦截事件是父视图优先有子视图进行拦截,处理事件是子视图优先父视图进行处理。
总结:onInterceptTouchEvent负责对事件进行拦截,拦截成功后交给最先遇到onTouchEvent返回true
的那个view进行处理。
结合事件源码分析事件处理机制的话,可以阅读《从源码角度解析android事件原理》
下面将要详细讲解上面结论是怎么得出的,准备分两部分进行一步步讲解。如果上面说的看明白的话,
下面的内容就不要看了,因为会很啰嗦。
图1的布局代码如下所示:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
<com.example.demo.AView 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" >
<com.example.demo.BView
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.demo.CView
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.demo.DView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="测试demo" />
</com.example.demo.CView>
</com.example.demo.BView>
</com.example.demo.AView>
其中最后一个D是一个自定义的TextView,与A B C三个View的区别就是D只重写了onTouchEvent方法,A
B C 这三个自定义控件还重写了onInterceptEvent方法。
D的代码如下,A B C代码基本上除了类名和输出log不一样外其余的都一样,所以为了减少这里只贴出其
中的一个。
DView的代码:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class DView extends TextView{
private static String tag = "D";
public DView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public DView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DView(Context context) {
super(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(tag, "--onTouchEvent--D");
return false;
}
}
AView的代码和C D的整体差不多,就贴出来一个:
[java] view plain copy 在CODE上查看代码片派生到我的代码片
public class AView extends RelativeLayout{
private static String tag = "A";
public AView(Context context) {
super(context);
}
public AView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public AView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(tag,"--onInterceptTouchEvent--A");
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(tag,"--onTouchEvent---A" );
return false;
}
}
刚开始的时候重写的方法全部返回false运行点击的效果输出log为:
转换成效果图为:
从此图可以看出,onInterceptTouchEvent事件的执行顺序是由父控件到子控件,并且优先于自己控件的
onTouchEvent方法执行,onTouchEvent事件执行的顺序正好相反由子控件到父控件。注意由于此时都返
回了false,是没有哪一个view来处理此次的touch事件的各个ACTION的,这也是为什么onTouchEvent为
什么会一直传递到A的原因。所以ACTION_MOVE和ACTION_UP等事件得不到相应(处理),此种情况下即使
你在D的onTouchEvent方法里面写了如下代码,也不会得到执行。
[java] view plain copy 在CODE上查看代码片派生到我的代码片
if(event.getAction()==MotionEvent.ACTION_MOVE){
Log.e(tag, "--onTouchEvent--*****");
}
1)如果A的InterceptTouchEvent返回了true,其余的仍然返回false,那么执行输出的log为:
转换成效果图为:
可以发现此时A拦截了此次Touch事件,事件不再向A的子控件B C D传递。此时所有的action事件比如手
指移动事件ACTION_MOVE或者ACTION_UP事件啦等等事件都交给A的onTouchEvent方法去处理(当然这是在
onTouchEvent方法返回true的情况下,如果返回false经过测试时不会相应这些action的)。B,C ,D控件
是的事件处理拦截方法和事件处理方法是无法得到执行的。
2)只有B的onIntercepteTouchEvent事件返回了true的情况下,打印的log为
转换成效果图为:
此时由B拦截了此次Touch事件,并不会向C D子控件传递;同样的由于onTouchEvent事件返回为false,所
以此次事件的event.getAction()的各种action都不会得到处理。
4)同理可知,C控件的onIntercept方法返回了true的情况下,其余的仍然返回false的情况下,输出log
为
转换成效果图为
下面说说各个view的onTouchEvent返回true的情况
由于onTouchEvent事件是从子控件到父控件传递的,当D的onTouchEvent返回true的时候,经测试输出效
果如下
转换成效果图为:
经过测试发现,此时D处理了此次Touch事件的各种action,C B D是的onTouchEvent的没有得到执行。
同理当C的onTouchEvent方法返回了true的时候,输出的log如下
转换成效果图如下:
经过测试发现,此时事件的各个action都在CView的onTouchEvent方法得到了响应,而D的onTouchEvent
是不会相应MotionEvent.ACTION_XX的。其余情况一次类推,就不在啰嗦了。经过一步步的测试得出了文
章开头的结文章有点啰嗦,希望可以对阅读此文的人有所帮助。
结合事件源码分析事件处理机制的话,可以阅读《从源码角度解析android事件原理》
========
Android事件分发机制浅析
目录(?)[+]
事件机制是Android中一个比较复杂且重要的知识点,比如你想自定义拦截事件,或者某系组件
中嵌套了其他布局,往往会出现这样那样的事件冲突,坑爹啊!!事件主要涵盖onTouch,onClick,
onTouchEvent,dispatchTouchEvent,onInterceptTouchEvent等等一系列事件,并且事件间还相互交互
耦合,甚至有的事件还有返回值,一会true,一会false,什么情况下返回true,什么情况下返回false
,为什么要有返回值,想想这些就感觉整个人都不好了。
这里写图片描述
但是(万恶的但是),该知识点还是必须要掌握的,知识的深度与广度决定了你走的远度,鉴于
此我们就来捅一捅该知识点。
准备工作
俗话说工欲善其事必先利其器,为了看他的执行流程,我们还是先写个样例,打几个日志看看执
行流程吧!
首先自定义一个外层布局的Layout,自定义Layout继承了LinearLayout,复写了相应的函数,在
调用之前输入日志。如下:
public class Layout extends LinearLayout {
public Layout(Context context) {
super(context);
init();
}
public Layout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public Layout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//requestDisallowInterceptTouchEvent(false);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e("Event", "Layout onInterceptTouchEvent " + MotionEvent.actionToString
(ev.getAction()));
return super.onInterceptTouchEvent(ev);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("Event", "Layout onTouchEvent " + MotionEvent.actionToString(event.getAction
()));
return super.onTouchEvent(event);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e("Event", "Layout dispatchTouchEvent " + MotionEvent.actionToString
(event.getAction()));
return super.dispatchTouchEvent(event);
}
}
我们还自定义了一个LogTextView,继承自TextView,也是为了输出日志,代码如下:
public class LogTextView extends TextView {
public LogTextView(Context context) {
super(context);
}
public LogTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LogTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("Event", "TextView onTouchEvent " + MotionEvent.actionToString
(event.getAction()));
return super.onTouchEvent(event);
}
@Override
public void setOnTouchListener(OnTouchListener l) {
super.setOnTouchListener(l);
}
@Override
public void setOnClickListener(OnClickListener l) {
super.setOnClickListener(l);
}
}
接下来是布局文件了:
<?xml version="1.0" encoding="utf-8"?>
<com.sunny.event.wigdet.Layout
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"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<com.sunny.event.wigdet.LogTextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="40dp"
android:background="#999999"
android:padding="20dp"
android:text="Hello World!"/>
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:src="@mipmap/ic_launcher"/>
</com.sunny.event.wigdet.Layout>
布局中嵌套了两个view,一个TextView,一个ImageView。最后就是主界面了。
public class MainActivity extends AppCompatActivity {
private LogTextView tv;
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViews();
setViewListener();
}
private void findViews() {
tv = (LogTextView) findViewById(R.id.textView);
imageView = (ImageView) findViewById(R.id.image);
}
private void setViewListener() {
tv.setOnTouchListener(new View.OnTouchListener() {
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("Event", "TextView onTouch " + MotionEvent.actionToString
(event.getAction()));
return true;
}
});
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("Event", "TextView onClick ");
}
});
imageView.setOnTouchListener(new View.OnTouchListener() {
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.e("Event", "ImageView onTouch " + MotionEvent.actionToString
(event.getAction()));
return false;
}
});
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e("Event", "ImageView onClick ");
}
});
}
}
执行结果
round 1
TextView的onTouch返回为false,点击TextView,日志如下:
05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: Layout dispatchTouchEvent
ACTION_DOWN
05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: Layout onInterceptTouchEvent
ACTION_DOWN
05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: TextView onTouch ACTION_DOWN
05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: TextView onTouchEvent ACTION_DOWN
05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: Layout dispatchTouchEvent
ACTION_MOVE
05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: Layout onInterceptTouchEvent
ACTION_MOVE
05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: TextView onTouch ACTION_MOVE
05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: TextView onTouchEvent ACTION_MOVE
05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: Layout dispatchTouchEvent
ACTION_UP
05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: Layout onInterceptTouchEvent
ACTION_UP
05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: TextView onTouch ACTION_UP
05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: TextView onTouchEvent ACTION_UP
05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: TextView onClick
根据日志我们可以看到首先有一个ACTION_DOWN事件,执行的顺序是Layout的
dispatchTouchEvent→onInterceptTouchEvent→(TextView)onTouch要→onTouchEvent,之后的我帕
金森发生了,产生了ACTION_MOVE事件,传递的顺序与Down是一致的,最后一个事件是UP事件,正常点击
不滑动是不会产生MOVE事件的,在这个这个三个事件最后调用了TextView的onClick事件。
小结:
1 . 事件的传递顺序是先外层容器,之后再是具体的View。
2. onTouch事件先于onTouchEvent事件,onTouchEvent先于onClick事件
round 2
我们将TextView的onTouch事件返回true。重新执行。执行顺序如下:
05-05 14:10:20.556 27644-27644/com.sunny.event E/Event: Layout dispatchTouchEvent
ACTION_DOWN
05-05 14:10:20.556 27644-27644/com.sunny.event E/Event: Layout onInterceptTouchEvent
ACTION_DOWN
05-05 14:10:20.556 27644-27644/com.sunny.event E/Event: TextView onTouch ACTION_DOWN
05-05 14:10:20.616 27644-27644/com.sunny.event E/Event: Layout dispatchTouchEvent
ACTION_MOVE
05-05 14:10:20.616 27644-27644/com.sunny.event E/Event: Layout onInterceptTouchEvent
ACTION_MOVE
05-05 14:10:20.616 27644-27644/com.sunny.event E/Event: TextView onTouch ACTION_MOVE
05-05 14:10:20.622 27644-27644/com.sunny.event E/Event: Layout dispatchTouchEvent
ACTION_UP
05-05 14:10:20.622 27644-27644/com.sunny.event E/Event: Layout onInterceptTouchEvent
ACTION_UP
05-05 14:10:20.622 27644-27644/com.sunny.event E/Event: TextView onTouch ACTION_UP
从日志可以看出如果onTouch返回为true,执行顺序变成了如下:
首先还是ACTION_DOWN事件(Layout)dispatchTouchEvent→onInterceptTouchEvent→
(TextView)onTouch,ACTION_MOVE与ACTION_UP执行顺序同ACTION_DOWN,可以发现的是TextView的
onTouchEvent事件没有了,并且onClick事件也没有了。
小结
1,onTouch事件的返回值为true会拦截onTouchEvent事件
2,onTouchEvent与onClick有关联
上面的两次执行中每次都调用了onInterceptTouchEvent事件,这个到底又是啥?我们去看看他
的返回值是什么?
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
可以看到默认返回false,注释长的吓人,那我们就来改写一下他的返回值,这个函数是
ViewGroup才有的,说明与布局容器有关.
round 3
我们将Layout的onInterceptTouchEvent的返回值改为true。重新执行。执行顺序如下:
05-05 14:59:17.829 15157-15157/com.sunny.event E/Event: Layout dispatchTouchEvent
ACTION_DOWN
05-05 14:59:17.830 15157-15157/com.sunny.event E/Event: Layout onInterceptTouchEvent
ACTION_DOWN
05-05 14:59:17.830 15157-15157/com.sunny.event E/Event: Layout onTouchEvent ACTION_DOWN
从日志可以发现,只有最外层的控件能够执行事件,TextView已经收不到任何事件。
小结
父控件onInterceptTouchEvent返回true会拦截子控件的事件
追根溯源
我们从代码的层面来看看他是怎么执行的,当屏幕接收到点击事件时会首先传递到Activity的
dispatchTouchEvent:
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
在这里执行了三步,
1.第一告诉用户ACTION_DOWN,用户可以复写onUserInteraction来处理点击开始
2.调用了getWindow().superDispatchTouchEvent(ev),这里的getWindow得到是PhoneWindow对象,因此
执行的PhoneWindow的superDispatchTouchEvent函数,
3.调用了Activity的onTouchEvent事件
PhoneWindow的superDispatchTouchEvent又调用了DecorView的superDispatchTouchEvent函数,
每一个Activity都有一个PhoneWindow,每一个PhoneWindow都有一个DecorView,DecoView继承自
FrameLayout,这里又调用了super.dispatchTouchEvent(event),FrameLayout里面是没有改函数的,所
以最终执行的是ViewGroup的dispatchTouchEvent函数。
这里我们先穿插一点界面的知识,以我测试手机为例,DecorView中有两个child,分别是
ViewStub和LinerLayout,LinerLayout中又包含了FrameLayout,FrameLayout中包含了一个
ActionBarOverlayLayout,ActionBarOverlayLayout里又包含了两个child,分别是ActionBarContainer
与ContentFrameLayout。
接下来我们去看看ViewGroup的dispatchTouchEvent函数:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
.........
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
............
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
.......
if (!canViewReceivePointerEvents(child) || !
isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is
handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child,
idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original
index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child,
target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled || actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
我们只看最重要的部分
1: 事件为ACTION_DOWN时,执行了cancelAndClearTouchTargets函数,该函数主要清除上一次点击传递
的路径,之后执行了resetTouchState,重置了touch状态,其中执行了 mGroupFlags &=
~FLAG_DISALLOW_INTERCEPT;就是拦截状态为false,这个与requestDisallowInterceptTouchEvent函数
相关。
2: 获取intercepted的值,首先判断了disallowIntercept状态,是否拦截子控件的事件执行。从代码
可以看到当disallowIntercept为false时,该状态主要取决于onInterceptTouchEvent函数的返回值,这
就是前面我们拦截的函数,如果为true,这时intercepted为true标识拦截。
3: 接着判断了!canceled && !intercepted的值,canceled这里为false,如果intercepted为false,
则会进入判断条件,这里假设不拦截,进入后继续判断如果是ACTION_DOWN事件,则会继续进入判断,遍
历所有子控件,isTransformedTouchPointInView会判断当前点击区域是否在控件内,如果不在则遍历下
一个,之后调用dispatchTransformedTouchEvent函数。最后在调用addTouchTarget函数,将当前选中的
控件,挂载到当前点击目标链表。alreadyDispatchedToNewTouchTarget赋值为true。
接着判断mFirstTouchTarget是否为空,经过上一步的addTouchTarget的执行,这里mFirstTouchTarget
不为空。第一个事件alreadyDispatchedToNewTouchTarget为true,且target == newTouchTarget,因此
handled值为true,如果是后续的事件,则会进入dispatchTransformedTouchEvent中。
我们接着看看第三部中的dispatchTransformedTouchEvent函数:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
.......
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
.......
// Done.
transformedEvent.recycle();
return handled;
}
这里会进入到第10行,且传递过来的child不为空,因此会继续执行child.dispatchTouchEvent
,这里继续执行ViewGroup的dispatchTouchEvent,一直递归执行,直到真正接受点击的控件,到最后
child会为空,这里要么是一个View控件,要么是未包含任何子控件的ViewGroup,这时这里会执行View
的dispatchTouchEvent。
从上述执行逻辑可以直到,先从DecorView一直递归到Layout,最后再到TextView,这里我们去
看看View的dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent event) {
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) ==
ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL
||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
首先会停止掉嵌套滑动,之后先判断了ListenerInfo不为空,这里只要是设置了onTouch,onKey
,onHover,onDrag等等中的任何一个这里就不为空,具体可以去看看ListenerInfo包含的listenerInfo
类型。其次判断了mOnTouchListener不为空,只要设置了onTouchListener这里就不为空,再之后判断了
该控件是否是enabled,一般都会enabled,可以代码设置为false,再之后调用了mOnTouchListener的
onTouch事件,这里就是外面传进来的onTouchListener,从这里可以看到无论onTouch返回任何值,
onTouch事件都会执行,但是如果返回为true,则会导致result为true,!result && onTouchEvent
(event)因为短路,不会执行到onTouchEvent事件。
小结
1:onTouch返回为true导致onTouchEvent不能执行
2:如果enable为false,因为短路onTouch不会执行
到此还没有看到任何onClick事件的执行,我们继续去看看onTouchEvent函数:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
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, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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();
}
}
}
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();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
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();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// 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;
}
return true;
}
return false;
}
我们首先看ACTION_DOWN事件,这里主要看checkForLongClick,CheckForTap中也调用了该函数
,这里就是添加一个长按事件,如果达到长按标准且长按listener不为空,则执行长按事件,接着我们
看ACTION_UP,这里看到如果不是长按事件,则调用了performClick,performClick里面执行了onClick
事件。
小结
1:onClick事件与onLongClick事件是在onTouchEvent中执行的
2:如果执行了长按事件则onClick不执行
3:就api 23代码,长按的时间间隔为500毫秒
上面解析了intercepted为false的情况,那intercepted为true,它到底是怎么拦截的?
如果intercepted为true,则!canceled && !intercepted为false,不能进入该判断,
mFirstTouchTarget为空,会继续执行如下分支:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
这里第三参数传递的child为null,因此就会执行该控件onTouch与onTouchEvent函数,不会继续
递归传递,因此也就拦截了子控件的执行。
总结
事件接收先从父控件到子控件,如果父控件onInterceptTouchEvent为true,则表示拦截事件。
dispatchTouchEvent的ACTION_DOWN事件中,会清除上一次的点击目标列表,且重置disallowIntercept
状态为false,表示拦截,但是真正的拦截状态还是靠onInterceptTouchEvent函数的返回值决定。
如果为自定义控件,还需要重写onInterceptTouchEvent。
如果onLongClick执行,api 23 默认时间为500毫秒,则onClick不执行。
如果onTouch事件返回为true,则会拦截onTouchEvent事件,onClick,onLongClick事件均不在执行。
========