android事件分發(fā)

android事件分發(fā)

示例代碼地址https://github.com/kinglong123/androiddistribution

基礎(chǔ)知識(shí)

事件主要有 down (MotionEvent.ACTION_DOWN),move(MotionEvent.ACTION_MOVE),up(MotionEvent.ACTION_UP)。
基本上的手勢(shì)均由 down 事件為起點(diǎn),up 事件為終點(diǎn),中間可能會(huì)有一定數(shù)量的 move 事件。這三種事件是大部分手勢(shì)動(dòng)作的基礎(chǔ)。

先來(lái)分析View的分發(fā)

結(jié)合下面的布局

<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        tools:context="touch.touchdemo.MainActivity">
    <Button
            android:text="Hello World!"
            android:id="@+id/bt"
            android:textAllCaps="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    <ImageView
            android:text="Hello World!"
            android:id="@+id/iv"
            android:layout_below="@+id/bt"
            android:background="@mipmap/ic_launcher"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

</RelativeLayout>

首先為button設(shè)置點(diǎn)擊事件和OnTouch事件并返回false。

        btn = (Button) findViewById(R.id.bt);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.v("TAG","onClick execute!");
            }
        });
        btn.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View view, MotionEvent event) {
                // TODO Auto-generated method stub
                Log.v("TAG","onTouch execute,"+"action is "+ ViewTool.actionToString(event.getAction()));
                return false;
            }
        });

這時(shí)點(diǎn)擊button的打印信息

09-16 12:09:55.194 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
09-16 12:09:55.302 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_UP
09-16 12:09:55.302 1720-1720/touch.touchdemo V/TAG: onClick execute!

再把button的OnTouch事件的返回值設(shè)為true。

        btn = (Button) findViewById(R.id.bt);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.v("TAG","onClick execute!");
            }
        });
        btn.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View view, MotionEvent event) {
                // TODO Auto-generated method stub
                Log.v("TAG","onTouch execute,"+"action is "+ ViewTool.actionToString(event.getAction()));
                return true;
            }
        });

這時(shí)點(diǎn)擊button的打印信息

09-16 12:09:55.194 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
09-16 12:09:55.302 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_UP

發(fā)現(xiàn)點(diǎn)擊事件沒(méi)有執(zhí)行

onClick()方法沒(méi)有被執(zhí)行,這里我們把這種現(xiàn)象叫做點(diǎn)擊事件被onTouch()消費(fèi)掉了,事件不會(huì)在繼續(xù)向onClick()方法傳遞了

onTouch中返回了true時(shí)底層到底發(fā)生了什么?為什么在onTouch中返回了true,事件便不會(huì)繼續(xù)向下傳遞了?onTouch和onTouchEvent的區(qū)別到底在哪里?為了解決我們心中的疑惑,我們必須去深入分析相關(guān)的源代碼了。

補(bǔ)充知識(shí)點(diǎn):Android中所有的事件都必須經(jīng)過(guò)disPatchTouchEvent(MotionEvent ev)這個(gè)方法的分發(fā)。<br />
然后決定是自身消費(fèi)當(dāng)前事件還是繼續(xù)往下分發(fā)給子控件處理。<br />
那么我們看看這個(gè)view里面的disPatchTouchEvent(MotionEvent ev)方法<br />

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

代碼有點(diǎn)多,我們一步步來(lái)看:

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

最前面這一段就是判斷當(dāng)前事件是否能獲得焦點(diǎn),如果不能獲得焦點(diǎn)或者不存在一個(gè)View那我們就直接返回False跳出循環(huán),接下來(lái):

 if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

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

設(shè)置一些標(biāo)記和處理input與手勢(shì)等傳遞,不用管,到這里:

  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 (onFilterTouchEventForSecurity(event))是用來(lái)判斷View是否被遮住等,ListenerInfo是View的靜態(tài)內(nèi)部類,專門用來(lái)定義一些XXXListener等方法的,到了重點(diǎn):

 if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

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

設(shè)置一些標(biāo)記和處理input與手勢(shì)等傳遞,不用管,到這里:

            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }

很長(zhǎng)的一個(gè)判斷,一個(gè)個(gè)來(lái)解釋:第一個(gè)li肯定不為空,因?yàn)樵谶@個(gè)If判斷語(yǔ)句之前就new了一個(gè)li,第二個(gè)條件li.mOnTouchListener != null,怎么確定這個(gè)mOnTouchListener不為空呢?我們?cè)赩iew類里面發(fā)現(xiàn)了如下方法:

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

