您的位置:首页 > 移动开发 > Android开发

Android 事件分发机制源码攻略(三) —— View篇

2017-10-12 00:00 447 查看
继上篇Android 事件分发机制源码攻略(二) —— ViewGroup篇的介绍后,我们知道事件如何从Activity的dispatchTouchEvent经由顶层ViewDecorView 再到ViewGroup的dispatchTouchEvent,ViewGroup层的分发,我个人觉得是整个事件分发最为关键的一部分,理解透了ViewGroup层的事件传递,相当于对整个事件分发传递也就差不多了。现在事件传递到View层,这一篇,我们将对分析事件在View层的dispatchTouchEvent、onTouchEvent、OnTouchListener、OnClickListener这些方法的传递顺序。首先,我们来看下dispatchTouchEvent这个方法。

/**
* 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();
}

//跟ViewGroup层的一样,都是安全策略
if (onFilterTouchEventForSecurity(event)) {
//鼠标拖拉处理,不是分发的重点
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
//内部类,包含各种触摸监听
ListenerInfo li = mListenerInfo;
//优先判断mOnTouchListener是否为空,为空的话就跳过,不为空的话,就走到onTouchEvent方法
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//result为false的情况
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方法相对简单,没有复杂的逻辑。
整个方法最为关键的地方是从第41行开始,先对OnTouchListener这个监听进行判断


如果给这个View设置了OnTouchListener监听并且返回true,那result就直接为true了,这直接导致事件没有进到onTouchEvent方法就结束分发了。如果没有设置OnTouchListener监听,那就会进到onTouchEvent方法,然后View的dispatchTouchEvent方法到此也就结束了。接着,我们再来看看onTouchEvent()方法。

/**
* 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();
//如果View的状态是DISABLED,那返回值将由View可点击性(单点、长按等)决定
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//如果View是可点击的,返回true,即是事件默认消费了。否则返回false;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
//在ACTION_DOWN选项里会设置mPrivateFlags的状态
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);
}
//如果这事件是从ACTION_DOWN开始,那mHasPerformedLongPress应该为false
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();
}
// 取消View的Press状态
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:
//设置长按的触发位置标签为false
mHasPerformedLongPress = false;

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
//设置View为Pressed,并触发长按事件
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;

case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;

case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);

// Be lenient about moving outside of buttons
//移动的位置是否已经不在该View的范围内
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
//移除CheckForTap
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
// 移除长按任务
removeLongPressCallback();
// 取消View的Press状态
setPressed(false);
}
}
break;
}

return true;
}

return false;
}


哇,初看这个方法很长,以为很麻烦。其实里面很简单,依旧是没什么麻烦的逻辑的。以上方法可以总结成以下两点

1、如果View是可点击、可长按的,则返回true;否则返回false;

2、如果View是可点击、可长按的,优先触发performLongClick()长按事件,当抬起来时,触发onClick()事件(前提是你设置了OnClickListener监听)。

整个方法的话,除了上面说的前提外,剩下的要点就是分析在ACTION_DOWN、ACTION_MOVE、ACTION_UP所做的处理了。首先来看下ACTION_DOWN里面做了那些操作。

看到第110行有这么个方法isInScrollingContainer,我们进去看看是做了什么。

/**
* @hide 隐藏的方法
*/
public boolean isInScrollingContainer() {
ViewParent p = getParent();
while (p != null && p instanceof ViewGroup) {
if (((ViewGroup) p).shouldDelayChildPressedState()) {
return true;
}
p = p.getParent();
}
return false;
}


这个方法是调用其父布局的shouldDelayChildPressedState方法,那再去看看这个方法做了什么

/**
* Return true if the pressed state should be delayed for children or descendants of this
* ViewGroup. Generally, this should be done for containers that can scroll, such as a List.
* This prevents the pressed state from appearing when the user is actually trying to scroll
* the content.
*
* The default implementation returns true for compatibility reasons. Subclasses that do
* not scroll should generally override this method and return false.
*/
public boolean shouldDelayChildPressedState() {
return true;
}


这个注释大概的意思是父布局如果是可滑动的,应该返回true;否则,返回False;这个不属于我们这次讨论的问题,有兴趣的同学可以自行深入探究。

接着我们再看上面的第117行-124行,这几句代码的主要的用途是延迟执行CheckForTap这个任务,我们再看看这个CheckForTap做了那些操作。

private final class CheckForTap implements Runnable {
public float x;
public float y;

@Override
public void run() {
//更改flag
mPrivateFlags &= ~PFLAG_PREPRESSED;
//设置pressed为true
setPressed(true, x, y);
//检查并执行长按
checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
}
}


