Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> Android View事件分發機制

Android View事件分發機制

編輯:關於android開發

Android View事件分發機制


最近在開發中遇到view滑動沖突的問題,由於一開始就知道這個問題與view事件分發有關,之後在網上看了幾篇關於事件分發的資料後,開發中遇到的問題很快便得到解決。
在這裡總結一下我對view事件分發的理解。

首先,看下事件分發流程圖:
這裡寫圖片描述

Button事件演示

在對view的事件分發機制進行分析前,我們可以通過一個demo看看Button的事件處理的流程。
在布局文件中添加一個button控件,然後在代碼中實現Button的setOnClickListener和setOnTouchListener方法,注冊Click監聽和Touch監聽。

/**
 * button事件
 */
private void showButtonTouch() {
    mBtn = (Button) findViewById(R.id.btn);
    mBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.e(TAG, "Button onClick");
        }
    });
    mBtn.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    Log.e(TAG, "Button onTouch " + "ACTION_UP");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG, "Button onTouch " + "ACTION_MOVE");
                    break;
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG, "Button onTouch " + "ACTION_DOWN");
                    break;
            }

            return false;
        }
    });
}



    

demo運行起來之後點擊button,看下log日志(在點擊時移動一下保證ACTION_MOVE事件能被觸發):
這裡寫圖片描述

在這裡通過日志可以看出事件處理的流程。

View事件分發源碼解析

在View的源碼中,我們可以看到dispatchTouchEvent方法,這個方法可以理解為是View事件分發的入口。(代碼基於4.0.3即API 15,建議大家在理解源碼時不要看太高的版本,高版本源碼會有過多的優化,會妨礙我們對於代碼主要功能邏輯的理解

dispatchTouchEvent方法解析

以下是View中dispatchTouchEvent方法代碼:

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement

        //這個是View事件分發的主要邏輯判斷
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            return true;
        }

        if (onTouchEvent(event)) {
            return true;
        }
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    return false;
}

在這整個方法中,事件分發的關鍵就在這個判斷中 (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))

如果這個判斷為true,那麼該方法返回true;否則,執行onTouchEvent()方法並根據onTouchEvent方法的返回值返回具體結果。

li != null:代碼中ListenerInfo對象li,是View內定義的一個靜態類,這個類內部定義了View中所有Listener相關的類,一般情況下這個類不為空,在這裡不做過多解釋(2.3版本源碼中沒有該對象)。

li.mOnTouchListener != null:是否為空,mOnTouchListener 對象就是我們通過mBtn.setOnTouchListener(new View.OnTouchListener() {})設置的,所以這裡不為空。

(mViewFlags & ENABLED_MASK) == ENABLED:判斷控件是否是enable,很顯然為true。

li.mOnTouchListener.onTouch(this, event):最後就是判斷onTouch方法中返回值是否為true,onTouch方法就是我們在Button控件注冊Touch監聽時@Override的onTouch方法。

在前面的demo中,因為mOnTouchListener.onTouch(this, event)方法的返回值為false,我們在log中看到button先執行touch事件在執行click事件。在這裡我們將onTouch方法的返回值改為true,可以看到以下log:
這裡寫圖片描述

在log中可以看到click方法沒有被執行,這又是為什麼呢?

其實在這裡大家通過閱讀dispatchTouchEvent方法代碼可以想到,因為onTouch返回值為true,所以這個判斷條件成立即dispatchTouchEvent方法返回true。if (onTouchEvent(event))->onTouchEvent方法沒有被執行,會不會是由於這個原因導致click方法沒有被執行呢? 很顯然我們要看看onTouchEvent方法的源碼。

onTouchEvent源碼解析:

以下是onTouchEvent方法的源碼:

public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
            mPrivateFlags &= ~PRESSED;
            refreshDrawableState();
        }
        // 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));
    }

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


    //這是onTouchEvent代碼主要邏輯功能
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                if ((mPrivateFlags & 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.
                        mPrivateFlags |= PRESSED;
                        refreshDrawableState();
                   }

                    if (!mHasPerformedLongPress) {
                        // 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();
                }
                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 |= PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    mPrivateFlags |= PRESSED;
                    refreshDrawableState();
                    checkForLongClick(0);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
                removeTapCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                final int x = (int) event.getX();
                final int y = (int) event.getY();

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();

                        // Need to switch from pressed to not pressed
                        mPrivateFlags &= ~PRESSED;
                        refreshDrawableState();
                    }
                }
                break;
        }
        return true;
    }

    return false;
}

對於onTouchEvent方法,主要的功能邏輯我們只需要從第23行代碼開始閱讀便可。
首先判斷View是否是可點擊的,因為Button默認是可點擊的,所以這個條件成立,我們可以進入到switch分支判斷中。在這裡給大家留下一個疑問,如果這個條件不成立即就是控件是不可點擊的,會出現什麼樣的情況呢?

MotionEvent.ACTION_UP:在這個分支判斷中我們可以看到在58行有個performClick()方法,我們進入到這個方法體中可以看到:

public boolean performClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        return true;
    }

    return false;
}

在該方法中可以看到li.mOnClickListener.onClick(this)被調用了,到此處我們可以確認Button的click事件就是在onTouchEvent方法中調用的

TextView和Button比較

在onTouchEvent中曾留下一個疑問,如果判斷控件是否可點擊為false,會出現什麼樣的情況呢?

為了驗證這個問題我們可以通過TextView和Button進行比較一下,因為TextView默認不可點擊,Button默認可點擊的。

首先在xml中添加TextView和Button,在代碼中分別為他們注冊setOnTouchListener監聽,代碼如下:




    
/**
 * button事件
 */
private void showButtonTouch() {
    mBtn = (Button) findViewById(R.id.btn);
    mBtn.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    Log.e(TAG, "Button onTouch " + "ACTION_UP");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG, "Button onTouch " + "ACTION_MOVE");
                    break;
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG, "Button onTouch " + "ACTION_DOWN");
                    break;
            }

            return false;
        }
    });
}
/**
 * TextView事件
 */
private void showTextTouch() {
    mTv = (TextView) findViewById(R.id.tv);
    mTv.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    Log.e(TAG, "TextView onTouch " + "ACTION_UP");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG, "TextView onTouch " + "ACTION_MOVE");
                    break;
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG, "TextView onTouch " + "ACTION_DOWN");
                    break;
            }
            return false;
        }
    });
}

因為我們需要進入到onTouchEvent方法中,所以必須讓setOnTouchListener.onTouch()方法的返回值為false,在這裡我們看看demo運行起來後分別點擊Button和TextView打印的log:
這裡寫圖片描述系統會認為ACTION_DOWN事件沒有被執行完成,那麼其他的touch事件就不能被觸發了。

現在大家再去看事件分發流程圖中的View部分想必就能看懂了吧!!!

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