意味著只要給控件注冊(cè)了onTouch事件這個(gè)mOnTouchListener就一定會(huì)被賦值,接下來(lái)(mViewFlags & ENABLED_MASK) == ENABLED是通過(guò)位與運(yùn)算來(lái)判斷這個(gè)View是否是ENABLED的,我們默認(rèn)控件都是ENABLED的,所以這一條也成立;最后一條li.mOnTouchListener.onTouch(this, event)是判斷onTouch()的返回值是否為True,我們后面把默認(rèn)為False的返回值改成了True,所以這一整系列的判斷都是True,那么這個(gè)disPatchTouchEvent(MotionEvent ev)方法直接就返回了True,那么接下來(lái)的代碼都不會(huì)被執(zhí)行。<br />
這就解釋了上面為什么setOnTouchListener的毀掉onTouch返回true時(shí),onClick不執(zhí)行了。<br />

結(jié)合上面的代碼可以得到結(jié)論:<br />
<br />
1 . OnTouchListener的優(yōu)先級(jí)比onTouchEvent要高,聯(lián)想到剛才的小Demo也可以得出OnTouchListener 中的onTouch方法優(yōu)先于onClick()方法執(zhí)行(onClick()是在onTouchEvent(event)方法中被執(zhí)行的這個(gè)待會(huì)會(huì)說(shuō)到)
<br />
2 . 如果控件(View)的onTouch返回False或者mOnTouchListener為null(控件沒(méi)有設(shè)置setOnTouchListener方法)或者控件不是ENABLE的情況下會(huì)調(diào)用onTouchEvent方法,此時(shí)dispatchTouchEvent方法的返回值與onTouchEvent的返回值一樣。

繼續(xù)分析dispatchTouchEvent方法里面onTouchEvent的實(shí)現(xiàn)

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

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

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

代碼還是很多,我們依然一段一段來(lái)分析,最前面的一段代碼:

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

根據(jù)前面的分析我們知道這一段代碼是對(duì)當(dāng)前View處于不可用狀態(tài)的情況下的分析,通過(guò)注釋我們知道即使是一個(gè)不可用狀態(tài)下的View依然會(huì)消耗點(diǎn)擊事件,只是不會(huì)對(duì)這個(gè)點(diǎn)擊事件作出響應(yīng)罷了,另外通過(guò)觀察這個(gè)return返回值,只要這個(gè)View的CLICKABLE和LONG_CLICKABLE或者CONTEXT_CLICKABLE有一個(gè)為True,那么返回值就是True,onTouchEvent方法會(huì)消耗當(dāng)前事件。<br />

看下一段代碼:

if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

這段代碼的意思是如果View設(shè)置有代理,那么還會(huì)執(zhí)行TouchDelegate的onTouchEvent(event)方法,這個(gè)onTouchEvent(event)的工作機(jī)制看起來(lái)和OnTouchListener類似,這里不深入研究.<br />

下面看一下onTouchEvent中對(duì)點(diǎn)擊事件的具體處理流程:

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

我們還是一行行來(lái)分解:

  if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                     case MotionEvent.ACTION_UP:
                      ....
                      performClick();
                      ....
            //省略
            }
             return true;
    }
   return false;

這邊主要關(guān)注兩點(diǎn)

  1. 可點(diǎn)擊的view返回true,否則返回false
  2. 在 MotionEvent.ACTION_UP:中會(huì)進(jìn)行點(diǎn)擊事件判斷

performClick()源碼:

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

那就是當(dāng)ACTION_UP事件發(fā)生時(shí),會(huì)觸發(fā)performClick()方法,如果這個(gè)View設(shè)置了OnClickListener那么最終會(huì)執(zhí)行到OnClickListener的回調(diào)方法onClick(),這也就驗(yàn)證了剛才所說(shuō)的:onClick()方法是在onTouchEvent內(nèi)部被調(diào)用的。

繼續(xù):我們?yōu)閐emo中的imageView設(shè)置touch事件

        imageView  = (ImageView) findViewById(R.id.iv);
        imageView.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // TODO Auto-generated method stub
                Log.v("TAG","onTouch execute,"+"action is "+ViewTool.actionToString(event.getAction()));
                return false;
            }
        });

這時(shí)點(diǎn)擊imageView打印信息為:

09-16 12:09:55.194 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN

