Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android事件分發機制源碼分析之View篇

Android事件分發機制源碼分析之View篇

編輯:關於Android編程

對於Android事件分發機制,我們在開發的過程中,肯定曾經遇到在最外層添加了ScrollView之後ListView無法正常滑動、我們的圖片輪播在左右滑動圖片為什麼感覺很難控制。這些都是我們用戶在屏幕上進行交互的一系列操作,因此深入了解Android事件分發機制是非常的重要。

事件分發的概念

所謂點擊事件的事件分發,就是當一個MotionEvent產生了以後,系統需要把這個事件傳遞給一個具體的View(ViewGroup也繼承於View),這個傳遞的過程就叫做分發過程。Android的觸摸事件分發傳遞過程中,最重要的是可以分為:

Activity

dispatchTouchEvent(MotionEvent) //事件分開 onTouchEvent(MotionEvent) //事件處理

ViewGroup

dispatchTouchEvent(MotionEvent) onInterceptTouchEvent(MotionEvent) //事件攔截 onTouchEvent(MotionEvent)

View

dispatchTouchEvent(MotionEvent) onTouchEvent(MotionEvent)

然而在用戶點下屏幕之後,我們通過下面這張圖對觸摸事件要有一個整體的了解。

這裡寫圖片描述

示例

大家在有了整體了解之後,我們今次主要分析的是View的事件分發。
我們通過下面簡單的代碼來了解一下。我們自定以一個CustomButton繼承Button,然後把跟View的事件傳播有關的方法進行復寫,然後再Log打印下。
CustomButton:我們重寫一下onTouchEvent和dispatchTouchEvent方法並打印。

public class CustomButton extends Button {
    private static final String TAG = "CustomButton";

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "onTouchEvent ACTION_UP");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(TAG, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "dispatchTouchEvent ACTION_UP");
                break;

            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }
}

簡單看一下我們的布局文件:




    

最後是我們的MainActivity代碼:

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";
    private CustomButton mbtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mbtn = (CustomButton) findViewById(R.id.button);
        mbtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick Event");
            }
        });
        mbtn.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        Log.i(TAG, "onTouch ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.i(TAG, "onTouch ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.i(TAG, "onTouch ACTION_UP");
                        break;
                    default:
                        break;
                }

                return false;
            }
        });
    }

}

好了上面就是大致的代碼,我們接著編譯運行一下,看看打印的結果:

10-27 14:03:05.264 14668-14668/com.example.pc.myapplication I/CustomButton: dispatchTouchEvent ACTION_DOWN
10-27 14:03:05.264 14668-14668/com.example.pc.myapplication I/MainActivity: onTouch ACTION_DOWN
10-27 14:03:05.264 14668-14668/com.example.pc.myapplication I/CustomButton: onTouchEvent ACTION_DOWN
10-27 14:03:05.273 14668-14668/com.example.pc.myapplication I/CustomButton: dispatchTouchEvent ACTION_MOVE
10-27 14:03:05.273 14668-14668/com.example.pc.myapplication I/MainActivity: onTouch ACTION_MOVE
10-27 14:03:05.273 14668-14668/com.example.pc.myapplication I/CustomButton: onTouchEvent ACTION_MOVE
10-27 14:03:05.308 14668-14668/com.example.pc.myapplication I/CustomButton: dispatchTouchEvent ACTION_UP
10-27 14:03:05.308 14668-14668/com.example.pc.myapplication I/MainActivity: onTouch ACTION_UP
10-27 14:03:05.308 14668-14668/com.example.pc.myapplication I/CustomButton: onTouchEvent ACTION_UP
10-27 14:03:05.326 14668-14668/com.example.pc.myapplication I/MainActivity: onClick EVENT

從打印結果來看,無論是DOWN,MOVE,UP的動作,執行的步驟都是
1. dispatchTouchEvent
2. setOnTouchListener的onTouch
3. onTouchEvent

分析

那我們就根據打印來看一下dispatchTouchEvent的源碼。我們在CustomButton類中按CTRL+O,搜索dispatchTouchEvent方法,你會發現dispatchTouchEvent()是直接指向View類中的dispatchTouchEvent(),這是因為Button類沒有覆寫dispatchTouchEvent(),Button類繼承TextView類,而TextView類也沒有覆寫dispatchTouchEvent(),最後是TextView類繼承View類,所以我們CustomButton覆寫的dispatchTouchEvent()直接指向它的父類View中。(可能有同學產生疑問雖然是先執行dispatchTouchEvent(),但是為什麼呢?還會有Activity–>ViewGroup–>View這樣分發下來,那麼Activity又是怎麼執行到dispatchTouchEvent()的呢?這部分的問題不是我們這篇討論分析的范圍,我們先定性理解先執行dispatchTouchEvent()。)

    public boolean dispatchTouchEvent(MotionEvent event) {
        ···

        boolean result = false;

        ···

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

        ···

        return result;
    }

