新版的view的事件分发机制相较于老版本的还是改动了一些地方,这个准备记录下来,以免忘记和不时之需。
Android中的触摸事件的分发执行顺序是从ViewGroup中的dispatchTouchEvent–onInterceptTouchEvent,如果onInterceptTouchEvent返回true,表示拦截,这时执行ViewGroup中的onTouch方法。如果onInterceptTouchEvent返回false,这时触摸事件就分发到下一级别,也就是ViewGroup 里面的view中了。
代码测试,首先定义一个MyLayout继承LinearLayout
package com.example.administrator.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
public class MyLayout extends LinearLayout {
public MyLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction())
{
case MotionEvent.ACTION_DOWN:
Log.e("ethan","layout dispatch Action Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("ethan","layout dispatch Action Move");
break;
case MotionEvent.ACTION_UP:
Log.e("ethan","layout dispatch Action Up");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction())
{
case MotionEvent.ACTION_DOWN:
Log.e("ethan","InterceptionTouchEvent Action Down");
return false;
case MotionEvent.ACTION_MOVE:
Log.e("ethan","InterceptionTouchEvent Action Move");
return false;
case MotionEvent.ACTION_UP:
Log.e("ethan","InterceptionTouchEvent Action Up");
return false;
}
return false;
}
}
该类重写了LinearLayout中的dispatchTouchEventon和InterceptTouchEvent方法,同时对于每一个事件传递都打印出相应的状态。然后定义一个xml布局文件。
<?xml version="1.0" encoding="utf-8"?>
<com.example.administrator.view.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/myLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button" />
<ImageView
android:id="@+id/imageview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />
</com.example.administrator.view.MyLayout>
该布局文件包含一个Button,和一个ImageView,至于为什么包含这两个,因为Button和ImageView的处理效果有一些不一样。下面就是主布局文件
package com.example.administrator.view;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button button;
private ImageView imageView;
private MyLayout mylayot;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.ethan);
init();
}
private void init() {
button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("ethan", "button onTouch ACTION_DOWN");
return false;
case MotionEvent.ACTION_MOVE:
Log.e("ethan", "button onTouch ACTION_MOVE");
return false;
case MotionEvent.ACTION_UP:
Log.e("ethan", "button onTouch ACTION_UP");
return false;
}
return false;
}
});
imageView = (ImageView) findViewById(R.id.imageview);
imageView.setOnClickListener(this);
imageView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("ethan","imageView onTouch ACTION_DOWN");
return false;
case MotionEvent.ACTION_MOVE:
Log.e("ethan","imageView onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e("ethan","imageView onTouch ACTION_UP");
return false;
}
return false;
}
});
mylayot = (MyLayout) findViewById(R.id.myLayout);
mylayot.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("ethan","mylayot onTouch ACTION_DOWN");
return false;
case MotionEvent.ACTION_MOVE:
Log.e("ethan","mylayot onTouch ACTION_MOVE");
return false;
case MotionEvent.ACTION_UP:
Log.e("ethan","mylayot onTouch ACTION_UP");
return false;
}
return false;
}
});
mylayot.setOnClickListener(this);
}
@Override
public void onClick (View v){
switch (v.getId())
{
case R.id.button:
Log.e("ethan","button clicked");
break;
case R.id.imageview:
Log.e("ethan","imageview clicked");
break;
case R.id.myLayout:
Log.e("ethan","layout clicked");
break;
}
}
}
主类里面分别注册了 layout,button,imageview的onTouch事件和OnClick事件。大体的事件分发机制架构实现了,测试下。
点击 layout(空白处)和button,imageview 分别提示为
layout log:
E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: mylayot onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: mylayot onTouch ACTION_UP
E/ethan: layout clicked
button log:
E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: button onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: InterceptionTouchEvent Action Up
E/ethan: button onTouch ACTION_UP
E/ethan: button clicked
imageview log:
E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: imageView onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: InterceptionTouchEvent Action Up
E/ethan: imageView onTouch ACTION_UP
E/ethan: imageview clicked
三次都是点击事件为了简单没有触发Action_Move,咋一看三次点击log都是一样的。同时事件分发是Action_Down分发完毕后,再次从viewgroup分发Action_up,Action_Move同理。这时候在MainActivity中把 button.setOnClickListener(this);和imageView.setOnClickListener(this);都注释掉,再次看一下点击效果。
button log:
E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: button onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: InterceptionTouchEvent Action Up
E/ethan: button onTouch ACTION_UP
imageview log:
E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: imageView onTouch ACTION_DOWN
E/ethan: mylayot onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: mylayot onTouch ACTION_UP
E/ethan: layout clicked
这时候一个奇怪的现象出现了 两个View都没有注册点击事件,button执行到button onTouch ACTION_UP结束,而imageview执行到layout clicked结束。这是因为在事件分发中,一次点击事件首先由ViewGroup分发,如果不拦截,进入到子View中,如果子View中没有消耗该点击事件的方法,那么这次点击事件就会会传到ViewGroup中去执行。 可以想象为机场行李的传送带,行李出来后,相当与点击事件下发到子View中,如果不拿行李,相当于不消耗该点击事件,那么行李还会回传回去,也就是点击事件又传回ViewGroup,由ViewGroup中的onTouch再次进行处理。
但是button的点击事件为什么没有回传到ViewGroup中呢?明明button的注册点击事件也注释掉了。这是因为button默认是可以点击的,而imageview默认是不可点击的。可点击的button,虽然没有注册点击事件,但是系统还是默认它消耗掉了该事件,imageview默认不可点击,默认不消耗该事件。当执行imageView.setOnClickListener(this)的时候相当于imageview也可以点击了,自然和button一样,也就是上面的button和imageview的打印log一样的情况。
这时候在xml中把button的android:clickable=”false”再次运行点击
button log:
E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: button onTouch ACTION_DOWN
E/ethan: mylayot onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: mylayot onTouch ACTION_UP
E/ethan: layout clicked
这时候发现,button和imageview都没有注册点击事件,但是两个的log一样了,最终都回传到ViewGroup中进行处理了。
总结:事件分发先从Viewgroup下发,到子View中进行处理,如果子View消耗点击事件,那么事件不回传,如果子View不消耗点击事件,子View回传到ViewGroup中处理。同时就是子View没有注册点击事件,但是只要是子View是可以点击的,那么系统就默认该点击事件被该子View消耗掉了,也就不回传到Viewgroup中处理。
事件分发流程处理完了,继续拦截事件的分析,拿button为例,其余同理:
首先把mylayout中的onInterceptTouchEvent 中的MotionEvent.ACTION_DOWN:return true。
button log:
E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: mylayot onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: mylayot onTouch ACTION_UP
E/ethan: layout clicked
当mylayout中的onInterceptTouchEvent 中的MotionEvent.ACTION_DOWN:return true的时候也就是down点击事件就被拦截了,那么直接执行layout中的touch方法和click方法,并且Viewgroup再分发Action Up的时候就不经过onInterceptTouchEvent 了,直接执行onTouch ACTION_UP;
这是换一种,在mylayout中的onInterceptTouchEvent 中的MotionEvent.ACTION_Up:return true其他的都不拦截
button log:
E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: button onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: InterceptionTouchEvent Action Up
这时候由于ViewGroup没有拦截action down 所以能够执行button的 ontouch,分发到 Action Up时拦截,就不继续分发了。但是log中并没有显示任何onclick,那么这次点击事件是被谁消耗掉了呢?由于执行了button 的ontouch方法,button可点击并注册了点击事件,那么默认点击事件是被button消耗了,但是由于action up被ViewGroup拦截掉了,因此button的onclick方法并不会执行。这时候把button的clicable改为false 并且取消注册点击事件
button log:
E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: button onTouch ACTION_DOWN
E/ethan: mylayot onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: mylayot onTouch ACTION_UP
E/ethan: layout clicked
这时候 看到layout clicked,由于button不可点击并且没有注册点击事件(其实注册点击事件默认的也就把View设置为可点击),因此该点击事件回传到ViewGroup中被ViewGroup消耗掉。
同理,如果在button的ontouch中拦截了点击事件,哪怕只拦截一个,最终button的onclick方法都不会执行,但是点击事件还是被button给消耗掉了。
修改layout 三个都不拦截,在button的ontouch的action up中拦截
button log:
E/ethan: layout dispatch Action Down
E/ethan: InterceptionTouchEvent Action Down
E/ethan: button onTouch ACTION_DOWN
E/ethan: layout dispatch Action Up
E/ethan: InterceptionTouchEvent Action Up
E/ethan: button onTouch ACTION_UP
最终执行到button ontouch action up就不执行了,点击事件被消耗,但是由于action up被拦截因此并不会执行。
总结:事件分发到View的时候,如果view是可以点击的,不管是否注册了setonclicklistener,默认该点击事件被该View给消耗掉,继续下发action move 和action down。如果action down,action move,action up中有一个被拦截,那么虽然消耗点击事件但是不会继续执行onclick方法。如果view是不可点击的,默认该View不能消耗掉点击事件,那么会回传到Viewgroup中,由ViewGroup的Ontouch方法继续执行。