在Android的日常开发中,我们总会碰到要给某个LinearLayout、RelativeLayout等设置OnClickListener,以便达到点击其子view能够触发设置的OnClickListener。但是当我们点击子view的时候,对应的Listener并没有触发到,这是为什么呢,接下来我们将结合例子从源码角度去解释它。
实例
? 我们从一个简单的需求出发:有一个Button和一个TextView,当他们被点击后都要响应相同的事件。我们可以同时设置Button和TextView的点击监听,但是这样代码量就比较大了。作为爱偷懒的程序员,又怎么愿意这么干呢,反正我的内心是拒绝的。我们容易想到的方法就是给Button和TextView所在的ViewGroup设置点击事件。可能我们会这样做:
- 假如布局文件我们是这样写的:
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
|
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/layout_group"
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:layout_marginLeft="100dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/test"
android:textSize="30sp"
android:layout_marginLeft="30dp"
android:id="@+id/tv"/>
</LinearLayout>
</RelativeLayout>
|
- 代码内容也很简单,信手捏来
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
|
package com.zhuzp.test;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
/**
* Created by zhuzp on 2016/9/29.
*/
public class TestActivity extends Activity {
private static final String TAG = "TestActivity";
private View layoutView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_activity);
initView();
}
private void initView(){
layoutView = findViewById(R.id.layout_group);
layoutView.setOnClickListener(mClickListener);
}
private View.OnClickListener mClickListener = new View.OnClickListener() {
public void onClick(View v) {
Log.d(TAG,"onClick...."+ v.toString());
}
};
}
|
我们在代码中给Button和TextView所在的LinearLayout中设置了点击事件监听。Easy,是不是?然后我们看下运行的结果吧。
?点击Button怎么没有打印出来,但是点击TextView的时候有打印,打印的结果如下:
1
|
01-02 14:19:32.220 5891-5891/com.zhuzp.test D/TestActivity: onClick....com.zhuzp.test.widget.MyLinearLayout{41907c58 V.E...C. ...P.... 64,16-339,64 #7f0c0063 app:id/layout_group}
|
不和常理啊,按道理应该都有打印出来的,对不对?嗯,有可能你知道问题出在哪,下面我先给出几种解决方法,然后再解释为什么这几种方法是可行的。
解决方法
1,重写LinearLayout的setOnClickListener(View.OnClickListener listener)方法,通过给每个子view设置点击事件。
比如我们定义个MyLinearLayout,继承LinerLayout。
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
|
package com.zhuzp.test.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
/**
* Created by zhuzp on 2016/9/28.
*/
public class MyLinearLayout extends LinearLayout {
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setOnClickListener(OnClickListener l) {
super.setOnClickListener(l);
int N = getChildCount();
for (int i = 0; i < N; i++){
View view = getChildAt(i);
view.setOnClickListener(l);
}
}
}
|
? 然后修改布局文件
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
|
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
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.zhuzp.test.widget.MyLinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/layout_group"
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:id="@+id/btn"
android:layout_marginLeft="100dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text"
android:textSize="30sp"
android:layout_marginLeft="30dp"
android:id="@+id/tv"/>
</com.zhuzp.test.widget.MyLinearLayout>
</RelativeLayout>
|
? 然后我们看下运行的结果:
1
2
|
10-08 12:00:12.765 6247-62471/com.zhuzp.test D/TestActivity: onClick....android.widget.Button{4190a4a0 VFED..C. ...P.... 100,0-187,48 #7f0c0064 app:id/btn}
10-08 12:00:12.765 6247-6247/com.zhuzp.test D/TestActivity: onClick....android.widget.TextView{41911480 V.ED..C. ...P.... 217,3-275,44 #7f0c0066 app:id/tv}
|
从结果来看,看样子我们是已经解决了问题,OK,这就是第一种方法。
2,重写LinearLayout的onInterceptTouchEvent()方法,返回true。
?代码就不重复了,和方法1中基本类似。结果每次点击Button或者TextView时,打印信息为:
1
2
|
10-08 12:02:12.765 6247-6247/com.zhuzp.test D/TestActivity: onClick....android.widget.LinearLayout{419cd1b0 V.E...C. ...P.... 16,16-291,64 #7f0c0063 app:id/layout_group}
10-08 12:02:14.355 6247-6247/com.zhuzp.test D/TestActivity: onClick....android.widget.LinearLayout{419cd1b0 V.E...C. ...P.... 16,16-291,64 #7f0c0063 app:id/layout_group}
|
从打印信息来看我们的问题好像也得到了解决,但是如果你的Button 和TextView设置了背景selector,是没有相应的效果的。
3,增加Button 的android:clickable=“false”.
? 布局文件如下:
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
|
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/layout_group"
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:layout_marginLeft="100dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:text="Button1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text"
android:textSize="30sp"
android:layout_marginLeft="30dp"
android:id="@+id/tv"/>
</LinearLayout>
</RelativeLayout>
|
然后,我们看下点击Button和TextView打印的log 的结果:
1
2
|
10-08 12:05:12.765 6247-6247/com.zhuzp.test D/TestActivity: onClick....android.widget.LinearLayout{419cd1b0 V.E...C. ...P.... 16,16-291,64 #7f0c0063 app:id/layout_group}
10-08 12:05:14.355 6247-6247/com.zhuzp.test D/TestActivity: onClick....android.widget.LinearLayout{419cd1b0 V.E...C. ...P.... 16,16-291,64 #7f0c0063 app:id/layout_group}
|
从log中我们可以看到,当我们点击Button和TextView的时候,实际上响应点击的是他们所在的ViewGroup——LinearLayout。
对比两种方式,很明显第三种方法来得简单
原因分析
? 首先要明确:
- 当我们给View(ViewGroup)设置了View.OnClickListener后,View (ViewGroup)是否响应点击事件是在其onTouchEvent(MotionEvent event)判断的。
- View事件分发的流程图,这里我们只关心ViewGroup和View这一块
?明确上面两点后,解下来就比较容易理解了。
方法1其实是一种比较hack的方法,为每个子view设置了监听,最终由每个子view去响应点击事件。方法二,结合上面的图就很好理解了,子view根本没有收到touch 事件,touch事件由ViewGroup的onTouchEvent()方法处理,在onTouchEvent()方法中会去处理点击事件。方法3为什么可以呢,从上面的图你可能已经猜到在Button设置了android:clickable=”false”属性之后,Button 的onTouchEvent()应该是返回了false。究竟是否是这样的呢,接下来我们一起结合源码调试下,你没听错,就是调试。
源码解析
?知道上面的结论后,我们开始结合代码验证下:
- 创建一个类继承Button,方法比如命名为MyButton,重写onTouchEvent()。
- 在布局文件中引用创建的MyButton。
MyButton代码如下:
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
|
package com.zhuzp.test.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
/**
* Created by zhuzp on 2016/9/29.
*/
public class MyButton extends Button {
private static final String TAG = "MyButton";
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean onTouchEvent(MotionEvent event) {
boolean result = super.onTouchEvent(event);
Log.d(TAG,"onTouchEvent = " + result);
return result;
}
}
|
布局文件如下:
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
|
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/layout_group"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.zhuzp.test.widget.MyButton
android:layout_marginLeft="100dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text"
android:textSize="30sp"
android:layout_marginLeft="30dp"
android:id="@+id/tv"/>
</LinearLayout>
</RelativeLayout>
|
最后Activity代码如下:
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
|
package com.zhuzp.test;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
/**
* Created by zhuzp on 2016/9/29.
*/
public class TestActivity extends Activity {
private static final String TAG = "TestActivity";
private View layoutView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_activity);
initView();
}
private void initView(){
layoutView = findViewById(R.id.layout_group);
layoutView.setOnClickListener(mClickListener);
}
private View.OnClickListener mClickListener = new View.OnClickListener() {
public void onClick(View v) {
Log.d(TAG,"onClick...."+ v.toString());
}
};
}
|
?整个代码内容都很简单,为什么我们要定义一个继承Button的类,这里主要是为了通过Android Studio结合源码调试。我们在MyButton的”boolean result = super.onTouchEvent(event);”前面打个断点,然后开始debug调试。
- 在你的Android设备上点击button
如上图,然后按”F7”进入super.onTouchEvent(),如果你有安装多个sdk版本,选择你Android设备对应的版本,比如我的设备是Android4.4,那么我就选择API 19。
- 选择对应的的版本后,进入到TextView(Button继承自TextView)的onTouchEvent()方法,
按“F8”到下一步,然后到“final boolean superResult = super.onTouchEvent(event);”,按“F7”进入View的onTouchEvent()方法。
然后一直“F8”,然后走到”if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE))”中,你会发现进入了这个if语句后,View的onTouchEvent()方法返回的都是true,如果没进if语句,返回的就是false。
当从View的onTouchEvent()中返回后,回到TextView.onTouchEvent()方法,然后一步步走下去,你会发现最终TextView.onTouchEvent()返回的就是super.onTouchEvent()的返回值。所以要想上面的例子中的LinearLayout响应点击事件,要设置Button的android:clickable=”false”。