我們直接看重點部分,判斷if(onFilterTouchEventForSecurity(event)),這個主要是判斷當前事件到來的時候,窗口有沒有被遮擋,如果被遮擋則會直接返回false。接著是將mListenerInfo賦給ListenerInfo的li對象,ListenerInfo是什麼呢?

static class ListenerInfo {
        ···
        public OnClickListener mOnClickListener;

        protected OnLongClickListener mOnLongClickListener;


        protected OnContextClickListener mOnContextClickListener;

        protected OnCreateContextMenuListener mOnCreateContextMenuListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;
        ···
}

是我們平時用到的一些監聽,然而mListenerInfo又是從哪裡賦值的?繼承找

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
    /**
     * 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;
    }

從給出的注釋就可以理解得到,當我們在視圖中設置一個setOnTouchListener(),那麼就會注冊一個回調函數調用。
接著往下有四個條件判斷:li != null ; li.mOnTouchListener != null; (mViewFlags & ENABLED_MASK) == ENABLED; li.mOnTouchListener.onTouch(this, event)

第一個li != null,如果我們設置了setOnTouchListener(),在ListenerInfo li = mListenerInfo;中就會被賦值。

第二個li.mOnTouchListener != null,在setOnTouchListener()源碼中可以看到賦值。不為空。

第三個(mViewFlags & ENABLED_MASK) == ENABLED,是判斷當前點擊的控件是否是enable的,按鈕默認都是enable的,因此這個條件恆定為true。

第四個,mOnTouchListener.onTouch(this, event),其實也就是去回調控件注冊touch事件時的onTouch方法的返回值,默認值是返回false。

上面的四個判斷條件都成立整個方法賦值返回true。有一個不成立都會賦值返回false。繼續接下下面的判斷。!result && onTouchEvent(event)。

第一個根據上面的判斷返回值result,如果result為false才會接著執行onTouchEvent(event)。

小總結:上面的源碼分析,證明了之前的打印信息順序。先執行dispatchTouchEvent(),然後執行mOnTouchListener.onTouch(this, event)的回調返回,滿足條件之後再調用onTouchEvent()。

我們緊接著看一下onTouchEvent(event)源碼:
由於源碼較長,這裡分段來講述。

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

如果當前View是Disabled不可用狀態狀態且是可點擊則會消費掉事件(return true);

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

如果設置了mTouchDelegate,則會將事件交給代理者處理,直接return true;

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
            ···

            return true;
        }

        return false;

在一開始的判斷滿足其中一個情況就返回true,否則返回false。接下就是我們事件分發的重點。

MotionEvent.ACTION_DOWN

                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;

設置mHasPerformedLongPress=false;表示長按事件還未觸發;
isInScrollingContainer判斷上一層結構,判斷是否在一個滾動的容器中;
如果在滾動容器中會做一個短延遲,區分滾動還是長按,接著最終的方法都是差不多,記錄橫縱坐標和檢測長按監聽。

MotionEvent.ACTION_MOVE

                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;

檢測橫縱坐標的變化傳遞給畫板或者子視圖;
判斷觸摸點有沒有移出我們的View,如果移出了:
執行removeTapCallback();
然後判斷是否包含PRESSED標識,如果包含,移除長按的檢查:removeLongPressCallback();

其實ACTION_DOWN與ACTION_MOVE都進行了一些必要的設置與置位,重點是ACTION_UP。

MotionEvent.ACTION_UP

                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;

首先判斷了是否被按下 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;接下來判斷是不是可以獲得焦點,同時嘗試去獲取焦點;再處理點擊下顯示效果;清除長按回調。經過上述的種種判斷之後我們重點看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;
    }

終於到看到li.mOnClickListener.onClick(),這裡檢測了當前View是否設置了onClickListener,如果設置了那麼回調它的onClick方法,所以驗證了我們一開始打印的數據,onClick()在onTouch()的後面執行,因為onClick()方法是在onTouchEvent內部被調用的。
接下來的是我們處理完performClick()後的一些狀態標識、狀態的改變和回調的操作。

總結:經過一系列的源碼,我們了解到dispatchTouchEvent()、onTouch()、onTouchEvent()。在View的dispatchTouchEvent中調用onTouch()和onTouchEvent(),onTouch優先於onTouchEvent執行。如果在onTouch方法中通過返回true將事件消費掉,onTouchEvent將不會再執行。onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能為空,第二當前點擊的控件必須是enable的(ImageView、TextView非enable)。因此如果你有一個控件是非enable的,那麼給它注冊onTouch事件將永遠得不到執行。

但是,我們上述說的只是事件分發的事件流向分發部分,我們將在下一篇事件分發之ViewGroup篇來理解事件分發另一個重點————事件攔截。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved