View的事件分發(fā)機(jī)制

事件分發(fā)機(jī)制在View體系中幾乎是最重要的知識點,理解透徹后在以后自定義各種復(fù)雜的View時我們就能更加得心應(yīng)手了。

點擊事件的傳遞規(guī)則

點擊事件即MotionEvent,點擊事件的事件分發(fā),也就是對MotionEvent事件的分發(fā)過程,當(dāng)一個MotionEvent產(chǎn)生之后,系統(tǒng)需要把這個事件傳遞到一個具體的View,而這個傳遞過程就是分發(fā)過程。點擊事件的分發(fā)過程由下面三個很重要的方法共同完成,dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。

  • dispatchTouchEvent:用來進(jìn)行事件的分發(fā),如果事件能傳遞給當(dāng)前View,那么此方法一定會被調(diào)用,返回結(jié)果受當(dāng)前View的onTouchEvent和下級View的dispatchTouchEvent方法影響,表示是否消耗當(dāng)前事件。

  • onInterceptTouchEvent:該方法在dispatchTouchEvent方法內(nèi)部調(diào)用,用來判斷是否攔截某個事件,如果當(dāng)前View攔截了某個事件,那么在同一個事件序列中,此方法不會被再次調(diào)用,返回的結(jié)果表示是否攔截當(dāng)前事件。

  • onTouchEvent:該方法在dispatchTouchEvent方法內(nèi)部調(diào)用,用來處理點擊事件,返回結(jié)果表示是否消耗當(dāng)前事件,如果不消耗,則在同一事件序列中,當(dāng)前View無法再次接收到事件。

這個三個方法的關(guān)系:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean consume = false;// 是否消耗事件
    if (onInterceptTouchEvent(event)) {
        consume = onTouchEvent(event);
    } else {
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

上述偽代碼表示的傳遞規(guī)則為:對于一個ViewGroup來說,點擊事件產(chǎn)生后,首先會傳遞給它本身,這是它的dispatchTouchEvent方法會被調(diào)用,如果這個ViewGroup的onInterceptTouchEvent方法返回的是true就表示它要攔截這個事件,接著,這個事件就會交給這個ViewGroup來處理了,接著它的onTouchEvent方法就會被調(diào)用;但是如果這個ViewGroup的onInterceptTouchEvent返回false就表示它不攔截當(dāng)前事件,這時事件就會繼續(xù)傳遞給它的子元素,接著子元素的dispatchTouchEvent方法會被調(diào)用,過程反復(fù)直到事件被最終處理。

當(dāng)一個View需要處理事件時,如果它設(shè)置了onTouchListener,那么onTouchListener中的onTouch方法會被回調(diào)。這時,事件如何處理還要看onTouch的返回值,如果返回false,則當(dāng)前View的onTouchEvent方法會被調(diào)用,如果返回true,那么onTouchEvent方法不會被調(diào)用。因此,給View設(shè)置onTouchListener,其優(yōu)先級比onTouchEvent更高,在onTouchEvent方法中,如果當(dāng)前View有設(shè)置OnClickListener,那么它的onClick方法將會被調(diào)用,可以看出,OnClickListener對View的優(yōu)先級最低,處于事件傳遞末端。

當(dāng)一個點擊事件產(chǎn)生后,它的傳遞過程遵循以下的順序:Activity->Window->View,即事件總是先傳遞給Activity,Activity再傳給Window,Window再傳給最頂層的View,頂層View接收到事件后,就會按照事件分發(fā)機(jī)制去分發(fā)事件。如果一個View的onTouchEvent方法中返回了false,那么它的父容器的onTouchEvent將會被調(diào)用,依此類推。如果所有的元素都不處理這個事件,那么這個事件最終將會傳遞給Activity處理,即Activity的onTouchEvent方法將會被調(diào)用。

關(guān)于事件傳遞機(jī)制的一些結(jié)論

  • 同一個事件序列是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結(jié)束,在這個過程中所產(chǎn)生的一系列事件,這個事件序列以DOWN事件開始,以UP事件結(jié)束,中間有N多個MOVE事件。

  • 正常情況下,一個事件序列只能被一個View攔截且消耗。因為一旦一個元素攔截了某次事件,那么同一個事件序列內(nèi)的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理。

  • 某個View一旦決定攔截,那么這一個事件序列都只能由它來處理,并且它的onInterceptTouchEvent方法不會再被調(diào)用,就是說當(dāng)一個View決定攔截一個事件后,那么系統(tǒng)會把同一個事件序列內(nèi)的其他方法都直接交給它來處理,因此就不用再調(diào)用這個View的onInterceptTouchEvent方法去詢問它是否要攔截了。

  • 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN(onTouchEvent方法返回了false),那么同一事件序列中的其他事件也不會再交給它來處理,并且事件將重新交給它的父元素去處理,也就是父元素的onTouchEvent方法會被調(diào)用。

  • 如果View不消耗除ACTION_DOWN以外的其他事件,那么這個點擊事件會消失,此時父元素的onTouchEvent方法并不會被調(diào)用,并且當(dāng)前View可以持續(xù)收到手續(xù)的事件,最終這些消失的點擊事件會傳遞給Activity處理。

  • ViewGroup默認(rèn)不攔截任何事件。因為在Android源碼中ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false。

  • View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它,那么它的onTouchEvent方法就會被調(diào)用。

  • View的onTouchEvent方法默認(rèn)都會返回true表示消耗事件,除非它是不可點擊的(clickable和longClickable同時為false)。View的longClickable屬性默認(rèn)為false,clickable屬性要分情況,比如Button的clickable默認(rèn)為true,而TextView的clickable默認(rèn)為false。

  • View的enable屬性不影響onTouchEvent的默認(rèn)返回值。即使一個View是disable狀態(tài)的,只要它的clickable或者longClickable有一個為true,那么它的onTouchEvent就返回true。

  • onClick方法會被調(diào)用的前提是當(dāng)前View是可點擊的,并且它收到了ACTION_DOWN和ACTION_UP事件。

  • 事件傳遞的過程是由外向內(nèi)的,即事件總是先傳給父元素,然后由父元素分發(fā)給子View,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過程,但是ACTION_DOWN事件除外。

事件分發(fā)的源碼解析

當(dāng)一個點擊事件產(chǎn)生時,最先傳遞給當(dāng)前的Activity,由Activity的dispatchTouchEvent來進(jìn)行事件分發(fā),具體是由Activity內(nèi)部的Window來完成的。Window會將事件傳遞給decor view,decor view一般就是當(dāng)前界面的底層容器(也就是setContentView所設(shè)置的View的父容器),通過Activity.getWindow.getDecorView()可以獲得。

下面是Activity的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

首先事件開始交給Activity所屬的Window進(jìn)行分發(fā),如果返回true,整個事件循環(huán)就結(jié)束了,返回false表示事件沒人處理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent方法會被調(diào)用。

接著看Window如何將事件傳遞給ViewGroup。我們知道Window其實是一個抽象類,它的實現(xiàn)類其實是PhoneWindow,我們來看看PhoneWindow中的superDispatchTouchEvent方法,這個類的路徑是/Users/daniel/Library/Android/sdk/sources/android-xx/com/android/internal/policy/PhoneWindow.java。

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

PhoneWindow直接將事件傳遞給了DecorView,這個DecorView的類所在目錄和PhoneWindow的路徑一樣。

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {}

在PhoneWindow中是這樣獲取DecorView的:

@Override
public final View getDecorView() {
    if (mDecor == null || mForceDecorInstall) {
        installDecor();
    }
    return mDecor;
}

我們通過

((ViewGroup) getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)

這種方式可以獲取到當(dāng)前Activity所設(shè)置的View,這個mDecor就是getWindow().getDecorView()返回的View,而通過setContentView設(shè)置的View就是它的一個子View。目前事件已經(jīng)傳遞到了DecorView,而DecorView又是繼承自FrameLayout的,所以事件之后會傳遞到DecorView的子View也就是Activity里面通過setContentView設(shè)置的View,這個View是根View,也叫頂級View,一般來說,根View都是ViewGroup。

ViewGroup對點擊事件的分發(fā)過程,主要實現(xiàn)在ViewGroup的dispatchTouchEvent方法中,它描述的是當(dāng)前View是否攔截點擊事件的邏輯。先看ViewGroup的dispatchTouchEvent方法中這么一段代碼:

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
} else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}

