自定义View流程
1. 构造函数
View初始化
2. onMeasure
测量View大小
3. onLayout
确定子View布局(自定义ViewGroup时才有用)
4. onDraw
实际绘制内容(一般自定义ViewGroup不用重写该方法)
5. 提供接口
改变视图状态(在改变属性值的同时要调用下面几个方法,才会真正改变视图的状态)
invalidate(),会回调onDraw方法
postInvalidate(),会回调onDraw方法
区别,前者在主线程中调用,后者可以在子线程中调用
requestLayout(),会回调onMeasure-》onLayout-》onDraw
注意,requestLayout有可能会导致onDraw被调用,也可能不导致onDraw被调用,取决于view的l,t,r,b是否改变,改变则调用
当View大小发生变化时,用requestLayout,如果仅仅只改变View的状态,用invalidate即可。
事件分发机制
1. 分发机制的概念
当一个点击事件产生后,系统需要将这个事件传递给一个具体的View去处理,这个事件的传递过程就是分发机制。
2. 事件分发的顺序
事件产生后,最先传递给Activity,然后依次向下传递
Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View
不考虑PhoneWindow和DecorView的传递过程
Activity -> ViewGroup -> View
如果在这个过程中,没有任何View消费事件,那么这个事件会按照反方向回传,最终会回传给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃
Activity <- ViewGroup <- View
3. 事件分发涉及到的重要方法
dispatchTouchEvent
作用:分发点击事件
调用时刻:当点击事件能够传递到当前View时,该方法就被调用
onInterceptTouchEvent
作用:判断是否拦截了某个事件(只有ViewGroup有这个方法,Activity和View均没有该方法)
调用时刻:在dispatchTouchEvent方法内部调用
onTouchEvent
作用:处理点击事件
调用时刻:在dispatchTouchEvent方法内部调用
4. 源码分析分发机制的事件传递过程
要想充分理解Android分发机制,本质上是要理解
- Activity对点击事件的分发机制
- ViewGroup对点击事件的分发机制
- View对点击事件的分发机制
4.1 Activity的事件分发机制
public boolean dispatchTouchEvent(MotionEvent ev) {
//关注点1
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//关注点2
onUserInteraction();
}
//关注点3
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//关注点4
return onTouchEvent(ev);
}
关注点1
一般事件开始都是down,因此这里基本返回为true
关注点2
进入该方法,发现是一个空方法,暂时不深究了
关注点3
Window是抽象类,PhoneWindow是Window类的唯一实现类
superDispatchTouchEvent是一个抽象方法
-
通过PhoneWindow类中看一下superDispatchTouchEvent()的作用
public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); //mDecor 是DecorView的实例 }
-
接下来我们看mDecor.superDispatchTouchEvent(event)
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); //DecorView继承自FrameLayout,那么它的父类就是ViewGroup // super.dispatchTouchEvent调用的就是ViewGroup的dispatchTouchEvent方法 }
所以
- 执行getWindow().superDispatchTouchEvent(ev)实际上就是执行了ViewGroup.dispatchTouchEvent(ev)
- 这样事件就从Activity传递到了ViewGroup
关注点4
只有getWindow().superDispatchTouchEvent(ev)返回false的时候,才会执行,那什么时候返回false了,就是当所有View都没有消费该事件的时候,这个时候事件就回传给了Activity,Activity就回调自己的onTouchEvent。
4.2 ViewGroup的事件分发机制
ViewGroup的dispatchTouchEvent代码比较多,因此我只列出伪代码
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默认状态为没有消费过
//关注点1
if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View
//关注点2
result = child.dispatchTouchEvent(ev);
}
if (!result) { // 如果事件没有被消费,询问自身onTouchEvent
//关注点3
result = onTouchEvent(ev);
}
return result;
}
关注点1
ViewGroup在分发事件时,都会先调用onInterceptTouchEvent方法
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
//直接返回false,即默认ViewGroup是不拦截事件的,如果想拦截,需要重写该方法
}
关注点2
- 由于是伪代码,其实这里省略了很多
- 实际上会通过点击事件的坐标,找到此次点击事件在哪个child范围内
- 找到之后,就会调用该child的dispatchTouchEvent方法
- 这样事件就从ViewGroup传递到了View
关注点3
只有child.dispatchTouchEvent返回false时,才会调用。这种情况实际上就是View没有消费此次事件,这样ViewGroup就会调用自身的onTouchEvent方法
4.3 View事件的分发机制
为了看的更清晰一点,我们截取关键代码
public boolean dispatchTouchEvent(MotionEvent event) {
....
....
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//关注点1
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//关注点2
if (!result && onTouchEvent(event)) {
result = true;
}
}
....
....
return result;
}
关注点1
-
从代码中可以看出,只有以下4个条件都为真,dispatchTouchEvent才返回true,否则调用onTouchEvent方法
第一个条件:li != null
第二个条件:li.mOnTouchListener != null
第三个条件:(mViewFlags & ENABLED_MASK) == ENABLED
第四个条件:li.mOnTouchListener.onTouch(this, event) -
下面,我们来看看这四个条件
第一个条件:li != null
第二个条件:li.mOnTouchListener != nullListenerInfo getListenerInfo() { if (mListenerInfo != null) { return mListenerInfo; } mListenerInfo = new ListenerInfo(); return mListenerInfo; } public void setOnTouchListener(OnTouchListener l) { getListenerInfo().mOnTouchListener = l; //即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空),同时满足了条件1和条件2 }
第三个条件:(mViewFlags & ENABLED_MASK) == ENABLED
- 该条件是判断当前点击的控件是否enable
- 由于很多View默认是enable的,因此该条件恒定为true
第四个条件:li.mOnTouchListener.onTouch(this, event)
-
控件注册Touch事件时的回调方法onTouch
//手动调用设置 button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return false; } });
如果onTouch返回true,则四个条件都满足
如果onTouch返回false,则调用自身的onTouchEvent方法
关注点2
当上述四个条件有一个不满足时,即View未设置onTouch事件,或者View是不可enable(点击)的,或者onTouch返回false时,就会执行
为了更清晰我们只列出关键代码
public boolean onTouchEvent(MotionEvent event) {
...
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_DOWN:
...
...
mHasPerformedLongPress = false;
//关注点1
checkForLongClick(0);
break;
}
case MotionEvent.ACTION_UP:
...
...
//关注点2
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
//关注点3
performClick();
}
//关注点4
return true;
}
return false;
}
关注点1(onTouchEvent)
- 我们来看一下checkForLongClick方法内部
private void checkForLongClick(int delayOffset) {
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
//初始值为false
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
//在查看一下CheckForLongPress构造方法的内部
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}
-
查看CheckForLongPress构造方法
private final class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; @Override public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { //查看该方法内部 if (performLongClick()) { //如果长按事件返回true,则此值为true mHasPerformedLongPress = true; } } } public void rememberWindowAttachCount() { mOriginalWindowAttachCount = mWindowAttachCount; } }
-
查看performLongClick方法
public boolean performLongClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLongClickListener != null) { //只要设置了onLongClick监听,就会执行 handled = li.mOnLongClickListener.onLongClick(View.this); } if (!handled) { handled = showContextMenu(); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }
关注点2(onTouchEvent)
- 不考虑mIgnoreNextUpEvent,假定它总为false
- mHasPerformedLongPress的值就是onLongClick的返回值
所以
- 当onLongClick返回false时,click事件会继续触发,
返回true时,click事件就不会触发了
关注点3(onTouchEvent)
-
来看看performClick方法
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; }
- 只要View设置了onClick方法,就返回true
关注点4(onTouchEvent)
只要 View 是可点击,就会返回true
5 总结
- 事件分发原理: 责任链模式,事件层层传递,直到被消费。
- 事件是否消费由返回值决定,true表示消费,false表示不消费,与是否使用了事件无关
- 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件
- 一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
- 只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容(即onTouchEvent如果返回false,则不再触发ACTION_MOVE和ACTION_UP)
- ViewGroup 的 dispatchTouchEvent 主要用于将事件分发给具体的View
- View 的dispatchTouchEvent 主要用于自身的监听器和onTouchEvent
事件分发的顺序总结
-
事件的传递顺序
Activity->ViewGroup->View
-
方法的调用顺序
dispatchTouchEvent->onInterceptTouchEvent->onTouch-> onTouchEvent->onLongClick->onClick