这是一个实现Runnable接口的类,后面我们会看到很多的执行操作都是实现Runnable接口;这块代码跟128处的代码差不多,也就是说,两者的区别在于是否延迟执行后续任务了。好了,我们再来看看checkForLongClick这个方法做了什么。

private void checkForLongClick(int delayOffset, float x, float y) {
//该View是否可以长按
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
mHasPerformedLongPress = false;

if (mPendingCheckForLongPress == null) {
//实例长按任务
mPendingCheckForLongPress = new CheckForLongPress();
}
//设置锚点
mPendingCheckForLongPress.setAnchor(x, y);
//修改WindowAttachCount
mPendingCheckForLongPress.rememberWindowAttachCount();
//延迟执行长按任务
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
}


这个方法很简单,先是判断这个View是否可以长按,为true的话,将当前位置设置下去,并修改相应的View所关联的window数量。紧接着延迟执行长按任务。

private final class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
private float mX;
private float mY;

@Override
public void run() {
//pressed为true并且父布局不为空
if (isPressed() && (mParent != null)
//基本上有调用rememberWindowAttachCount这个方法基本是相等的
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick(mX, mY)) {
mHasPerformedLongPress = true;
}
}
}

public void setAnchor(float x, float y) {
mX = x;
mY = y;
}

public void rememberWindowAttachCount() {
mOriginalWindowAttachCount = mWindowAttachCount;
}
}

/**
* Calls this view's OnLongClickListener, if it is defined. Invokes the
* context menu if the OnLongClickListener did not consume the event,
* anchoring it to an (x,y) coordinate.
*
* @param x x coordinate of the anchoring touch event, or {@link Float#NaN}
*          to disable anchoring
* @param y y coordinate of the anchoring touch event, or {@link Float#NaN}
*          to disable anchoring
* @return {@code true} if one of the above receivers consumed the event,
*         {@code false} otherwise
*/
public boolean performLongClick(float x, float y) {
mLongClickX = x;
mLongClickY = y;
final boolean handled = performLongClick();
mLongClickX = Float.NaN;
mLongClickY = Float.NaN;
return handled;
}

/**
* Calls this view's OnLongClickListener, if it is defined. Invokes the
* context menu if the OnLongClickListener did not consume the event.
*
* @return {@code true} if one of the above receivers consumed the event,
*         {@code false} otherwise
*/
public boolean performLongClick() {
return performLongClickInternal(mLongClickX, mLongClickY);
}

/**
* Calls this view's OnLongClickListener, if it is defined. Invokes the
* context menu if the OnLongClickListener did not consume the event,
* optionally anchoring it to an (x,y) coordinate.
*
* @param x x coordinate of the anchoring touch event, or {@link Float#NaN}
*          to disable anchoring
* @param y y coordinate of the anchoring touch event, or {@link Float#NaN}
*          to disable anchoring
* @return {@code true} if one of the above receivers consumed the event,
*         {@code false} otherwise
*/
private boolean performLongClickInternal(float x, float y) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

boolean handled = false;
final ListenerInfo li = mListenerInfo;
//触发长按监听事件
if (li != null && li.mOnLongClickListener != null) {
handled = li.mOnLongClickListener.onLongClick(View.this);
}
if (!handled) {
final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
}
if (handled) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
return handled;
}


上面的代码没有太多的逻辑,最终的实现是最后面这一块。从最后这里可以看出,View的onTouchEvent事件处理首个ACTION_DOWN事件,优先响应的是长按监听(如果你有设置的话),如果该布局是可滑动的(比如listView这类),会有相应的延迟。好了,到这里ACTION_DOWN的事件到此就结束了。接着,我们来看下ACTION_MOVE事件的处理流程。

从上面的代码的第145-157行,观察注释就基本了解了。在View的范围内,基本不处理。那我们再来看看ACTION_UP做了哪些处理。

我们可以直接看第74行PerformClick,其他的代码基本上都是一些简单的判断,主要的就是PerformClick这个类。

private final class PerformClick implements Runnable {
@Override
public void run() {
performClick();
}
}

/**
* Call this view's OnClickListener, if it is defined.  Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
*         otherwise is returned.
*/
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;
}


看到这里我们知道,ACTION_UP事件最终会走到OnClickListener这个监听去。自此我们可以得出当首个ACTION_DOWN事件下来,在View的这一层首先是

OnTouchListener->onTouchEvent->OnLongClickListener->OnClickListener


我们来做对View层的事件分发做个小结。



至此,View事件分发就到此结束了。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 源码
相关文章推荐