本节目标,通过案例,先看程序运行结果,然后跟踪源码,理解为什么会有这样的输出,继而理解view group的分发机制,感觉和证明题很像呢。
考虑以下程序的运行结果:
case1:
public class MyView extends View {
private static final String TAG = "MyView";
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "onClick: ");
}
});
this.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "onTouch: " + event.getAction());
return false;
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent: " + event.getAction());
return super.onTouchEvent(event);
}
}
public class MyViewGroup extends LinearLayout {
private static final String TAG = "MyViewGroup";
public MyViewGroup(Context context) {
super(context);
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "dispatchTouchEvent:" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent:" + event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent:" + ev.getAction());
return super.onInterceptTouchEvent(ev);
}
}
<?xml version="1.0" encoding="utf-8"?>
<com.example.chj.eventdispatch.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
tools:context=".MainActivity">
<com.example.chj.eventdispatch.MyView
android:id="@+id/myView"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/colorAccent"/>
</com.example.chj.eventdispatch.MyViewGroup>
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// findViewById(R.id.myView).setClickable(false);
// findViewById(R.id.myView).setEnabled(false);
}
}
case2:
在case1的基础上将MyView中的setOnClickListener的如下部分注释:
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "onClick: ");
}
});
case3:
在case1的基础上将MyView中的onTouchEven返回true:
case4:
在case1的基础上将MyViewGroup中的onInterceptTouchEvent返回true
运行结果
case1:
case2:
case3:
case4:
case1分析
源码分析:
case1的log步骤
1 ViewGroup:dispatchTouchEvent
2 ViewGroup:onInterceptTouchEvent
3 View:onTouch
4 View:onTouchEvent
5 View:onClick
事件分发基本是从 ViewRootIml->DecorView->PhoneWindow->id为content的Android原生控件->MyViewGroup->MyView一级级传递下来的,这一点打个断点很容易看出来 因为MyViewGroup dispatchTouchEvent在上面4个case都是起点,今天我们就简单从 MyViewGroup dispatchTouchEvent事件分发开始看起
那么首先调用了super.dispatchTouchEvent 即ViewGroup的dispatchTouchEvent
简化版代码流程(删除部分代码)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//是否消费
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
//如注释所说 清空之前的手势
cancelAndClearTouchTargets(ev);//mFirstTouchTarget置空
resetTouchState();
}
// Check for interception.
final boolean intercepted;
//第一次点击为MotionEvent.ACTION_DOWN可以进入
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//可以向父类请求不要打断
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//如果没有请求父类 那么打不打断取决于自身的onInterceptTouchEvent
intercepted = onInterceptTouchEvent(ev);//很明显case1会走这里 重要!!!!
//走到MyViewGroup的onInterceptTouchEvent 且返回值默认是false
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;//请求了父类不打断 那么intercepted=false
}
}
//非人为cancel 基本代码无法控制
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
if (!canceled && !intercepted) {//if条件成立
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {//MotionEvent.ACTION_DOWN条件成立
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
//newTouchTarget在clearTouchTargets清空
//childrenCount应当为1
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {//倒序遍历
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//走到这里 跟踪对应方法 非常重要的方法 下面还会调用
//此时 child不是null
//子view是否消费
// Child wants to receive touch within its bounds.
newTouchTarget = addTouchTarget(child, idBitsToAssign);//关键 这一步走到了 mFirstTouchTarget赋值后仍然为null
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();
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {//上面dispatchTransformedTouchEvent的返回值决定了这里如何走向
//如果dispatchTransformedTouchEvent返回true 那么第三个参数不是null
//否则 那么第三个参数是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;
}
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
//非常重要
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
MyViewGroup.dispatchTouchEvent–>ViewGroup.dispatchTouchEvent–>onInterceptTouchEvent(因为MyViewGroup覆盖了该方法 因此走了MyViewGroup.onInterceptTouchEvent)–>ViewGroup.onInterceptTouchEvent返回默认值false–>ViewGroup.dispatchTransformedTouchEvent–>child.dispatchTouchEvent (等待一个返回值)即MyView.dispatchTouchEvent(因为没有复写dispatchTouchEvent因此走到View.dispatchTouchEvent)–>走到之前博客的内容了 以下内容copy自博客“红橙Darren视频笔记 View事件分发源码分析 基于API29” dispatchTouchEvent->(MyView.OnTouchListener onTouch )OnTouchListener onTouch ->(MyView.OnTouchListener onTouchEvent )onTouchEvent -->child.dispatchTouchEvent的返回值计算完毕 其结果取决于onTouchEvent的返回值Case1时返回true–>dispatchTransformedTouchEvent因为dispatchTouchEvent的返回值计算结果为true–>newTouchTarget = addTouchTarget(child, idBitsToAssign);newTouchTarget 被赋值为一个非空的值 一轮循环结束 下次循环类似 加粗的部分就是log打印的部分
注:
Case1时返回true原因是在下面的第七行 clickable被赋值为true 使clickable这个值为true可以调用setClickable(true);或者给这个view 注册一个点击监听setOnClickListener
View
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
case MotionEvent.ACTION_MOVE:
...
}
return true;
}
return false;
}
case2分析
将setOnClickListener部分注释
注释之后一开始的流程一样直到MyView.onTouchEvent–>View.onTouchEvent 因为我们把setOnClickListener注释掉了 因此上面的第七行clickable被复制为false onTouchEvent返回值为false–>View.dispatchTouchEvent返回值为false–>ViewGroup.dispatchTransformedTouchEvent中的handled赋值false 同时ViewGroup.dispatchTransformedTouchEvent返回值false(简化版代码流程48行)–>dispatchTouchEvent中mFirstTouchTarget没有赋值–>ViewGroup.dispatchTransformedTouchEvent(简化版代码流程71行)–>child==null handled = super.dispatchTouchEvent(event);即调用了View.dispatchTouchEvent–>View.onTouchEvent因为MyViewGroup覆盖了onTouchEvent方法 最后调用了MyViewGroup.onTouchEvent
case3分析
大致流程和case1类似 只不过少了最后一个click事件 为什么不触发click事件事件呢 因为我们将
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent: " + event.getAction());
return super.onTouchEvent(event);
}
return super.onTouchEvent(event);部分修改为直接返回true 自然应该考虑这里有问题了
我们跟踪View的对应方法 我们观察到
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {//重点
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
}
return true;
}
return false;
}
我们观察到performClick写在onTouchEvent的抬起事件中 因此 如果我们直接return true自然走不到父类的方法 因此点击事件被忽略
case4分析
因为将MyViewGroup中的onInterceptTouchEvent返回true 因此intercepted自然是true
因此无法进入if (!canceled && !intercepted) {
因此mFirstTouchTarget 跳过赋值 mFirstTouchTarget == null成立 dispatchTransformedTouchEvent调用时第三个参数为null 接下来就是和case2相同的流程了
总结 事件分发的几个常见问题
我们需要注意几个问题
1.View.onTouchEvent的ACTION_UP中有着点击事件 如果View.onTouchEvent的ACTION_UP时没有调用父类 则click事件不会调用
2.onInterceptTouchEvent返回true可以打断所有子view的事件
3.有时会遇到case2类似问题 只能收到down事件 move up事件收不到 要规避此问题有两个方案一个是调用View的setClickable 一个是加clicklistener 一个是在View.onTouchEvent的down事件返回true