死亡是一座永恒的灯塔,不管你驶向何方,最终都会朝它转向。一切都将逝去,只有死神永生。 —— 《三体》
浅谈Android事件分发机制
这是本人第一次写博文,主要是想记录自己的学习过程(毕竟菜鸟一个还需非常努力),仅是个人观点,如果有问题错误,请各位大佬指出,立刻改正绝不会一错再错
前因:
当重写Button的ontouch()事件时返回true不执行onclick返回false则执行,原理是什么?为什么要这么做?这些小疑惑阻挡了我一点点进步,于是查资料学习了Android的事件分发机制,写下了自己的理解。
正文:
进入正题:
1.什么是点击事件。
2.什么是事件分发。
3.事件如何进行传递和分发顺序。
4.事件分发的方法。
5.源码分析。
6.实例分析
依此解决上面的小疑惑吧。
1.什么是点击事件:
简单的来说就是当我们点击屏幕的时候,就会产生点击事件也就是Touch,事件的类型有四种:
MotionEvent.ACTION_DOWN | 手指触摸按下 |
---|---|
MotionEvent.ACTION_UP | 手指抬起 |
MotionEvent.ACTION_MOVE | 手指移动 |
MotionEvent.ACTION_CANCEL | 非人为原因的结束事件 |
很好理解吧。
举个生活中的例子:
按手印 就是一系列事件组成。这里纸就相当于View,
手指按下(MotionEvent.ACTION_DOWN)——手指抬起( MotionEvent.ACTION_UP )一个手印出现了。
用手指写血书 也是一系列事件组成。这里书就相当于View,手指按下(MotionEvent.ACTION_DOWN)——手指移动(MotionEvent.ACTION_MOVE)——手指抬起( MotionEvent.ACTION_UP ) 一个大气的字出现了。
由这个很生活的例子可以发现,一系列的事件都必须是以(DOWN)事件开始,(UP)事件结束,中间可以有(MOVE)事件也可以没有
产生事件了以后通过参数 MotionEvent传递给需要的VIEW进行处理。
2.什么是事件的分发
通俗的说上面举的例子中 手印的出现,血字的出现过程就是点击事件传递的过程。
3.事件如何进行传递和传递顺序
这里我们需要了解,Android的ui界面是由**Activity**,**View**,**ViewGroup**所组成 ViewGroup是View的子类,但是ViewGroup能包含View,举个例子,LinearLayout是ViewGroup,而Button是View。
而事件的分发顺序是Activity——ViewGroup——View
4.事件分发的方法
这里有dispatchTouchEvent() (事件分发)、onInterceptTouchEvent()(事件拦截)和onTouchEvent()(事件处理)三个方法
这里需要注意(onInterceptTouchEvent()是VIewGroup独有的)
也就是说当产生点击事件,dispatchTouchEvent() 就会调用 如果是ViewGroup 就会调用onInterceptTouchEvent() 判断是否拦截事件 然后在dispatchTouchEvent() 内部onTouchEvent()会被调用。
5.源码分析
接下来我们来进行源码分析吧,首先附上View中dispatchTouchEvent(MotionEvent event)源码 进行重要部分分析
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatemen
/******************************请注意这***************************************/
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;
}
/******************************请注意这***************************************/
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
上面这一大堆就是View的dispatchTouchEvent(MotionEvent event)源码(真的好多好多)请看我标出来的那部分源码
/******************************请注意这***************************************/
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;
}
/******************************请注意这***************************************/
咱们主要分析这其决定作用的部分,为了好理解,我将用大白话进行说明:首先我们看条件判断` if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event))`
也就是说当着4个参数都为真的时候,执行下面的 result = true;
语句,而这四个参数中有一个不为真则都返回false,如果返回false则满足` if (!result && onTouchEvent(event)) {
result = true;
}`就会执行onTouchEvent(event)方法。而 result 和dispatchTouchEvent(MotionEvent event)返回值相同。
继续对条件判断进行分析首先第一个参数li != null
li肯定不为null,为真再来看第二个参数li.mOnTouchListener != null
这也就是说当我们调用了setOnTouchListener的方法它会给mOnTouchListener 赋值,所以既然我们用了这个方法第二个参数也为真
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;/*第二个参数*/
}
我们再来看第三个参数 (mViewFlags & ENABLED_MASK) == ENABLED
这句代码是通过位运算来判断控件的属性(激活还是未激活)当我们创建一个控件的时候默认会激活(ENABLED)所以第三个参数也为真
现在就差第四个参数(也是最后一个关键的参数)li.mOnTouchListener.onTouch(this, event)
在上面咱们说到,如果四个参数都为真,则执行 result = true; 如果有一个不为真则执行onTouchEvent(event)
,上面咱们判断了其余三个参数都为真,所以第四个参数就是决定性的参数,也就是说li.mOnTouchListener.onTouch(this, event)
为真执行 result = true;(从而使得View.dispatchTouchEvent()直接返回true,事件分发结束) li.mOnTouchListener.onTouch(this, event)
为假就执行onTouchEvent(event)
。磨磨唧唧说了这么多废话,那到底li.mOnTouchListener.onTouch(this, event)
返回什么呢,这就看我们回调重写onTouch()中返回什么它就是什么。
假设我们让onTouch()返回false 则执行onTouchEvent(event)
我们来看onTouchEvent(event)
源码
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
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:
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)) {
performClickInternal();
}
}
}
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:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
这也太*多了,难受香菇,不用担心,我们来挑重要代码进行分析找到case MotionEvent.ACTION_DOWN
:/触摸按下事件*
我们来看这
`if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} `
简单的讲就是通过post向UI线程中发送消息,然后会执行 performClick()方法,这个方法是执行点击事件。看到这里会不会哎呦忽然知道了什么?,没错,也就是onclick()是在ontouch()处理的时候后发生的,也就是ontouch()优先于onclick()
我们再来分析performClick()源码
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
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;
}
其中`if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}`
又是这个判断,虽然不一样但是类似,我们看li!=null是真,第二个参数li.mOnClickListener != null
当我们调用控件的setOnclicklistener()事件是,mOnClickListener 会被赋值,所以第二个参数也为真 所以控件点击事件被处理。
以上说了这么多会不会有点懵,没关系,我们通过实例来证明以上的观点
- 6.实例分析
创建一个布局,添加一个button当作测试控件
package a_text.com.tasdasdasd;
import android.app.Activity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
public class MainActivity extends Activity {
private Button button;
private static final String tast="日志分析";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button=findViewById(R.id.button);
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i(tast,"执行了onTouch()+ 动作是:" + event.getAction());
return false;
}
});
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(tast,"执行了onClick()");
}
});
}
}
运行程序,点击button 查看日志
会发现没毛病onTouch()优先级确实比onClick()高,也证实了我们以上的说法。
接下来我们将onTouch()返回值改成true那会怎么样呢,按照我们之前的说法将不会执行onClick()方法,也就是日志中不会在有执行了onClick(),究竟是不是这样呢。请看日志:
我们发现叮咚,我们对了,证实了之上的观点。
总结:onTouch();返回true将会阻碍事件继续向下传递,
所以onClick()方法不会被执行,反之执行onClick()方法。
现在我们回到我最开始提出的问题,为什么我在重写onTouch()方法中返回true,按钮不执行onclick方法,而返回值改成false则正确,通过上面的浅谈你应该会有想法了。
白话总结:
View.dispatchTouchEvent派发事件是传递的,如果返回值为true将停止下次事件派发,如果返回false将继续下次派发。例如,当前派发ACTION_DOWN事件,如果返回false则继续派发ACTION_UP,如果返回true派发完ACTION_DOWN就停止了,所以接受不到ACTION_UP、ACTION_MOVE。
结束语:
好了,到这里就结束了,作为一个菜鸟,我还是有梦想的,通过不断学习中进步。因为本人水平有限,所以本文只是浅谈,有更多的细节没有介绍到,如果有问题,希望大佬们指出,写这个文章是希望记录自己的学习过程,温故而知新。