1.简述
谈到自定义View,不可避免的要涉及View的各种事件,由于View并非独立存在其必要有父体和子体,而其触发响应事件,又不得不触及事件传递等。故而事件分发作为完善View触发事件以及解决各种冲突起着至关重要的作用。我这里由于有限水平,就不讲解原理了,举几个我遇到的小例子,概念我提供一些我觉得不错的博客,不对的地方多多指出,也希望能帮助一些开发者。
事件分发的小案例
目的:
通过重写包括相对布局,以及子ViewButton,打印其执行的Log,去观察其事件调用的顺序。窥探在Java代码中onTouchEvent()中返回true or false的区别。
mRV.setOnTouchListener((v, event) -> {
Log.e("试图传递","布局 onTouch");
return false;
});
mRV.setOnClickListener(v -> Log.e("试图传递","布局 OnClick"));
mBtn.setOnTouchListener((v, event) -> {
Log.e("试图传递","按钮 onTouch");
// 通过改变onTouchEvent的值,去进行修改
return false;
});
mBtn.setOnClickListener(v -> Log.e("试图传递","按钮 OnClick"));
MyDvRelativeLayout.xml
public class MyDvRelativeLayout extends RelativeLayout {
public MyDvRelativeLayout(Context context) {
super(context);
}
public MyDvRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyDvRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("事件分发","父ViewGroup dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e("事件分发","父ViewGroup onInterceptTouchEvent "+super.onInterceptTouchEvent(ev));
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("事件分发","父ViewGroup onTouchEvent");
return super.onTouchEvent(event);
}
}
MyButton.xml
public class MyButton extends android.support.v7.widget.AppCompatButton {
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.e("事件分发","子View dispatchTouchEvent");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("事件分发","子View onTouchEvent 返回: "+super.onTouchEvent(event) +" 触摸状态 "+event.getAction() );
return super.onTouchEvent(event);
}
}
当onTouchEvent 返回true或者默认效果,点击按钮运行结果:
如下图可以发现:布局并未响应点击事件,事件的流向打印如下:
07-19 14:01:54.354 15257-15257/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
07-19 14:01:54.354 15257-15257/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent false
07-19 14:01:54.354 15257-15257/com.example.mydairytestproject E/事件分发: 子View dispatchTouchEvent
07-19 14:01:54.356 15257-15257/com.example.mydairytestproject E/事件分发: 子View onTouchEvent 返回: true 触摸状态 0
07-19 14:01:54.426 15257-15257/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
07-19 14:01:54.426 15257-15257/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent false
07-19 14:01:54.426 15257-15257/com.example.mydairytestproject E/事件分发: 子View dispatchTouchEvent
07-19 14:01:54.427 15257-15257/com.example.mydairytestproject E/事件分发: 子View onTouchEvent 返回: true 触摸状态 1
按钮的响应如下,只触发了按钮的事件,正如上图所示,由于子view的onTouchEvent( )返回了true了,那么就意味着触摸事件已经被消费了,那么就不会返回给父控件再进行处理了
07-19 14:04:04.141 15257-15257/com.example.mydairytestproject E/试图传递: 按钮 onTouch
07-19 14:04:04.199 15257-15257/com.example.mydairytestproject E/试图传递: 按钮 onTouch
07-19 14:04:04.200 15257-15257/com.example.mydairytestproject E/试图传递: 按钮 OnClick
07-19 14:04:04.201 15257-15257/com.example.mydairytestproject E/试图传递: 按钮 OnClick
合起来如下:
07-19 14:06:49.395 15257-15257/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
07-19 14:06:49.395 15257-15257/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent false
07-19 14:06:49.395 15257-15257/com.example.mydairytestproject E/事件分发: 子View dispatchTouchEvent
07-19 14:06:49.395 15257-15257/com.example.mydairytestproject E/试图传递: 按钮 onTouch
07-19 14:06:49.397 15257-15257/com.example.mydairytestproject E/事件分发: 子View onTouchEvent 返回: true 触摸状态 0
07-19 14:06:49.434 15257-15257/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
07-19 14:06:49.434 15257-15257/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent false
07-19 14:06:49.434 15257-15257/com.example.mydairytestproject E/事件分发: 子View dispatchTouchEvent
07-19 14:06:49.434 15257-15257/com.example.mydairytestproject E/试图传递: 按钮 onTouch
07-19 14:06:49.434 15257-15257/com.example.mydairytestproject E/事件分发: 子View onTouchEvent 返回: true 触摸状态 2
07-19 14:06:49.483 15257-15257/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
07-19 14:06:49.483 15257-15257/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent false
07-19 14:06:49.483 15257-15257/com.example.mydairytestproject E/事件分发: 子View dispatchTouchEvent
07-19 14:06:49.483 15257-15257/com.example.mydairytestproject E/试图传递: 按钮 onTouch
07-19 14:06:49.484 15257-15257/com.example.mydairytestproject E/事件分发: 子View onTouchEvent 返回: true 触摸状态 1
07-19 14:06:49.484 15257-15257/com.example.mydairytestproject E/试图传递: 按钮 OnClick
07-19 14:06:49.487 15257-15257/com.example.mydairytestproject E/试图传递: 按钮 OnClick
当MyButton的 onTouchEvent()如下返回false时,打印日志如下
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e("事件分发","子View onTouchEvent 返回: "+super.onTouchEvent(event) +" 触摸状态 "+event.getAction() );
return false;
}
07-19 14:12:26.425 18319-18319/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
07-19 14:12:26.425 18319-18319/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent false
07-19 14:12:26.425 18319-18319/com.example.mydairytestproject E/事件分发: 子View dispatchTouchEvent
07-19 14:12:26.426 18319-18319/com.example.mydairytestproject E/试图传递: 按钮 onTouch
07-19 14:12:26.427 18319-18319/com.example.mydairytestproject E/事件分发: 子View onTouchEvent 返回: true 触摸状态 0
07-19 14:12:26.427 18319-18319/com.example.mydairytestproject E/试图传递: 布局 onTouch
07-19 14:12:26.427 18319-18319/com.example.mydairytestproject E/事件分发: 父ViewGroup onTouchEvent
07-19 14:12:26.496 18319-18319/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
07-19 14:12:26.496 18319-18319/com.example.mydairytestproject E/试图传递: 布局 onTouch
07-19 14:12:26.496 18319-18319/com.example.mydairytestproject E/事件分发: 父ViewGroup onTouchEvent
07-19 14:12:26.497 18319-18319/com.example.mydairytestproject E/试图传递: 布局 OnClick
onTouchListener 返回true
此时不会执行点击事件 也不会执行onTouchevent
2019-11-07 14:29:23.744 16487-16487/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
2019-11-07 14:29:23.744 16487-16487/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent false
2019-11-07 14:29:23.744 16487-16487/com.example.mydairytestproject E/事件分发: 子View dispatchTouchEvent
2019-11-07 14:29:23.745 16487-16487/com.example.mydairytestproject E/事件分发: onTouch: 0
2019-11-07 14:29:23.937 16487-16487/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
2019-11-07 14:29:23.937 16487-16487/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent false
2019-11-07 14:29:23.937 16487-16487/com.example.mydairytestproject E/事件分发: 子View dispatchTouchEvent
2019-11-07 14:29:23.937 16487-16487/com.example.mydairytestproject E/事件分发: onTouch: 1
下面看看默认的情况吧
2019-11-07 14:37:37.618 17188-17188/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
2019-11-07 14:37:37.618 17188-17188/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent false
2019-11-07 14:37:37.618 17188-17188/com.example.mydairytestproject E/事件分发: 子View dispatchTouchEvent
2019-11-07 14:37:37.618 17188-17188/com.example.mydairytestproject E/事件分发: onTouch: 0
2019-11-07 14:37:37.621 17188-17188/com.example.mydairytestproject E/事件分发: 子View onTouchEvent 默认返回: true 触摸状态 0
2019-11-07 14:37:38.597 17188-17188/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
2019-11-07 14:37:38.597 17188-17188/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent false
2019-11-07 14:37:38.597 17188-17188/com.example.mydairytestproject E/事件分发: 子View dispatchTouchEvent
2019-11-07 14:37:38.597 17188-17188/com.example.mydairytestproject E/事件分发: onTouch: 1
2019-11-07 14:37:38.597 17188-17188/com.example.mydairytestproject E/事件分发: 子View onTouchEvent 默认返回: true 触摸状态 1
2019-11-07 14:37:38.598 17188-17188/com.example.mydairytestproject E/事件分发: OnClickListener:
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//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;
}
}
onTouchEvent()小结
点击事件是在手指抬起时触发的,只有OnTouchEvent()默认一般情况下是返回true的,意味着事件被该控件(布局)给消费了,如果返回 false 那么意味着无法消费,那么就不得不交给父布局处理。
接下啦来把父视图的 onInterceptTouchEvent( )返回 true,即拦截事件看看,Button啥都没调用
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
07-19 14:25:00.493 21372-21372/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
07-19 14:25:00.493 21372-21372/com.example.mydairytestproject E/事件分发: 父ViewGroup onInterceptTouchEvent true
07-19 14:25:00.493 21372-21372/com.example.mydairytestproject E/试图传递: 布局 onTouch
07-19 14:25:00.493 21372-21372/com.example.mydairytestproject E/事件分发: 父ViewGroup onTouchEvent
07-19 14:25:00.563 21372-21372/com.example.mydairytestproject E/事件分发: 父ViewGroup dispatchTouchEvent
07-19 14:25:00.564 21372-21372/com.example.mydairytestproject E/试图传递: 布局 onTouch
07-19 14:25:00.564 21372-21372/com.example.mydairytestproject E/事件分发: 父ViewGroup onTouchEvent
07-19 14:25:00.565 21372-21372/com.example.mydairytestproject E/试图传递: 布局 OnClick
在下面的博文1中,get到了多点触控的知识,像每增加一个触控点就会增一个pointer来描述这个触控点,
值得学习的概念基础:
这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。
如上,我们对view的 dispatchTouchEvent onTouchEvent 以及 viewGroup的 dispatchTouchEvent onInterceptTouchEvent onTouchEvent 都是有所了解的。那么针对具体的点击事件,为什么是该过程呢,那么我们就应该去看源码了。
首先默认 第一个方法调用的是 dispatchTouchEvent , 为什么先调这个后续可以去研究。
view$dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent event) {
// ......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
/**
如果设立点击事件,且是enable,而且调用 onTouchListener 其返回值为 true,
那么派发结束,这时候并不会执行 view 的 onTouchEvent
**/
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;
}
view$onTouchEvent
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// 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;
view$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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
viewGroup$dispaTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//
// 此处调用了 viewGroup自己的 onInterceptTouchEvent
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}
........
if (!canceled && !intercepted) {
// ......
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 = ev.getX(actionIndex);
final float y = 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 (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();
}
// .....
}
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 。。。。。。
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;
}