Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android事件傳遞淺析

Android事件傳遞淺析

編輯:關於Android編程

事件機制是Android中一個比較復雜且重要的知識點,比如你想自定義攔截事件,或者某系組件中嵌套了其他布局,往往會出現這樣那樣的事件沖突,坑爹啊!!事件主要涵蓋onTouch,onClick,onTouchEvent,dispatchTouchEvent,onInterceptTouchEvent等等一系列事件,並且事件間還相互交互耦合,甚至有的事件還有返回值,一會true,一會false,什麼情況下返回true,什麼情況下返回false,為什麼要有返回值,想想這些就感覺整個人都不好了。
但是(萬惡的但是),該知識點還是必須要掌握的,知識的深度與廣度決定了你走的遠度,鑒於此我們就來捅一捅該知識點。

准備工作

俗話說工欲善其事必先利其器,為了看他的執行流程,我們還是先寫個樣例,打幾個日志看看執行流程吧!

首先自定義一個外層布局的Layout,自定義Layout繼承了LinearLayout,復寫了相應的函數,在調用之前輸入日志。如下:

public class Layout extends LinearLayout {
    public Layout(Context context) {
        super(context);
        init();
    }

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

    public Layout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //requestDisallowInterceptTouchEvent(false);
    }


    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e("Event", "Layout onInterceptTouchEvent " + MotionEvent.actionToString(ev.getAction()));
        return super.onInterceptTouchEvent(ev);
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("Event", "Layout onTouchEvent " + MotionEvent.actionToString(event.getAction()));
        return super.onTouchEvent(event);
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e("Event", "Layout dispatchTouchEvent " + MotionEvent.actionToString(event.getAction()));
        return super.dispatchTouchEvent(event);
    }

}

我們還自定義了一個LogTextView,繼承自TextView,也是為了輸出日志,代碼如下:

public class LogTextView extends TextView {

    public LogTextView(Context context) {
        super(context);
    }

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

    public LogTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("Event", "TextView  onTouchEvent " + MotionEvent.actionToString(event.getAction()));
        return super.onTouchEvent(event);
    }

    @Override
    public void setOnTouchListener(OnTouchListener l) {
        super.setOnTouchListener(l);
    }

    @Override
    public void setOnClickListener(OnClickListener l) {
        super.setOnClickListener(l);
    }
}

接下來是布局文件了:




    

    

布局中嵌套了兩個view,一個TextView,一個ImageView。最後就是主界面了。

public class MainActivity extends AppCompatActivity {

    private LogTextView tv;
    private ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViews();
        setViewListener();
    }

    private void findViews() {
        tv = (LogTextView) findViewById(R.id.textView);
        imageView = (ImageView) findViewById(R.id.image);
    }

    private void setViewListener() {
        tv.setOnTouchListener(new View.OnTouchListener() {
            @TargetApi(Build.VERSION_CODES.KITKAT)
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e("Event", "TextView  onTouch " + MotionEvent.actionToString(event.getAction()));
                return true;
            }
        });
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("Event", "TextView  onClick ");
            }
        });
        imageView.setOnTouchListener(new View.OnTouchListener() {
            @TargetApi(Build.VERSION_CODES.KITKAT)
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e("Event", "ImageView onTouch " + MotionEvent.actionToString(event.getAction()));
                return false;
            }
        });
        imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e("Event", "ImageView onClick ");
            }
        });
    }
}

執行結果

round 1

TextView的onTouch返回為false,點擊TextView,日志如下:

05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_DOWN
05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_DOWN
05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_DOWN
05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: TextView  onTouchEvent ACTION_DOWN
05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_MOVE
05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_MOVE
05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_MOVE
05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: TextView  onTouchEvent ACTION_MOVE
05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_UP
05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_UP
05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_UP
05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: TextView  onTouchEvent ACTION_UP
05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: TextView  onClick 

根據日志我們可以看到首先有一個ACTION_DOWN事件,執行的順序是Layout的dispatchTouchEvent→onInterceptTouchEvent→(TextView)onTouch要→onTouchEvent,之後的我帕金森發生了,產生了ACTION_MOVE事件,傳遞的順序與Down是一致的,最後一個事件是UP事件,正常點擊不滑動是不會產生MOVE事件的,在這個這個三個事件最後調用了TextView的onClick事件。

小結:

1 . 事件的傳遞順序是先外層容器,之後再是具體的View。
2. onTouch事件先於onTouchEvent事件,onTouchEvent先於onClick事件

