Android View事件分發(fā)機(jī)制源碼解析

對于View的事件分發(fā),涉及的有dispatchTouchEvent、onTouchEvent、onTouch、onClick
為了更好的查看View的事件轉(zhuǎn)發(fā),我們先來看個demo
首先定義一個自定義View

public class CustomView extends AppCompatButton {

    private static final String TAG = "CustomView";

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                LogUtils.e(TAG,"dispatchTouchEvent=ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                LogUtils.e(TAG,"dispatchTouchEvent=ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                LogUtils.e(TAG,"dispatchTouchEvent=ACTION_UP");
                break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                LogUtils.e(TAG,"onTouchEvent=ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                LogUtils.e(TAG,"onTouchEvent=ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                LogUtils.e(TAG,"onTouchEvent=ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }
}

把自定義的按鈕添加到布局中:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.xiaoma.restudy.customviews.CustomView
            android:id="@+id/mbt_cmb"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Custom View" />
    </LinearLayout>

</android.support.constraint.ConstraintLayout>

最后看CustomActivity代碼

public class CustomActivity extends BaseActivity {
    private CustomView mMbtCmb;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_custom);
        initView();
    }

    @SuppressLint("ClickableViewAccessibility")
    private void initView() {
        mMbtCmb = (CustomView) findViewById(R.id.mbt_cmb);
        mMbtCmb.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        LogUtils.e(TAG, "onTouch=ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        LogUtils.e(TAG, "onTouch=ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        LogUtils.e(TAG, "onTouch=ACTION_UP");
                        break;
                }
                return false;
            }
        });
        mMbtCmb.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LogUtils.e(TAG, "onClick");
            }
        });

        mMbtCmb.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                LogUtils.e(TAG, "onLongClick");
                return false;
            }
        });
    }
}

點(diǎn)擊按鈕后,運(yùn)行的結(jié)果如下

 E/CustomView: dispatchTouchEvent=ACTION_DOWN
E/.customviews.CustomActivity: onTouch=ACTION_DOWN
 E/CustomView: onTouchEvent=ACTION_DOWN
 E/CustomView: dispatchTouchEvent=ACTION_MOVE
E/.customviews.CustomActivity: onTouch=ACTION_MOVE
 E/CustomView: onTouchEvent=ACTION_MOVE
E/.customviews.CustomActivity: onLongClick
 E/CustomView: dispatchTouchEvent=ACTION_MOVE
E/.customviews.CustomActivity: onTouch=ACTION_MOVE
 E/CustomView: onTouchEvent=ACTION_MOVE
 E/CustomView: dispatchTouchEvent=ACTION_UP
E/.customviews.CustomActivity: onTouch=ACTION_UP
 E/CustomView: onTouchEvent=ACTION_UP
E/.customviews.CustomActivity: onClick

點(diǎn)擊下就可以出現(xiàn)上面日志,否則出現(xiàn)一系列ACTION_MOVE事件;但總結(jié)來看是ACTION_DOWN、ACTION_MOVE、ACTION_UP三個事件的傳遞是dispatchTouchEvent-->onTouch-->onTouchEvent-->onLongClick-->onClick
查看源碼從View的dispatchTouchEvent方法開始,摘抄代碼如下

  if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
           //上面沒有沒有消費(fèi),在onTouchEvent中返回為true進(jìn)行消費(fèi)。
            if (!result && onTouchEvent(event)) {
                result = true;
            }

1、如果onTouchListener不等于null,并且是enabled,且onTouch返回true,則消費(fèi)掉事件
2、上面如果沒有消費(fèi),則執(zhí)行onTouchEvent,如果返回true,則消費(fèi)掉事件

總結(jié):也就是說如果onTouch方法返回ture,則不執(zhí)行onTouchEvent方法

View的onTouchEvent方法如下,摘要主要功能如下

 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:
      break;
                case MotionEvent.ACTION_DOWN:
      break;
                case MotionEvent.ACTION_CANCEL:
      break;
                case MotionEvent.ACTION_MOVE:
      break;
    return  true;
}

1.clickable變量標(biāo)識如果view是可點(diǎn)擊或者長按的狀態(tài)就是true;
2.如果view是disabled狀態(tài),不處理事件,返回clickable變量;意味著一個disabled狀態(tài)的view,如果可點(diǎn)擊或者長按,還是消費(fèi)事件,但是不響應(yīng)
3.如果mTouchDlegate不為空,則當(dāng)前view不消費(fèi)事件,交給觸摸代理消費(fèi)事件;
4.如果clickable返回true或者view可以在懸?;蛘唛L按時顯示工具,則view消耗此事件;

查看MotionEvent.ACTION_DOWN事件,源碼如下

  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;
  1. mHasPerformedLongPress = false; 長按事件還未觸發(fā)
  2. boolean isInScrollingContainer = isInScrollingContainer(); 判斷是否是個滑動容器
  3. 如果是滑動容器,設(shè)置 mPrivateFlags |= PFLAG_PREPRESSED; 設(shè)置為預(yù)按狀態(tài);發(fā)送一個延遲為ViewConfiguration.getTapTimeout()=100的消息;如果非滑動容器,則直接設(shè)置為按壓狀態(tài),且發(fā)送延遲500后執(zhí)行CheckForLongPress()
  4. 到達(dá)延遲時間后,執(zhí)行CheckForTap()里的run()方法,取消預(yù)按狀態(tài),且在setPressed(true, x, y)方法里設(shè)置按壓狀態(tài) mPrivateFlags |= PFLAG_PRESSED;且檢查是否有長按事件,如果有再發(fā)送一個延遲為ViewConfiguration.getLongPressTimeout()=500的消息(減去上面的延遲時間),到達(dá)延遲時間后,執(zhí)行CheckForLongPress()

總結(jié):100用來檢測是否按壓了,500時長用來檢測是否長按

  1. 如果設(shè)置了長按的回調(diào);如果長按的返回值為true,則設(shè)置 mHasPerformedLongPress = true;如果沒有設(shè)置長按回調(diào)或者返回false,則mHasPerformedLongPress依舊等于false;
 private final class CheckForTap implements Runnable {
        public float x;
        public float y;

        @Override
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            setPressed(true, x, y);
            checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
        }
    }
##################################
  private void checkForLongClick(int delayOffset, float x, float y) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            mPendingCheckForLongPress.rememberPressedState();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }
#################
 private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;

        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick(mX, mY)) {
                    mHasPerformedLongPress = true;
                }
            }
        }

查看MotionEvent.ACTION_MOVE事件,源碼如下:

 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;
判斷當(dāng)前坐標(biāo)是否移除了當(dāng)前的view,pointInView(x, y, mTouchSlop),如果移出,則
  1. 執(zhí)行removeTapCallback(),移出是滑動容易內(nèi),則設(shè)置 mPrivateFlags &= ~PFLAG_PREPRESSED且不到100移除回調(diào)
  2. 執(zhí)行 removeLongPressCallback(),移除長按回調(diào)
  3. 如果是按壓狀態(tài),則設(shè)置 mPrivateFlags &= ~PFLAG_PRESSED

總結(jié):只要用戶移出了我們的控件:則將mPrivateFlags取出PRESSED標(biāo)識,且移除所有在DOWN中設(shè)置的檢測,長按等;

查看MotionEvent.ACTION_UP事件,源碼如下:

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;
  1. 如果mPrivateFlags包含PFLAG_PREPRESSED或者PFLAG_PRESSED則進(jìn)入if執(zhí)行語句,也就是無論100ms內(nèi)或者之后抬起都會進(jìn)入if語句執(zhí)行
  2. 如果mHasPerformedLongPress是false,即長按沒有執(zhí)行,則進(jìn)入方法體,移除長按回調(diào)
  3. 如果mPerformClick為null,則初始化一個實(shí)例;然后立即通過Handler添加到消息隊(duì)列尾部,如果添加失敗,直接執(zhí)行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;
    }
  1. 如果mOnClickListener!=null,則回調(diào)onClick()方法
  2. 如果prepressed為true,則執(zhí)行mUnsetPressedState.run()方法。我們的mPrivateFlags中的PRESSED取消,如果mPendingCheckForTap不為null,移除;
總結(jié)

1.整個view的事件分發(fā)流程是:
View.dispatchTouchEvent->View.setOnTouchListener->View.onTouchEvent
在dispatchTouchEvent中會進(jìn)行onTouchListener的判斷,如果不為null且返回ture,則整個事件被消費(fèi),onTouchEvent不會執(zhí)行,否則執(zhí)行onTouchEvent

  1. onTouchEvent中的DOWN、MOVE、UP
DOWN時

1、首先設(shè)置mHasPerformedLongPress=false,代表長按是false;
2、如果在滑動容器內(nèi),首先設(shè)置mPrivateFlags=PFLAG_PREPRESSED且發(fā)送一個延遲100ms的mPendingCheckForTap,則將標(biāo)識位mPrivateFlags=PFLAG_PRESSED,清除PFLAG_PREPRESSED標(biāo)識,同時發(fā)送一個延遲500-100ms的mPendingCheckForLongPress,檢測長按任務(wù)消息
3、如果不在滑動容器內(nèi)容,則直接設(shè)置mPrivateFlags=PFLAG_PRESSED且發(fā)送一個延遲500ms的mPendingCheckForLongPress的檢測長按任務(wù)消息
4、如果時間超過500ms,則觸發(fā)mOnLongClickListener;如果mOnLongClickListener不為null,且mOnLongClickListener.onLongClick返回true,則mHasPerformedLongPress=true;否則mHasPerformedLongPress還是false

MOVE時

主要就是檢測是否劃出了View控件,如果劃出了
直接移除mPendingCheckForTap,mPendingCheckForLongPress;如果100ms后mPrivateFlags==PFLAG_PRESSED,則清除mPrivateFlags值

UP時
  1. 如果mPrivateFlags=PFLAG_PREPRESSED,即100ms內(nèi)觸發(fā)up,則依次執(zhí)行 setPressed(true, x, y)、mUnsetPressedState
    2、如果mHasPerformedLongPress==false,代表長按未觸發(fā),移除長按檢測,執(zhí)行onClick回調(diào)
    3.500ms后,如果設(shè)置了onLongClickListener,且onLongClickListener.onClick返回true,則點(diǎn)擊事件OnClick無法觸發(fā);如果沒有設(shè)置onLongClickListener或者onLongClickListener.onClick返回false,則點(diǎn)擊事件OnClick依舊可以觸發(fā)
  2. 所以onLongClickListener是在move觸發(fā),onClick是在up觸發(fā)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容