再為imageView增加點(diǎn)擊事件

 imageView  = (ImageView) findViewById(R.id.iv);
        imageView.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // TODO Auto-generated method stub
                Log.v("TAG","onTouch execute,"+"action is "+ViewTool.actionToString(event.getAction()));
                return false;
            }
        });
        imageView.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub
                Log.v("TAG","onClick execute!");
            }
        });

這時(shí)的單元信息為

09-16 13:03:14.682 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
09-16 13:03:14.782 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_UP
09-16 13:03:14.782 1720-1720/touch.touchdemo V/TAG: onClick execute!

為什么只設(shè)置setOnTouchListener時(shí)只相應(yīng)了 ACTION_DOWN,增加設(shè)置了setOnClickListener時(shí)ACTION_DOWN、ACTION_UP事件都得到相應(yīng)呢?

這邊補(bǔ)充一個(gè)android分發(fā)的重要知識(shí)點(diǎn):<br />
關(guān)于dispatchTouchEvent的返回<br />

  1. 當(dāng)我們給某個(gè)控件設(shè)置了Touch事件,當(dāng)點(diǎn)擊該控件時(shí),會(huì)觸發(fā)一系列的事件,如ACTION_DOWN,ACTION_MOVE,ACTION_UP。

  2. dispatchTouchEvent在進(jìn)行事件分發(fā)時(shí),如果某個(gè)ACTION返回了false,那么后面的ACTION都將得不到執(zhí)行。也就是說(shuō),只有前一個(gè)ACTION返回true,后一個(gè)的ACTION才會(huì)得到執(zhí)行。

當(dāng)imageView只設(shè)置setOnTouchListener事件時(shí):<br />
Imageview—不可點(diǎn)擊setOnTouchListener<br />
-- onTouchEvent返回false(上面有分析過(guò),不可點(diǎn)擊onTouchEvent返回false)<br />
-- dispatchTouchEvent返回false<br />
在ACTION_DOWN時(shí)dispatchTouchEvent返回了false。后續(xù)的ACTION得不到執(zhí)行。<br />

為什么設(shè)置了setOnClickListener后續(xù)的ACTION可以得到執(zhí)行呢?<br />

setOnClickListener的源碼:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

setOnClickListener方法,它先會(huì)去判斷當(dāng)前控件是否是Clickable的,如果不是Clickable的,則將當(dāng)前控件設(shè)置為Clickable的。當(dāng)我們調(diào)用了ImageView對(duì)象的setOnClickListener方法后,ImageView對(duì)象就已經(jīng)變成了Clickable的,所以其表現(xiàn)和Button一致也是自然的。

View總結(jié)

  1. onTouch和onTouchEvent都是在dispatchTouchEvent方法中被調(diào)用的方法。onTouch會(huì)優(yōu)先于onTouchEvent被執(zhí)行。

  2. 如果onTouch通過(guò)返回true將事件消費(fèi)掉,事件便不會(huì)傳遞到onTouchEvent中。特別要強(qiáng)調(diào)的一點(diǎn)是,只有當(dāng)mOnTouchListener不為null并且控件是enabled,onTouch方法才會(huì)得到執(zhí)行。

  3. dispatchTouchEvent在進(jìn)行事件分發(fā)時(shí),如果某個(gè)ACTION返回了false,那么后面的ACTION都將得不到執(zhí)行。

  4. setOnClickListener方法會(huì)設(shè)置view為可點(diǎn)擊。

接下來(lái)我們看 ViewGroup的事件分發(fā):

結(jié)合下面的布局:

<touch.touchdemo.widget.CustomLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:orientation="vertical"
        android:id="@+id/customLayout"
        tools:context="touch.touchdemo.MainActivity">




    <Button
            android:id="@+id/btn1"
            android:textAllCaps="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button1"
    />

    <Button
            android:id="@+id/btn2"
            android:textAllCaps="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button2"
    />
</touch.touchdemo.widget.CustomLayout>

CustomLayout 繼承LinearLayout:

public class CustomLayout extends LinearLayout {

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

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

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev){
        return false;
    }

}

為button1、button2設(shè)置點(diǎn)擊事件為 customLayout設(shè)置setOnTouchListener。

        customLayout.setOnTouchListener(new View.OnTouchListener(){

            @Override
            public boolean onTouch(View arg0, MotionEvent arg1) {
                Log.v("TAG","customLayout onTouch:"+ ViewTool.actionToString(arg1.getAction()));
                return false;
            }

        });
        btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.v("TAG","onClick execute!");
            }
        });

        btn2.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub
             