round 2

我們將TextView的onTouch事件返回true。重新執行。執行順序如下:

05-05 14:10:20.556 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_DOWN
05-05 14:10:20.556 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_DOWN
05-05 14:10:20.556 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_DOWN
05-05 14:10:20.616 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_MOVE
05-05 14:10:20.616 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_MOVE
05-05 14:10:20.616 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_MOVE
05-05 14:10:20.622 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_UP
05-05 14:10:20.622 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_UP
05-05 14:10:20.622 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_UP

從日志可以看出如果onTouch返回為true,執行順序變成了如下:
首先還是ACTION_DOWN事件(Layout)dispatchTouchEvent→onInterceptTouchEvent→(TextView)onTouch,ACTION_MOVE與ACTION_UP執行順序同ACTION_DOWN,可以發現的是TextView的onTouchEvent事件沒有了,並且onClick事件也沒有了。

小結

1,onTouch事件的返回值為true會攔截onTouchEvent事件
2,onTouchEvent與onClick有關聯

上面的兩次執行中每次都調用了onInterceptTouchEvent事件,這個到底又是啥?我們去看看他的返回值是什麼?

public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}

可以看到默認返回false,注釋長的嚇人,那我們就來改寫一下他的返回值,這個函數是ViewGroup才有的,說明與布局容器有關.
round 3

我們將Layout的onInterceptTouchEvent的返回值改為true。重新執行。執行順序如下:

05-05 14:59:17.829 15157-15157/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_DOWN
05-05 14:59:17.830 15157-15157/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_DOWN
05-05 14:59:17.830 15157-15157/com.sunny.event E/Event: Layout    onTouchEvent ACTION_DOWN

從日志可以發現,只有最外層的控件能夠執行事件,TextView已經收不到任何事件。

小結

父控件onInterceptTouchEvent返回true會攔截子控件的事件

追根溯源

我們從代碼的層面來看看他是怎麼執行的,當屏幕接收到點擊事件時會首先傳遞到Activity的dispatchTouchEvent:

/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 *
 * @param ev The touch screen event.
 *
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

在這裡執行了三步,

1.第一告訴用戶ACTION_DOWN,用戶可以復寫onUserInteraction來處理點擊開始
2.調用了getWindow().superDispatchTouchEvent(ev),這裡的getWindow得到是PhoneWindow對象,因此執行的PhoneWindow的superDispatchTouchEvent函數,
3.調用了Activity的onTouchEvent事件

PhoneWindow的superDispatchTouchEvent又調用了DecorView的superDispatchTouchEvent函數,每一個Activity都有一個PhoneWindow,每一個PhoneWindow都有一個DecorView,DecoView繼承自FrameLayout,這裡又調用了super.dispatchTouchEvent(event),FrameLayout裡面是沒有改函數的,所以最終執行的是ViewGroup的dispatchTouchEvent函數。

這裡我們先穿插一點界面的知識,以我測試手機為例,DecorView中有兩個child,分別是ViewStub和LinerLayout,LinerLayout中又包含了FrameLayout,FrameLayout中包含了一個ActionBarOverlayLayout,ActionBarOverlayLayout裡又包含了兩個child,分別是ActionBarContainer與ContentFrameLayout。

