之前写过一篇Android
事件分发机制详解 ,感觉比较乱,这里再总结一下。网上已经有很多前辈分析过源码,大家可以参考,我这里尽量不做过多的源码分析,仅仅从流程上分析。
0x01 基础部分 事件分发和消费我们主要涉及到以下三个方法: dispatchTouchEvent():分发事件 onInterceptTouchEvent():拦截事件 onTouchEvent():处理事件 还需要注意常用的两个接口对以上方法的影响: OnClickListener :点击事件监听 OnTouchListener :触摸事件监听
最后再认识一下MotionEvent,他表示用户的触摸事件,用户的点击、触摸或者滑动都会产生相应的MotionEvent,而我们所要关心得主要有三种: MotionEvent.ACTION_DOWN :int类型,值为0,表示用户的手指刚接触到屏幕 MotionEvent.ACTION_UP :int 类型,值为1,表示用户的手指从屏幕上抬起 MotionEvent.ACTION_MOVE :int 类型,值为2,表示用户的手指正在屏幕上移动
例如当我们触摸屏幕,可能产生一系列的事件: 点击一下屏幕,会产生ACTION_DOWN,ACTION_UP 点击屏幕并滑动然后松开,会产生ACTION_DOWN,一系列的ACTION_MOVE,ACTION_UP事件。
当然还有其他的动作,我们不做多的讲解,这三个动作是我们在操作手机时最常见到的,在处理滑动冲突中很关键,下文将会讲到。
0x02 View的事件分发与处理 这里的View一般指的是像Button,TextView这种,处于事件分发的最后一级。主要包含dispatchTouchEvent和onTouchEvent两个方法。我们注意到他没有onInterceptTouchEvent方法所以也就不需要判断是否需要拦截事件。为了方便讲解,这里举一个小例子来辅助说明。
首先定义一个CustomButton并重写其dispatchTouchEvent和onTouchEvent方法:
public class CustomButton extends AppCompatButton { String TAG = "CustomButton";
public CustomButton(Context context) {
super(context);
}
public CustomButton(Context context, AttributeSet
attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
Log.e(TAG, "onTouchEvent:" + "action:"
+ event.getAction());
boolean result = super.onTouchEvent(event);
Log.e(TAG, "super.onTouchEvent:result="
+ result);
return result;
}
@Override
public boolean dispatchTouchEvent(MotionEvent
event) {
Log.e(TAG, "dispatchTouchEvent:" + "action:"
+ event.getAction());
boolean result = super.dispatchTouchEvent(event);
Log.e(TAG, "super.dispatchTouchEvent:result="
+ result);
return result;
}
} |
在activity_main布局文件中使用该View,运行程序,点击该View一下,得以下结果:
CustomButton: dispatchTouchEvent:action:0 CustomButton: onTouchEvent:action:0 CustomButton: super.onTouchEvent:result=true CustomButton: super.dispatchTouchEvent:result=true CustomButton: dispatchTouchEvent:action:1 CustomButton: onTouchEvent:action:1 CustomButton: super.onTouchEvent:result=true CustomButton: super.dispatchTouchEvent:result=true |
从上边的日志可以得出: 1.dispatchTouchEvent会先于onTouchEvent方法回调。 2.对于该点击动作,先处理action为0,后处理action为1的事件。上文中讲到了0是MotionEvent.ACTION_DOWN事件,1是MotionEvent.ACTION_UP事件。
3.默认情况下dispatchTouchEvent和onTouchEvent方法都返回的true。这表示该View将要消费这些接收到的事件
阅读过源码的同学应该知道,该View的父ViewGroup将事件分发到它,也就是回调该View的dispatchTouchEvent方法。源码中要处理的事情很多,这里就不详细的介绍了,当然还是希望有兴趣的同学去阅读一下源码的。View的dispatchTouchEvent的方法被回调时也就是事件被交给View处理了,它可以选择去处理消费这些事件,也可以选择不去消费(onTouchEvent返回false)。如果返回false那么它又会将事件传递给父ViewGroup这是后话。
接下来我们讨论OnTouchListener,OnClickListener的影响。我们在MainActivity中给CustomButton设置一下监听:
public class MainActivity extends AppCompatActivity implements View.OnTouchListener, View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CustomButton cb = (CustomButton) findViewById(R.id.btn_1);
cb.setOnClickListener(this);
cb.setOnTouchListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event)
{
Log.e("onTouch", "onTouch:action:"+event.getAction());
return false;
}
@Override
public void onClick(View v) {
Log.e("onClick", "Button click");
}
} |
再点击一下CustomButton会得到以下Log:
CustomButton: dispatchTouchEvent:action:0 onTouch: onTouch:action:0 CustomButton: onTouchEvent:action:0 CustomButton: dispatchTouchEvent:action:1 onTouch: onTouch:action:1 CustomButton: onTouchEvent:action:1 onClick: Button click |
可以看出执行顺序为dispatchTouchEvent->onTouch->onTouchEvent->onClick,此时onTouch方法返回的为false,也就是表示它不会处理这些事件。那么我们将onTouch方法返回为true在重复上边的操作会得到什么结果呢?
CustomButton: dispatchTouchEvent:action:0 onTouch: onTouch:action:0 CustomButton: dispatchTouchEvent:action:1 onTouch: onTouch:action:1 |
执行结果dispatchTouchEvent->onTouch。可以看到onTouchEvent和onClick没有执行。要解释清楚这个就必须看看源码了。下边是我整理的View.dispatchTouchEvent()方法的部分源码:
public boolean dispatchTouchEvent(MotionEvent event) { ...... boolean result = false; ...... //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;
}
......
return result;
} |
最主要的就是if的判断条件
li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event) |
如果我们设置了OnTouchListener监听,那么li != null && li.mOnTouchListener
!= null 就为true,此时判断条件可以简化为
true&& (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event) |
接下来会判断View是否是ENABLED的(在xml文件中和代码中都可以设置enable属性)。从代码中如果设置该属性为false那么onTouch(onClick也不会,可以自己看源码)方法将不会回调,也就是设置OnTouchListener(OnClickListener)监听将失效!
但是要注意,根据下一句代码onTouchEvent还是可以回调的。如果不设置该属性,默认是true的,那么判断条件又可以简化
true&&true&&li.mOnTouchListener.onTouch(this, event) |
此时就很清楚了,如果onTouch()返回false那么result就为false。
if (!result && onTouchEvent(event)) { result = true; } |
onTouchEvent()就会回调,并且还会导致dispatchTouchEvent方法的返回值和onTouchEvent()方法相同。相反如果onTouch()返回为true那么onTouchEvent()方法就没有办法执行了。所以onTouchEvent()能不能执行还得看onTouch()方法的返回值。
注意:View的这下回调方法的优先级非常重要,务必搞清楚,dispatchTouchEvent->onTouch->onTouchEvent->onClick。不仅如此,还得非常清楚每个方法返回值的结果对整个事件流的影响。
下图是自己画的View事件分发的一部分流程图,希望能帮忙理解:
0x03 ViewGroup事件分发 上文介绍了View的分发,由于View一般是在事件分发中的最后一层,所以相对而言要简单一些,但是ViewGroup就要复杂多了。我们一般用的比较多的ViewGroup就是RelativeLayout,LinearLayout等常用布局。因为这些布局很可能会包含其他View,所以除了dispatchTouchEvent(),onTouchEvent()之外还多了一个onInterceptTouchEvent()用来拦截事件。我们还是简单的自定义ViewGroupOne,ViewGroupTwo,ViewGroupThree继承自LinearLayout。
public class ViewGroupOne extends LinearLayout { private final String TAG = "ViewGroupOne";
public ViewGroupOne(Context context) {
super(context);
}
public ViewGroupOne(Context context, @Nullable
AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent
ev) {
Log.e(TAG, "dispatchTouchEvent:" + "action:"
+ ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent
ev) {
Log.e(TAG, "onInterceptTouchEvent:"
+ "action:" + ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.e(TAG, "onTouchEvent:" + "action:"
+ ev.getAction());
return super.onTouchEvent(ev);
}
} |
然后按照下图进行嵌套使用:
点击CustomButton可以得到以下信息:
ViewGroupOne: dispatchTouchEvent:action:0 ViewGroupOne: onInterceptTouchEvent:action:0 ViewGroupTwo: dispatchTouchEvent:action:0 ViewGroupTwo: onInterceptTouchEvent:action:0 ViewGroupThree: dispatchTouchEvent:action:0 ViewGroupThree: onInterceptTouchEvent:action:0 CustomButton: dispatchTouchEvent:action:0 CustomButton: onTouchEvent:action:0 ViewGroupOne: dispatchTouchEvent:action:1 ViewGroupOne: onInterceptTouchEvent:action:1 ViewGroupTwo: dispatchTouchEvent:action:1 ViewGroupTwo: onInterceptTouchEvent:action:1 ViewGroupThree: dispatchTouchEvent:action:1 ViewGroupThree: onInterceptTouchEvent:action:1 CustomButton: dispatchTouchEvent:action:1 CustomButton: onTouchEvent:action:1 |
默认情况下,CustomButton是消费事件的,这种情况下的分发还是很清晰的。
当事件传递到ViewGroupOne之后先回调dispatchTouchEvent,在该方法中会调用onInterceptTouchEvent方法来判断是否拦截手势,这里默认是不拦截的,那么会继续传递到ViewGroupTwo并调用其dispatchTouchEvent方法,接下来就和ViewGroupOne一样的流程,直到传递到View(CustomButton)并消费该事件(onTouchEvent返回true)。那么我们再来看看当View(CustomButton)不消费事件(onTouchEvent返回false)时是什么样:
ViewGroupOne: dispatchTouchEvent:action:0 ViewGroupOne: onInterceptTouchEvent:action:0 ViewGroupTwo: dispatchTouchEvent:action:0 ViewGroupTwo: onInterceptTouchEvent:action:0 ViewGroupThree: dispatchTouchEvent:action:0 ViewGroupThree: onInterceptTouchEvent:action:0 CustomButton: dispatchTouchEvent:action:0 CustomButton: onTouchEvent:action:0 ViewGroupThree: onTouchEvent:action:0 ViewGroupTwo: onTouchEvent:action:0 ViewGroupOne: onTouchEvent:action:0 |
这次的结果在事件分发的时流程大体一样,但是也有和上一次的有很大的不同的地方: 1.只有MotionEvent.ACTION_DOWN的事件 2.onTouchEvent由ViewGroupThree到ViewGroupOne依次执行。 对于第一点是因为我们将CustomView的onTouchEvent的返回值修改为false,也就是CustomView不消费此事件,而默认情况下ViewGroup也不消费(onTouchEvent返回false),所以就没有人消费事件了,那么后边的事件也就不再处理了。
对于对二点事件的消费和之前的分发流程至父到子传递过程不同,它是从子到父传递,也即对于自己不处理的事件,会传递给父布局让其处理,以此类推。直到传递到有人处理或者压根就没人处理就和上边的情况一样。
上边讲了半天都没有见到onInterceptTouchEvent()这个方法有什么用,现在我们选择将ViewGroupTwo的onInterceptTouchEvent()返回值从默认的false该为true。看看程序执行结果:
ViewGroupOne: dispatchTouchEvent:action:0 ViewGroupOne: onInterceptTouchEvent:action:0 ViewGroupTwo: dispatchTouchEvent:action:0 ViewGroupTwo: onInterceptTouchEvent:action:0 ViewGroupTwo: onTouchEvent:action:0 ViewGroupOne: onTouchEvent:action:0 |
结果很明显ViewGroupThree和CustomButton的方法都没有回调(其实收到了一个ACTION_CANCEL的事件),这就是intercept的意义所在,直接在ViewGroupTwo这一层将事件拦截了,然后直接交由ViewGroupTwo的onTouchEvent处理。
如果ViewGroupTwo的onTouchEvent消费事件(返回true)那么触摸产生的一系列事件都将传递到ViewGroupTwo的onTouchEvent中进行消费,并且不会再向上传递给ViewGroupOne的onTouchEvent:
ViewGroupOne: dispatchTouchEvent:action:0 ViewGroupOne: onInterceptTouchEvent:action:0 ViewGroupTwo: dispatchTouchEvent:action:0 ViewGroupTwo: onInterceptTouchEvent:action:0 ViewGroupTwo: onTouchEvent:action:0 ViewGroupOne: dispatchTouchEvent:action:1 ViewGroupOne: onInterceptTouchEvent:action:1 ViewGroupTwo: dispatchTouchEvent:action:1 ViewGroupTwo: onTouchEvent:action:1 |
以上结果印证上边的分析。如下图来表示该流程:
最后在强调一下OnTouchListener和OnClickListener对分发流程和事件消费的影响。如果阅读ViewGroup.dispatchTouchEvent方法源码不难得出优先级:
dispatchTouchEvent->onInterceptTouchEvent->onTouch->onTouchEvent->onClick
也就是说OnTouchListener不会对dispatch分发过程产生影响,而会影响事件的消费,因为在分析View时已经讲过了,如果设置了OnTouchListener监听,并且在onTouch方法中进行事件消费(返回true)的话,将不会再回调对应View的onTouchEvent方法。大家可以底下试验一下,这里篇幅原因不再给出了。
0x04 Activity的事件分发
本来不想讲Activity的事件分发的,因为要涉及到WMS比较复杂,不过想真正理解Android的事件的分发这一块还真的绕不过。因为上文中的事件流都是来源自Activity的。不过我还是不深入源码,如果对源码感兴趣可以参考Android
FrameWork——Touch事件派发过程详解。 我们都知道通常的Activity的UI框架,类似下图:
如果了解过AMS机制我们就知道,Activity实际并不会去控制UI视图,它主要是去控制生命周期和处理事件。真正的视图控制是Window。Window代表一个窗口,它持有一个DecorView,这个DecorView继承自FrameLayout,它是Activity的根布局。而他的内部通常又包含一个LinearLayout,这个LinearLayout有包括title和id为content的部分(当然这个也和Android系统版本和你设置的style有关系)。我们在Activity中调用setContentView()设置的布局就是被添加到content部分了。Android给我提供了方便获取content对应布局的方式,只需要调用ViewGroup
content=(ViewGroup)findViewById(R.android.id.content)即可。
在WindowManager(WindowManagerImpl)体系中,有一个ViewRoot(ViewRootImpl)它是连接WindowManagerService和DecorView的纽带
,View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。ViewRoot
实现了ViewParent接口,这让它可以作为View的名义上的父视图 。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。ViewRoot可以被理解为“View树的管理者”——它有一个mView成员变量,指向的就是DecorView。
简单点说当有触摸事件产生,会通过WMS传递到ViewRoot,ViewRoot再分发给对应的DecorView(FrameLayout它是一个ViewGroup),再按照上文ViewGroup的分发机制,向下依次传递,最终会传递到我们setContentView()设置布局的根布局,接下来就是我们上文已经熟悉的流程了。
掌握了这些,在开发中遇到的很多手势冲突的问题就知道产生冲突的原因和解决冲突的思路了。 |