點(diǎn)擊buttion1打印信息:

09-16 13:35:08.242 24349-24349/touch.touchdemo V/TAG: onClick execute!

點(diǎn)擊buttion2打印信息:

09-16 13:35:27.438 24349-24349/touch.touchdemo V/TAG: onClick execute!

點(diǎn)擊空白地方打印信息:

09-16 13:35:53.670 24349-24349/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN

修改CustomLayout中onInterceptTouchEvent的返回值為true:

public class CustomLayout extends LinearLayout {

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

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

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev){
        return true;
    }

}

這時(shí)點(diǎn)擊button1

09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN

這時(shí)點(diǎn)擊button1

09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN

這時(shí)點(diǎn)擊空白地方

09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN

這是為什么呢?點(diǎn)擊事件得不到執(zhí)行了,只有ACTION_DOWN得到相應(yīng)。<br />

這需要分析下ViewGroup的dispatchTouchEvent。源碼比較長(zhǎng)這里就不貼出來(lái)了,有興趣的可以自己去看看。<br />
我們可以用一段偽代碼來(lái)說(shuō)明ViewGroup的dispatchTouchEvent主要作用

public boolean dispatchTouchEvent(MotionEvent e) {
    boolean consumed = false;
    if (onInterceptTouchEvent(e)) {
        consumed = onTouchEvent(e);
    } else {
        for (View view: childs) {
            consumed = view.dispatchTouchEvent(e);
            if (consumed) {
                break;
            }
        }
        if (!consumed) {
            consumed = onTouchEvent(e);
        }
    }    
    return consumed;
}
  1. 首先判斷ViewGroup的onInterceptTouchEvent是否攔截,如果攔截執(zhí)行自身的onTouchEvent
  2. 不攔截向下分發(fā)給自view去執(zhí)行。
  3. 如果子view中有相應(yīng)的處理(dispatchTouchEvent返回true),ViewGroup的dispatchTouchEvent返回true。
  4. 如果子view中沒(méi)有相應(yīng)的處理(dispatchTouchEvent返回flase),ViewGroup會(huì)再執(zhí)行自身的onTouchEvent。

我們可以用一張流程圖來(lái)說(shuō)明這個(gè)過(guò)程:<br />

Paste_Image.png

結(jié)合這張圖有興趣的同學(xué)可以跑下demo中的流程打印驗(yàn)證下。

結(jié)合上面說(shuō)的。我們來(lái)分析下ViewPager是怎么處理滑動(dòng)沖突的:

Viewpager套Viewpager時(shí)的事件處理<br />

Paste_Image.png

demo中我們簡(jiǎn)單的寫一個(gè)示例Viewpager套Viewpager如上圖<br />
可以看到滑動(dòng)點(diǎn)在里面的viewpager時(shí),里面的viewpager滑動(dòng),滑動(dòng)點(diǎn)在外面的viewpager只外面的viewpager滑動(dòng)。<br />

我們看下Viewpager中的onInterceptTouchEvent實(shí)現(xiàn):<br />
代碼很多,關(guān)鍵是在ACTION_MOVE時(shí),他是如果判斷攔截與不攔截(攔截返回true和不攔截false)<br />
找到關(guān)鍵代碼:

                if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
                        canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
                }

可以看出在viewpager的onInterceptTouchEvent的MotionEvent.ACTION_MOVE:<br />
會(huì)去判斷當(dāng)前顯示的頁(yè)面是否可以滑動(dòng),如果可以滑動(dòng),則將該事件丟給當(dāng)前顯示的頁(yè)面處理。<br />

這種攔截法叫做:外部攔截法

所謂外部攔截法是指所有的觸摸事件都會(huì)先經(jīng)過(guò)經(jīng)過(guò)父容器的傳遞,從而父容器在需要此觸摸事件的時(shí)候就可以攔截此觸摸事件,否者就傳遞給子View。這樣就可以解決滑動(dòng)沖突的問(wèn)題,這種方法比較符合觸摸事件的傳遞、處理機(jī)制。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在該方法中根據(jù)滑動(dòng)沖突處理規(guī)則做相應(yīng)的攔截即可。<br />
可用下面的偽代碼來(lái)表示:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
 boolean intercepted = false;
 int x = (int) event.getX();
 int y = (int) event.getY();

 switch (event.getAction()) {
 case MotionEvent.ACTION_DOWN: {
     intercepted = false;
     break;
 }
 case MotionEvent.ACTION_MOVE: {
     if (父容器需要當(dāng)前觸摸事件) {
         intercepted = true;
     } else {
         intercepted = false;
     }
     break;
 }
 case MotionEvent.ACTION_UP: {
     intercepted = false;
     break;
 }
 default:
     break;
 }
 mLastXIntercept = x;
 mLastYIntercept = y;
 return intercepted;
}