接下來我們去看看ViewGroup的dispatchTouchEvent函數:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) { 
    .........
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 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;
        }
        ............
        // Update list of touch targets for pointer down, if needed.
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = 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 preorderedList = buildOrderedChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder
                                ? getChildDrawingOrder(childrenCount, i) : i;
                        final View child = (preorderedList == null)
                                ? children[childIndex] : preorderedList.get(childIndex);
                        .......

                        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;
                        }
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // Update list of touch targets for pointer up or cancel, if needed.
        if (canceled || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

我們只看最重要的部分

1: 事件為ACTION_DOWN時,執行了cancelAndClearTouchTargets函數,該函數主要清除上一次點擊傳遞的路徑,之後執行了resetTouchState,重置了touch狀態,其中執行了 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;就是攔截狀態為false,這個與requestDisallowInterceptTouchEvent函數相關。

2: 獲取intercepted的值,首先判斷了disallowIntercept狀態,是否攔截子控件的事件執行。從代碼可以看到當disallowIntercept為false時,該狀態主要取決於onInterceptTouchEvent函數的返回值,這就是前面我們攔截的函數,如果為true,這時intercepted為true標識攔截。

3: 接著判斷了!canceled && !intercepted的值,canceled這裡為false,如果intercepted為false,則會進入判斷條件,這裡假設不攔截,進入後繼續判斷如果是ACTION_DOWN事件,則會繼續進入判斷,遍歷所有子控件,isTransformedTouchPointInView會判斷當前點擊區域是否在控件內,如果不在則遍歷下一個,之後調用dispatchTransformedTouchEvent函數。最後在調用addTouchTarget函數,將當前選中的控件,掛載到當前點擊目標鏈表。alreadyDispatchedToNewTouchTarget賦值為true。

接著判斷mFirstTouchTarget是否為空,經過上一步的addTouchTarget的執行,這裡mFirstTouchTarget不為空。第一個事件alreadyDispatchedToNewTouchTarget為true,且target == newTouchTarget,因此handled值為true,如果是後續的事件,則會進入dispatchTransformedTouchEvent中。

我們接著看看第三部中的dispatchTransformedTouchEvent函數:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Calculate the number of pointers to deliver.
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    .......
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }
    .......
    // Done.
    transformedEvent.recycle();
    return handled;
}

這裡會進入到第10行,且傳遞過來的child不為空,因此會繼續執行child.dispatchTouchEvent,這裡繼續執行ViewGroup的dispatchTouchEvent,一直遞歸執行,直到真正接受點擊的控件,到最後child會為空,這裡要麼是一個View控件,要麼是未包含任何子控件的ViewGroup,這時這裡會執行View的dispatchTouchEvent。

從上述執行邏輯可以直到,先從DecorView一直遞歸到Layout,最後再到TextView,這裡我們去看看View的dispatchTouchEvent:

public boolean dispatchTouchEvent(MotionEvent event) {

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) {
        //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;
}

首先會停止掉嵌套滑動,之後先判斷了ListenerInfo不為空,這裡只要是設置了onTouch,onKey,onHover,onDrag等等中的任何一個這裡就不為空,具體可以去看看ListenerInfo包含的listenerInfo類型。其次判斷了mOnTouchListener不為空,只要設置了onTouchListener這裡就不為空,再之後判斷了該控件是否是enabled,一般都會enabled,可以代碼設置為false,再之後調用了mOnTouchListener的onTouch事件,這裡就是外面傳進來的onTouchListener,從這裡可以看到無論onTouch返回任何值,onTouch事件都會執行,但是如果返回為true,則會導致result為true,!result && onTouchEvent(event)因為短路,不會執行到onTouchEvent事件。

小結

1:onTouch返回為true導致onTouchEvent不能執行
2:如果enable為false,因為短路onTouch不會執行

到此還沒有看到任何onClick事件的執行,我們繼續去看看onTouchEvent函數:

public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

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

    return false;
}

我們首先看ACTION_DOWN事件,這裡主要看checkForLongClick,CheckForTap中也調用了該函數,這裡就是添加一個長按事件,如果達到長按標准且長按listener不為空,則執行長按事件,接著我們看ACTION_UP,這裡看到如果不是長按事件,則調用了performClick,performClick裡面執行了onClick事件。

小結

1:onClick事件與onLongClick事件是在onTouchEvent中執行的
2:如果執行了長按事件則onClick不執行
3:就api 23代碼,長按的時間間隔為500毫秒

上面解析了intercepted為false的情況,那intercepted為true,它到底是怎麼攔截的?

如果intercepted為true,則!canceled && !intercepted為false,不能進入該判斷,mFirstTouchTarget為空,會繼續執行如下分支:

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

這裡第三參數傳遞的child為null,因此就會執行該控件onTouch與onTouchEvent函數,不會繼續遞歸傳遞,因此也就攔截了子控件的執行。

總結

事件接收先從父控件到子控件,如果父控件onInterceptTouchEvent為true,則表示攔截事件。 dispatchTouchEvent的ACTION_DOWN事件中,會清除上一次的點擊目標列表,且重置disallowIntercept狀態為false,表示攔截,但是真正的攔截狀態還是靠onInterceptTouchEvent函數的返回值決定。 如果為自定義控件,還需要重寫onInterceptTouchEvent。 如果onLongClick執行,api 23 默認時間為500毫秒,則onClick不執行。 如果onTouch事件返回為true,則會攔截onTouchEvent事件,onClick,onLongClick事件均不在執行。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved