如何正确的给ViewGroup设置OnClickListener

在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;
@Override
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() {
@Override
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);
}
@Override
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这一块

image

 明确上面两点后,解下来就比较容易理解了。

方法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);
}
@Override
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;
@Override
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() {
@Override
public void onClick(View v) {
Log.d(TAG,"onClick...."+ v.toString());
}
};
}

 整个代码内容都很简单,为什么我们要定义一个继承Button的类,这里主要是为了通过Android Studio结合源码调试。我们在MyButton的”boolean result = super.onTouchEvent(event);”前面打个断点,然后开始debug调试。

  • 在你的Android设备上点击button

image

如上图,然后按”F7”进入super.onTouchEvent(),如果你有安装多个sdk版本,选择你Android设备对应的版本,比如我的设备是Android4.4,那么我就选择API 19。

image

  • 选择对应的的版本后,进入到TextView(Button继承自TextView)的onTouchEvent()方法,

image

按“F8”到下一步,然后到“final boolean superResult = super.onTouchEvent(event);”,按“F7”进入View的onTouchEvent()方法。

image

然后一直“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”。