我們繼續(xù):
現(xiàn)在的代碼我們不兼容4.0以前的版本了,所有viewpager嵌套viewpager的實(shí)現(xiàn)簡(jiǎn)單了很多<br />

在 API13及前面的版本Viewpager 套Viewpager 直接寫存在兼容問(wèn)題。<br />

我可以通過(guò)源碼來(lái)看為什么存在兼容:

                if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
                        canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
                }

canScroll的實(shí)現(xiàn)

    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if (v instanceof ViewGroup) {
            final ViewGroup group = (ViewGroup) v;
            final int scrollX = v.getScrollX();
            final int scrollY = v.getScrollY();
            final int count = group.getChildCount();
            // Count backwards - let topmost views consume scroll distance first.
            for (int i = count - 1; i >= 0; i--) {
                // TODO: Add versioned support here for transformed views.
                // This will not work for transformed views in Honeycomb+
                final View child = group.getChildAt(i);
                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
                        canScroll(child, true, dx, x + scrollX - child.getLeft(),
                                y + scrollY - child.getTop())) {
                    return true;
                }
            }
        }

        return checkV && ViewCompat.canScrollHorizontally(v, -dx);
    }

ViewCompat.canScrollHorizontally的調(diào)用

    public static boolean canScrollHorizontally(View v, int direction) {
        return IMPL.canScrollHorizontally(v, direction);
    }

static final ViewCompatImpl IMPL;的實(shí)現(xiàn)

    static final ViewCompatImpl IMPL;
    static {
        final int version = android.os.Build.VERSION.SDK_INT;
        if (version >= 23) {
            IMPL = new MarshmallowViewCompatImpl();
        } else if (version >= 21) {
            IMPL = new LollipopViewCompatImpl();
        } else if (version >= 19) {
            IMPL = new KitKatViewCompatImpl();
        } else if (version >= 17) {
            IMPL = new JbMr1ViewCompatImpl();
        } else if (version >= 16) {
            IMPL = new JBViewCompatImpl();
        } else if (version >= 15) {
            IMPL = new ICSMr1ViewCompatImpl();
        } else if (version >= 14) {
            IMPL = new ICSViewCompatImpl();
        } else if (version >= 11) {
            IMPL = new HCViewCompatImpl();
        } else if (version >= 9) {
            IMPL = new GBViewCompatImpl();
        } else if (version >= 7) {
            IMPL = new EclairMr1ViewCompatImpl();
        } else {
            IMPL = new BaseViewCompatImpl();
        }
    }

可以找到api13以以下的canScrollHorizontally的實(shí)現(xiàn):

        public boolean canScrollHorizontally(View v, int direction) {
            return (v instanceof ScrollingView) &&
                canScrollingViewScrollHorizontally((ScrollingView) v, direction);
        }

這邊(v instanceof ScrollingView),因?yàn)関為viewpager,(v instanceof ScrollingView)為fasle,所有api13以以前的canScrollHorizontally反false,即沒(méi)有實(shí)現(xiàn)滑動(dòng)判斷,永遠(yuǎn)都是flase。<br />
可以找到api14以以上的canScrollHorizontally的實(shí)現(xiàn):

        public boolean canScrollHorizontally(View v, int direction) {
            return ViewCompatICS.canScrollHorizontally(v, direction);
        }

最終調(diào)用的是view.java中的

    public boolean canScrollHorizontally(int direction) {
        final int offset = computeHorizontalScrollOffset();
        final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
        if (range == 0) return false;
        if (direction < 0) {
            return offset > 0;
        } else {
            return offset < range - 1;
        }
    }

這里幫你做了是否可滑動(dòng)判斷。<br />
到這里我們就從源碼層面分析了Viewpager 套Viewpager 兼容問(wèn)題<br />

我們來(lái)兼容下:<br />