可以看出這一段是檢查攔截的邏輯。ViewGroup會在下面兩種情況下判斷是否要攔截事件,一是事件類型為ACTION_DOWN,二是mFirstTouchTarget不等于null,這個mFirstTouchTarget在事件被ViewGroup的子元素成功處理時它會被賦值并指向子元素,也就是說當(dāng)ViewGroup不攔截事件并將事件交給子元素處理時mFirstTouchTarget就不等于null,反過來,如果ViewGroup被攔截了,那么mFirstTouchTarget就等于null,所以當(dāng)ACTION_MOVE和ACTION_UP事件到來時由于

(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 

這個條件不滿足,所以不會再調(diào)用onInterceptTouchEvent方法,并且同意事件序列中的其它事件都會默認(rèn)交給ViewGroup來處理。

有一種特殊情況,就是FLAG_DISALLOW_INTERCEPT這個標(biāo)記位被設(shè)置后,ViewGroup將無法攔截除了ACTION_DOWN之外的其它所有點擊事件,這個標(biāo)記位是通過requestDisallowInterceptTouchEvent方法來設(shè)置的,一般用在子View 中。為什么說是除了ACTION_DOWN之外的其它所有點擊事件都會攔截,而不攔截ACTION_DOWN事件呢?因為ViewGroup在分發(fā)事件時,如果是ACTION_DOWN就會重置FLAG_DISALLOW_INTERCEPT這個標(biāo)記位,導(dǎo)致子View中設(shè)置的這個標(biāo)記位無效。因此,當(dāng)面對ACTION_DOWN事件時,ViewGroup都是調(diào)用自己的onInterceptTouchEvent方法來詢問自己是否要攔截事件。

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

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

上面這段代碼就是當(dāng)ACTION_DOWN事件到來時,ViewGroup重置FLAG_DISALLOW_INTERCEPT標(biāo)記位的。

總結(jié)

當(dāng)ViewGroup決定攔截事件后,后續(xù)的事件都會默認(rèn)交給它處理并不再調(diào)用它的onInterceptTouchEvent方法。FLAG_DISALLOW_INTERCEPT這個標(biāo)記位的作用就是讓ViewGroup不攔截事件。onInterceptTouchEvent方法不是每次都會調(diào)動,如果我們想提前處理所有的點擊事件,就要選擇dispatchTouchEvent方法,因為這個方法是每次都會調(diào)用的。

當(dāng)ViewGroup不再攔截的時候,事件會向下分發(fā)交給它的子View進(jìn)行處理。

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 there is a view that has accessibility focus we want it
    // to get the event first and if not handled we will perform a
    // normal dispatch. We may do a double iteration but this is
    // safer given the timeframe.
    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;
    }

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // Give it the new pointer in addition to the ones it is handling.
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }

    resetCancelNextUpFlag(child);
    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);
}

這段代碼就是遍歷ViewGroup的子元素,然后判斷子元素是否能夠接收到點擊事件。是否能夠接收到點擊事件由兩個因素決定,看下面的代碼:

private static boolean canViewReceivePointerEvents(@NonNull View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null;
}

一個因素就是子元素是否在播放動畫,另一個因素是點擊時間的坐標(biāo)是否落在子元素的區(qū)域內(nèi)。如果滿足這兩個條件,那么事件就會傳遞給它來處理??梢钥吹絛ispatchTransformedTouchEvent方法實際上調(diào)用的就是子元素的dispatchTouchEvent方法。

if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
    event.setAction(MotionEvent.ACTION_CANCEL);
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
}

也就是說如果子元素的dispatchTouchEvent方法返回true,那么break循環(huán),并且給mFirstTouchTarget賦值。如果返回false,那么會繼續(xù)循環(huán)直至找到能處理事件的子元素。如果遍歷了所有子元素事件都沒有被處理,要么ViewGroup沒有子元素要么是子元素處理了點擊事件,但在dispatchTouchEvent方法中返回了false,這一般是因為子元素在onTouchEvent方法中返回了false。這兩種情況下ViewGroup會自己處理點擊事件。

/**
 * Adds a touch target for specified child to the beginning of the list.
 * Assumes the target child is not already present.
 */
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

當(dāng)mFirstTouchTarget為null時,ViewGroup會調(diào)用super.dispatchTouchEvent方法,因為下面的代碼中第三個參數(shù)child它傳的是null,而child為null時,正是調(diào)用的super.dispatchTouchEvent方法,我們知道ViewGroup也是繼承View,所以事件將由View來處理。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} 

View的點擊事件的處理比較簡單。

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 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;
        }
    }

    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是不包括ViewGroup的,是單個元素,不會再有子元素了,無法傳遞事件,所以只需考慮它本身對事件的處理了。

首先View會判斷有沒有設(shè)置onTouchListener,如果onTouchListener中的onTouch方法返回true,那么表示onTouch方法將事件消耗掉了,onTouchEvent方法不會再被調(diào)用。

以前做ListView的item點擊時,一般會有onItemClick處理單擊事件和onItemLongClick處理長按事件,而onItemLongClick返回類型就是boolean類型,這個結(jié)果表示是否要消耗掉點擊事件,如果返回true,那么在onItemClick中就不會再響應(yīng)了,因為onItemLongClick比onItemClick的優(yōu)先級高。所以要能同時響應(yīng)這兩個事件,onItemLongClick結(jié)果必須返回false。相信以前有人遇到過這個問題吧。

在View的onTouchEvent方法中對點擊事件的處理:

if (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
        (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            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;

        case MotionEvent.ACTION_DOWN:
            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
                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
            if (!pointInView(x, y, mTouchSlop)) {
                // Outside button
                removeTapCallback();
                if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                    // Remove any future long press/tap checks
                    removeLongPressCallback();

                    setPressed(false);
                }
            }
            break;
    }

    return true;
}

簡單來說就是CLICKABLE和LONG_CLICKABLE有一個為true那么這個View就可以消耗這個事件,最后onTouchEvent返回true。在ACTION_UP事件中最后會執(zhí)行performClick方法,在其內(nèi)部會調(diào)用listener的onClick方法:

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;
}

View的LONG_CLICKABLE默認(rèn)是false,CLICKABLE則是不同的View可能不同。比如Button默認(rèn)是true,TextView默認(rèn)是false。當(dāng)通過setOnClickListener/setOnLongClickListener方法來設(shè)置點擊監(jiān)聽時,就已經(jīng)將LONG_CLICKABLE或者CLICKABLE屬性設(shè)置為true了。

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
    if (!isLongClickable()) {
        setLongClickable(true);
    }
    getListenerInfo().mOnLongClickListener = l;
}

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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