View的事件分发机制中,有三个很重要的方法:dispatchTouchEvent(MotionEvent event)
、onInterceptTouchEvent(MotionEvent ev)
和onTouchEvent(MotionEvent event)
,而作用的对象主要是Activity
、ViewGroup
和View
首先,我们从日志查看一个点击事件的到底执行了哪些方法,执行的方法顺序是怎样的?
代码中我使用了Kotlin编写,ViewBinding框架,具体代码部分如下:
FunEx.kt中定义了扩展方法
fun MotionEvent.toEventAction(): String {
val action: Int = this.action
val actionMasked = action and MotionEvent.ACTION_MASK
return when (actionMasked) {
MotionEvent.ACTION_DOWN -> {
"DOWN-$actionMasked"
}
MotionEvent.ACTION_MOVE -> {
"MOVE-$actionMasked"
}
MotionEvent.ACTION_UP -> {
"UP-$actionMasked"
}
MotionEvent.ACTION_CANCEL -> {
"CANCEL-$actionMasked"
}
else -> {
"UNKONW-$actionMasked"
}
}
}
EventActivity.kt
const val EVENT_TEST = "event_test"
class EventActivity : AppCompatActivity() {
private val mBinding by lazy {
ActivityEventBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
mBinding.tvClickEvent.setOnClickListener {
Toast.makeText(this,"触发了点击事件",Toast.LENGTH_LONG).show()
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.i(EVENT_TEST, "Activity onTouchEvent start ${event?.toEventAction()}")
val result = super.onTouchEvent(event)
Log.i(EVENT_TEST, "Activity onTouchEvent end ${event?.toEventAction()} : $result")
return result
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.i(EVENT_TEST, "Activity dispatchTouchEvent start ${ev?.toEventAction()}")
val result = super.dispatchTouchEvent(ev)
Log.i(EVENT_TEST, "Activity dispatchTouchEvent end ${ev?.toEventAction()} : $result")
return result
}
}
EventConstraintLayout.kt
class EventConstraintLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.i(EVENT_TEST, "ViewGroup onTouchEvent start ${event?.toEventAction()}")
val result = super.onTouchEvent(event)
Log.i(EVENT_TEST, "ViewGroup onTouchEvent end ${event?.toEventAction()} : $result")
return result
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.i(EVENT_TEST, "ViewGroup dispatchTouchEvent start ${ev?.toEventAction()}")
val result = super.dispatchTouchEvent(ev)
Log.i(EVENT_TEST, "ViewGroup dispatchTouchEvent end ${ev?.toEventAction()} : $result")
return result
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
Log.i(EVENT_TEST, "ViewGroup onInterceptTouchEvent start ${ev?.toEventAction()}")
val result = super.onInterceptTouchEvent(ev)
Log.i(EVENT_TEST, "ViewGroup onInterceptTouchEvent end ${ev?.toEventAction()} : $result")
return result
}
}
EventTextView.kt
class EventTextView @JvmOverloads constructor
(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) :
AppCompatTextView(context, attrs, defStyleAttr) {
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.i(EVENT_TEST, "View onTouchEvent start ${event?.toEventAction()}")
val result = super.onTouchEvent(event)
Log.i(EVENT_TEST, "View onTouchEvent end ${event?.toEventAction()} : $result")
return result
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.i(EVENT_TEST, "View dispatchTouchEvent start ${ev?.toEventAction()}")
val result = super.dispatchTouchEvent(ev)
Log.i(EVENT_TEST, "View dispatchTouchEvent end ${ev?.toEventAction()} : $result")
return result
}
}
activity_event.xml
<?xml version="1.0" encoding="utf-8"?>
<com.example.mydatastructdemo.event.EventConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/teal_700"
tools:context=".event.EventActivity">
<com.example.mydatastructdemo.event.weight.EventTextView
android:id="@+id/tv_click_event"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/purple_500"
android:gravity="center"
android:text="Click Event"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.example.mydatastructdemo.event.EventConstraintLayout>
上边的代码很简单,主要在Activity
、ViewGroup
和View
对应事件方法中添加了日志。解下来点击EventTextView
,日志如下:
19:53:02.237 I Activity dispatchTouchEvent start DOWN-0
19:53:02.239 I ViewGroup dispatchTouchEvent start DOWN-0
19:53:02.239 I ViewGroup onInterceptTouchEvent start DOWN-0
19:53:02.239 I ViewGroup onInterceptTouchEvent end DOWN-0 : false
19:53:02.239 I View dispatchTouchEvent start DOWN-0
19:53:02.239 I View onTouchEvent start DOWN-0
19:53:02.240 I View onTouchEvent end DOWN-0 : true
19:53:02.241 I View dispatchTouchEvent end DOWN-0 : true
19:53:02.241 I ViewGroup dispatchTouchEvent end DOWN-0 : true
19:53:02.241 I Activity dispatchTouchEvent end DOWN-0 : true
19:53:02.253 I Activity dispatchTouchEvent start MOVE-2
19:53:02.254 I ViewGroup dispatchTouchEvent start MOVE-2
19:53:02.254 I ViewGroup onInterceptTouchEvent start MOVE-2
19:53:02.254 I ViewGroup onInterceptTouchEvent end MOVE-2 : false
19:53:02.254 I View dispatchTouchEvent start MOVE-2
19:53:02.254 I View onTouchEvent start MOVE-2
19:53:02.254 I View onTouchEvent end MOVE-2 : true
19:53:02.254 I View dispatchTouchEvent end MOVE-2 : true
19:53:02.254 I ViewGroup dispatchTouchEvent end MOVE-2 : true
19:53:02.254 I Activity dispatchTouchEvent end MOVE-2 : true
19:53:02.308 I Activity dispatchTouchEvent start UP-1
19:53:02.308 I ViewGroup dispatchTouchEvent start UP-1
19:53:02.309 I ViewGroup onInterceptTouchEvent start UP-1
19:53:02.309 I ViewGroup onInterceptTouchEvent end UP-1 : false
19:53:02.309 I View dispatchTouchEvent start UP-1
19:53:02.309 I View onTouchEvent start UP-1
19:53:02.309 I View onTouchEvent end UP-1 : true
19:53:02.309 I View dispatchTouchEvent end UP-1 : true
19:53:02.309 I ViewGroup dispatchTouchEvent end UP-1 : true
19:53:02.310 I Activity dispatchTouchEvent end UP-1 : true
因为DOWN、MOVE、UP
事件流程在日志中反应出一致,这里只分析DOWN
事件。
通过日志看出,当点击EventTextView
的时候,首先触发了Activity
中的dispatchTouchEvent()
,接着是ViewGroupdispatchTouchEvent()
->ViewGroup.onInterceptTouchEvent()
并返回false
,紧接着调用了View.dispatchTouchEvent()
->View.onTouchEvent()
并返回了true
,接着ViewGroup.dispatchTouchEvent ()、Activity.dispatchTouchEvent()
先后执行完成。
因此从Activity.dispatchTouchEvent()
开始分析,源码如下
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity.dispatchTouchEvent()
开始交给Window.superDispatchTouchEvent()
进行分发,返回true
,整个事件结束;返回false
,那么Activity.onTouchEvent()
会被调用。由于Window
的唯一实现类是PhoneWindow
,直接上PhoneWindow.superDispatchTouchEvent()
源码如下:
private DecorView mDecor;
//.....省略代码
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
DecorView.superDispatchTouchEvent()
如下:
class DecorView extends FrameLayout{
//......
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
//......
}
由于FrameLayout
中没有重写ViewGroup.dispatchTouchEvent()
那么Activity.dispatchTouchEvent()
执行第一步就是实际是调用了ViewGroup.dispatchTouchEvent()
,再来看看ViewGroup
的源码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
//---------------------代码片段 1 start------------------------------
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//---------------------代码片段 1 end------------------------------
//------------------------代码片段 2 start----------------------------------
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
//-----------------------------代码片段 2 end-----------------------------
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount;
}
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
//...
return handled;
}
一、首先看第1段代码
DOWN
事件的时候会就是重置。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
目标View
即mFirstTouchTarget!=null
,如果存在则进行统一清除操作。如果event为空,则将动作设为ACTION_CANCEL
,接着用一个for
循环不断向下传递触摸事件,然后再清除所有触摸目标,最后在回收拷贝的对象。
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
clearTouchTargets()
是将mFirstTouchTarget
恢复到原始状态,即mFirstTouchTarget =null
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
resetTouchState()
方法主要将mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT,即与
requestDisallowInterceptTouchEvent(false)等效,同时调用
resetCancelNextUpFlag(this)```将当前ViewGroup的取消状态重置。
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
requestDisallowInterceptTouchEvent()
用来控制是否可以执行ViewGroup.onInterceptTouchEvent()
,从而控制dispatchTouchEvent()
中的intercepted
的赋值,disallowIntercept=false
的时候表示会调用ViewGroup.onInterceptTouchEvent()
,从而确定intercepted
的值。
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
resetCancelNextUpFlag()
方法来检查当前事件是否被取消
private static boolean resetCancelNextUpFlag(@NonNull View view) {
if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
return true;
}
return false;
}
二、代码片段2
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
//ViewGroup子View不处理,此时mFirstTouchTarget=null,ViewGroup直接拦截
intercepted = true;
}
只有DOWN
事件和 mFirstTouchTarget != null(即事件由当前ViewGroup
的子View
处理)时执行会根据disallowIntercept
的值决定是否调用ViewGroup.onInterceptTouchEvent()
。默认情况下disallowIntercept=false
,即requestDisallowInterceptTouchEvent(false)
,反之,requestDisallowInterceptTouchEvent(true)
被调用时,intercepted=false
未完待续 。。。。