先介紹另一種滑動(dòng)沖突的解決方法<br />
內(nèi)部攔截法:<br />

  1. 內(nèi)部攔截法是指父容器不攔截任何觸摸事件,所有的觸摸事件都傳遞給子元素,如果子元素需要此觸摸事件就直接消耗掉,否者就交由父容器進(jìn)行處理,(通過(guò)內(nèi)部子元素來(lái)進(jìn)行是否進(jìn)行攔截)這種方法和Android中的事件傳遞、處理機(jī)制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起來(lái)較外部攔截法稍顯復(fù)雜。這種方法需要重寫子元素的dispatchTouchEvent方法。
  2. 子 View 可以使用 requestDisallowInterceptTouchEvent 影響去父 View 的分發(fā),可以決定父 View 是否要調(diào)用 onInterceptTouchEvent 。比如,requestDisallowInterceptTouchEvent(true),父 View 就不用調(diào)用 onInterceptTouchEvent 來(lái)判斷攔截,而就是不攔截。
    用偽代碼表示為:

子元素的dispatchTouchEvent方法中<br />

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
 int x = (int) event.getX();
 int y = (int) event.getY();

 switch (event.getAction()) {
 case MotionEvent.ACTION_DOWN: {
     getParent().requestDisallowInterceptTouchEvent(true);
     break;
 }
 case MotionEvent.ACTION_MOVE: {
     int deltaX = x - mLastX;
     int deltaY = y - mLastY;
     if (父容器需要當(dāng)前觸摸事件) {
         getParent().requestDisallowInterceptTouchEvent(false);
     }
     break;
 }
 case MotionEvent.ACTION_UP: {
     break;
 }
 default:
     break;
 }

 mLastX = x;
 mLastY = y;
 return super.dispatchTouchEvent(event);
}

在demo代碼中的的實(shí)現(xiàn)為:

public class ViewPagerCompat2 extends ViewPager {

    /** 觸摸時(shí)按下的點(diǎn) **/
    PointF downP = new PointF();
    /** 觸摸時(shí)當(dāng)前的點(diǎn) **/
    PointF curP = new PointF();
    private int first = 1;
    private float mLastMotionX;
    private float mLastMotionY;
    public ViewPagerCompat2(Context context) {
        super(context);
    }

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final float x = ev.getX();
        final float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);//告訴父view不攔截
                first = 1;
                mLastMotionX = x;
                mLastMotionY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (first == 1) {
                    if (Math.abs(x - mLastMotionX) < Math.abs(y - mLastMotionY)) {
                        first = 0;//y軸滑動(dòng)攔截
                        getParent().requestDisallowInterceptTouchEvent(false);

                    } else {
                        //x軸滑動(dòng)不攔截
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

上面這種內(nèi)部攔截法。當(dāng)然兼容也可以使用外部攔截法:<br />

既然ViewPager在API14以上可以正?;瑒?dòng)重寫了canScrollHorizontally(int)方法,查看ViewPager的canScrollHorizontally(int)方法源碼發(fā)現(xiàn)此方法不存在版本兼容問(wèn)題,在API13及其以下版本上也可直接調(diào)用。于是乎解決辦法就是繼承ViewPager重寫canScroll(View, boolean, int, int, int)方法,直接調(diào)用canScrollHorizontally(int)即可,如下:

public class ViewPagerCompat extends ViewPager {
    public ViewPagerCompat(Context context) {
        super(context);
    }

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

    @Override
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if(v instanceof ViewGroup){
            final ViewGroup group = (ViewGroup) v;
            final int scrollX = v.getScrollX();
            final int scrollY = v.getScrollY();
            final int count = group.getChildCount();
            // Count backwards - let topmost views consume scroll distance first.
            for (int i = count - 1; i >= 0; i--) {
                // TODO: Add versioned support here for transformed views.
                // This will not work for transformed views in Honeycomb+
                final View child = group.getChildAt(i);
                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
                        canScroll(child, true, dx, x + scrollX - child.getLeft(),
                                y + scrollY - child.getTop())) {
                    return true;
                }
            }
        }

        if(checkV){
            // Direct call ViewPager.canScrollHorizontally(int)
            if(v instanceof ViewPager){
                return ((ViewPager) v).canScrollHorizontally(-dx);
            }else{
                return ViewCompat.canScrollHorizontally(v, -dx);
            }
        }else{
            return false;
        }
    }
}

<br />
<br />
<br />
<br />

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

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

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