Android焦點事件分發與傳遞機制

如果您對TouchEvent事件分發機制不太了解的,可以參考我的這篇文章——安卓TounchEvent事件分發機制。

問題:TV端焦點滿天飛,如何解決和處理?

記得初入TV開發,以為很簡單。TV的這些界面與布局太簡單了,分分鐘就可以把頁面搭建出來,處理好,然后就沒有然后了。。。。

下面我們就從源碼來帶大家進行安卓TV焦點事件的傳遞

這里先給出Android系統View的繪制流程:

依次執行View類里面的如下三個方法:

  • measure(int ,int) :測量View的大小
  • layout(int ,int ,int ,int) :設置子View的位置
  • draw(Canvas) :繪制View內容到Canvas畫布上

ViewRootImpl的主要作用如下(此處不多講,如有意圖,看源碼):

  • A:鏈接WindowManager和DecorView的紐帶,更廣一點可以說是Window和View之間的紐帶。

  • B:完成View的繪制過程,包括measure、layout、draw過程。

  • C:向DecorView分發收到的用戶發起的event事件,如按鍵觸屏等事件。

ViewRootImpl不再多余敘述,進入正題:

Android焦點分發的主要方法以及攔截方法的講解。

在RootViewImpl中的函數通道是各種策略(InputStage)的組合,各策略負責的任務不同,如SyntheticInputStage、ViewPostImeInputStage、NativePostImeInputStage等等,這些策略以鏈表結構結構起來,當一個策略者沒有消費事件時,就傳遞個下一個策略者。其中觸摸和按鍵事件由ViewPostImeInputStage處理。

 @Override
        protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                return processKeyEvent(q);//如果是按鍵事件走此處,處理按鍵和焦點問題了
            } else {
                final int source = q.mEvent.getSource();
                if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                    return processPointerEvent(q);//如果是觸摸事件走此處
                } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                    return processTrackballEvent(q);
                } else {
                    return processGenericMotionEvent(q);
                }
            }
        }

processKeyEvent(QueuedInputEvent q)源碼如下:

   @Override
        protected void onDeliverToNext(QueuedInputEvent q) {
            if (mUnbufferedInputDispatch
                    && q.mEvent instanceof MotionEvent
                    && ((MotionEvent)q.mEvent).isTouchEvent()
                    && isTerminalInputEvent(q.mEvent)) {
                mUnbufferedInputDispatch = false;
                scheduleConsumeBatchedInput();
            }
            super.onDeliverToNext(q);
        }

        private int processKeyEvent(QueuedInputEvent q) {
            final KeyEvent event = (KeyEvent)q.mEvent;

            // Deliver the key to the view hierarchy.
            if (mView.dispatchKeyEvent(event)) {
                return FINISH_HANDLED;
            }

            if (shouldDropInputEvent(q)) {
                return FINISH_NOT_HANDLED;
            }

            // If the Control modifier is held, try to interpret the key as a shortcut.
            if (event.getAction() == KeyEvent.ACTION_DOWN
                    && event.isCtrlPressed()
                    && event.getRepeatCount() == 0
                    && !KeyEvent.isModifierKey(event.getKeyCode())) {
                if (mView.dispatchKeyShortcutEvent(event)) {
                    return FINISH_HANDLED;
                }
                if (shouldDropInputEvent(q)) {
                    return FINISH_NOT_HANDLED;
                }
            }

            // Apply the fallback event policy.
            if (mFallbackEventHandler.dispatchKeyEvent(event)) {
                return FINISH_HANDLED;
            }
            if (shouldDropInputEvent(q)) {
                return FINISH_NOT_HANDLED;
            }

            // Handle automatic focus changes.
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                int direction = 0;
                switch (event.getKeyCode()) {
                    case KeyEvent.KEYCODE_DPAD_LEFT:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_LEFT;
                        }
                        break;
                    case KeyEvent.KEYCODE_DPAD_RIGHT:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_RIGHT;
                        }
                        break;
                    case KeyEvent.KEYCODE_DPAD_UP:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_UP;
                        }
                        break;
                    case KeyEvent.KEYCODE_DPAD_DOWN:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_DOWN;
                        }
                        break;
                    case KeyEvent.KEYCODE_TAB:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_FORWARD;
                        } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
                            direction = View.FOCUS_BACKWARD;
                        }
                        break;
                }
                if (direction != 0) {
                    View focused = mView.findFocus();
                    if (focused != null) {
                        View v = focused.focusSearch(direction);
                        if (v != null && v != focused) {
                            // do the math the get the interesting rect
                            // of previous focused into the coord system of
                            // newly focused view
                            focused.getFocusedRect(mTempRect);
                            if (mView instanceof ViewGroup) {
                                ((ViewGroup) mView).offsetDescendantRectToMyCoords(
                                        focused, mTempRect);
                                ((ViewGroup) mView).offsetRectIntoDescendantCoords(
                                        v, mTempRect);
                            }
                            if (v.requestFocus(direction, mTempRect)) {
                                playSoundEffect(SoundEffectConstants
                                        .getContantForFocusDirection(direction));
                                return FINISH_HANDLED;
                            }
                        }

                        // Give the focused view a last chance to handle the dpad key.
                        if (mView.dispatchUnhandledMove(focused, direction)) {
                            return FINISH_HANDLED;
                        }
                    } else {
                        // find the best view to give focus to in this non-touch-mode with no-focus
                        View v = focusSearch(null, direction);
                        if (v != null && v.requestFocus(direction)) {
                            return FINISH_HANDLED;
                        }
                    }
                }
            }
            return FORWARD;
        }

進入源碼講解:

(1) 首先由dispatchKeyEvent進行焦點的分發

如果dispatchKeyEvent方法返回true,那么下面的焦點查找步驟就不會繼續了。

dispatchKeyEvent方法返回true代表事件(包括焦點和按鍵)被消費了。

dispatchKeyEvent(event)如果不了解,看我上一篇文章安卓TounchEvent事件分發機制。

mView的dispatchKeyEvent方法,
mView是是Activity的頂層容器DecorView,它是一FrameLayout

所以這里的dispatchKeyEvent方法應該執行的是ViewGroup的dispatchKeyEvent()方法,而不是View的dispatchKeyEvent方法。

 @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onKeyEvent(event, 1);
        }

        if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
                == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
            if (super.dispatchKeyEvent(event)) {
                return true;
            }
        } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
                == PFLAG_HAS_BOUNDS) {
            if (mFocused.dispatchKeyEvent(event)) {
                return true;
            }
        }

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

ViewGroup的dispatchKeyEvent簡略執行流程

首先ViewGroup會執行父類的dispatchKeyEvent方法,如果返回true那么父類的dispatchKeyEvent方法就會返回true,也就代表父類消費了該焦點事件,那么焦點事件自然就不會往下進行分發

然后ViewGroup會判斷mFocused這個view是否為空如果為空就會****return false,焦點繼續往下傳遞;如果不為空,那就會return mFocused的dispatchKeyEvent方法返回的結果。這個mFocused是ViewGroup中當前獲取焦點的子View,這個可以從requestChildFocus方法中得到答案。

requestChildFocus()的源碼如下:

   @Override
    public void requestChildFocus(View child, View focused) {
        if (DBG) {
            System.out.println(this + " requestChildFocus()");
        }
        if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
            return;
        }

        // Unfocus us, if necessary
        super.unFocus(focused);

        // We had a previous notion of who had focus. Clear it.
        if (mFocused != child) {
            if (mFocused != null) {
                mFocused.unFocus(focused);
            }

            mFocused = child;
        }
        if (mParent != null) {
            mParent.requestChildFocus(this, focused);
        }
    }

居然有這個彩蛋?

View的dispatchKeyEvent簡略執行流程

public boolean dispatchKeyEvent(KeyEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onKeyEvent(event, 0);
        }

        // Give any attached key listener a first crack at the event.
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
            return true;
        }

        if (event.dispatch(this, mAttachInfo != null
                ? mAttachInfo.mKeyDispatchState : null, this)) {
            return true;
        }

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

要修改ViewGroup焦點事件的分發:

  • 重寫view的dispatchKeyEvent方法
  • 給某個子view設置onKeyListener監聽

焦點沒有被dispatchKeyEvent攔截的情況下的繼續代碼中的處理過程,還是進入ViewRootImpl源碼

            // Handle automatic focus changes.
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                int direction = 0;
                switch (event.getKeyCode()) {
                    case KeyEvent.KEYCODE_DPAD_LEFT:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_LEFT;
                        }
                        break;
                    case KeyEvent.KEYCODE_DPAD_RIGHT:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_RIGHT;
                        }
                        break;
                    case KeyEvent.KEYCODE_DPAD_UP:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_UP;
                        }
                        break;
                    case KeyEvent.KEYCODE_DPAD_DOWN:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_DOWN;
                        }
                        break;
                    case KeyEvent.KEYCODE_TAB:
                        if (event.hasNoModifiers()) {
                            direction = View.FOCUS_FORWARD;
                        } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
                            direction = View.FOCUS_BACKWARD;
                        }
                        break;
                }
                if (direction != 0) {
                    View focused = mView.findFocus();
                    if (focused != null) {
                        View v = focused.focusSearch(direction);
                        if (v != null && v != focused) {
                            // do the math the get the interesting rect
                            // of previous focused into the coord system of
                            // newly focused view
                            focused.getFocusedRect(mTempRect);
                            if (mView instanceof ViewGroup) {
                                ((ViewGroup) mView).offsetDescendantRectToMyCoords(
                                        focused, mTempRect);
                                ((ViewGroup) mView).offsetRectIntoDescendantCoords(
                                        v, mTempRect);
                            }
                            if (v.requestFocus(direction, mTempRect)) {
                                playSoundEffect(SoundEffectConstants
                                        .getContantForFocusDirection(direction));
                                return FINISH_HANDLED;
                            }
                        }

                        // Give the focused view a last chance to handle the dpad key.
                        if (mView.dispatchUnhandledMove(focused, direction)) {
                            return FINISH_HANDLED;
                        }
                    } else {
                        // find the best view to give focus to in this non-touch-mode with no-focus
                        View v = focusSearch(null, direction);
                        if (v != null && v.requestFocus(direction)) {
                            return FINISH_HANDLED;
                        }
                    }
                }
            }

dispatchKeyEvent方法返回false后,先得到按鍵的方向direction一個int值。direction值是后面來進行焦點查找的。

接著會調用DecorView的findFocus()方法一層一層往下查找已經獲取焦點的子View。

DecorView則是PhoneWindow類的一個內部類,繼承于FrameLayout,由此可知它是一個ViewGroup。

那么,DecroView到底充當了什么樣的角色呢?

其實,DecorView是整個ViewTree的最頂層View,它是一個FrameLayout布局,代表了整個應用的界面。在該布局下面,有標題view內容view這兩個子元素。

 @Override
    public View findFocus() {
        if (DBG) {
            System.out.println("Find focus in " + this + ": flags="
                    + isFocused() + ", child=" + mFocused);
        }

        if (isFocused()) {
            return this;
        }

        if (mFocused != null) {
            return mFocused.findFocus();
        }
        return null;
    }

View的findFocus方法

 /**
     * Find the view in the hierarchy rooted at this view that currently has
     * focus.
     *
     * @return The view that currently has focus, or null if no focused view can
     *         be found.
     */
    public View findFocus() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
    }

View的hasFocus()方法和isFocused()方法對比

Stackoverflow解釋來了:

hasFocus() is different from isFocused(). hasFocus() == true means that the View or one of its descendants is focused. If you look closely, there's a chain of hasFocused Views till you reach the View that isFocused.

 /**
     * Returns true if this view has focus itself, or is the ancestor of the
     * view that has focus.
     *
     * @return True if this view has or contains focus, false otherwise.
     */
    @ViewDebug.ExportedProperty(category = "focus")
    public boolean hasFocus() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0;
    }

  /**
     * Returns true if this view has focus
     *
     * @return True if this view has focus, false otherwise.
     */
    @ViewDebug.ExportedProperty(category = "focus")
    public boolean isFocused() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0;
    }

接著,如果mView.findFocus()方法返回的mFocused不為空,說明找到了當前獲取焦點的view(mFocused),接著focusSearch會把direction(遙控器按鍵按下的方向)作為參數,找到特定方向下一個將要獲取焦點的view,最后如果該view不為空,那么就讓該view獲取焦點。

我們來看一下focusSearch方法的源碼以及具體實現。

 @Override
    public View focusSearch(View focused, int direction) {
        if (isRootNamespace()) {
            // root namespace means we should consider ourselves the top of the
            // tree for focus searching; otherwise we could be focus searching
            // into other tabs.  see LocalActivityManager and TabHost for more info
            return FocusFinder.getInstance().findNextFocus(this, focused, direction);
        } else if (mParent != null) {
            return mParent.focusSearch(focused, direction);
        }
        return null;
    }

focusSearch其實是一層一層地網上調用父View的focusSearch方法,直到當前view是根布局(isRootNamespace()方法),通過注釋可以知道focusSearch最終會調用DecorView的focusSearch方法。而DecorView的focusSearch方法找到的焦點view是通過FocusFinder來找到的。

FocusFinder是什么?

根據給定的按鍵方向,通過當前的獲取焦點的View,查找下一個獲取焦點的view這樣算法的類。焦點沒有被攔截的情況下,Android焦點的查找最終都是通過FocusFinder類來實現的。

FocusFinder是如何通過findNextFocus方法尋找焦點的?

 public final View findNextFocus(ViewGroup root, View focused, int direction) {
        return findNextFocus(root, focused, null, direction);
    }
    
     private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        View next = null;
        if (focused != null) {
            next = findNextUserSpecifiedFocus(root, focused, direction);
        }
        if (next != null) {
            return next;
        }
        ArrayList<View> focusables = mTempList;
        try {
            focusables.clear();
            root.addFocusables(focusables, direction);
            if (!focusables.isEmpty()) {
                next = findNextFocus(root, focused, focusedRect, direction, focusables);
            }
        } finally {
            focusables.clear();
        }
        return next;
    }
    
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
            int direction, ArrayList<View> focusables) {
        if (focused != null) {
            if (focusedRect == null) {
                focusedRect = mFocusedRect;
            }
            // fill in interesting rect from focused
            focused.getFocusedRect(focusedRect);
            root.offsetDescendantRectToMyCoords(focused, focusedRect);
        } else {
            if (focusedRect == null) {
                focusedRect = mFocusedRect;
                // make up a rect at top left or bottom right of root
                switch (direction) {
                    case View.FOCUS_RIGHT:
                    case View.FOCUS_DOWN:
                        setFocusTopLeft(root, focusedRect);
                        break;
                    case View.FOCUS_FORWARD:
                        if (root.isLayoutRtl()) {
                            setFocusBottomRight(root, focusedRect);
                        } else {
                            setFocusTopLeft(root, focusedRect);
                        }
                        break;

                    case View.FOCUS_LEFT:
                    case View.FOCUS_UP:
                        setFocusBottomRight(root, focusedRect);
                        break;
                    case View.FOCUS_BACKWARD:
                        if (root.isLayoutRtl()) {
                            setFocusTopLeft(root, focusedRect);
                        } else {
                            setFocusBottomRight(root, focusedRect);
                        break;
                    }
                }
            }
        }
        
    
    private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
        // check for user specified next focus
        View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
        if (userSetNextFocus != null && userSetNextFocus.isFocusable()
                && (!userSetNextFocus.isInTouchMode()
                        || userSetNextFocus.isFocusableInTouchMode())) {
            return userSetNextFocus;
        }
        return null;
    }

FocusFinder類通過findNextFocus來找焦點的。一層一層往尋找,后面會執行findNextUserSpecifiedFocus()方法,這個方法會執行focused(即當前獲取焦點的View)的findUserSetNextFocus方法,如果該方法返回的View不為空,
且isFocusable = true && isInTouchMode() = true的話。

FocusFinder找到的焦點就是findNextUserSpecifiedFocus()返回的View。

findNextFocus會優先根據XML里設置的下一個將獲取焦點的View的ID值來尋找將要獲取焦點的View。

 View findUserSetNextFocus(View root, @FocusDirection int direction) {
        switch (direction) {
            case FOCUS_LEFT:
                if (mNextFocusLeftId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusLeftId);
            case FOCUS_RIGHT:
                if (mNextFocusRightId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusRightId);
            case FOCUS_UP:
                if (mNextFocusUpId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusUpId);
            case FOCUS_DOWN:
                if (mNextFocusDownId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusDownId);
            case FOCUS_FORWARD:
                if (mNextFocusForwardId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusForwardId);
            case FOCUS_BACKWARD: {
                if (mID == View.NO_ID) return null;
                final int id = mID;
                return root.findViewByPredicateInsideOut(this, new Predicate<View>() {
                    @Override
                    public boolean apply(View t) {
                        return t.mNextFocusForwardId == id;
                    }
                });
            }
        }
        return null;
    }

焦點事件分發步驟:

  • DecorView會調用dispatchKey一層一層進行焦點的分發,如果dispatchKeyEvent方法返回true的話,那么焦點或者按鍵事件就不會往下分發了。

  • 如果你想攔截某個子View,對其設置OnKeyListener進行焦點的攔截。

  • 如果焦點沒有被攔截的話,那么焦點就會交給系統來處理,還是會繼續分發,直到找到那個獲取焦點的View

  • Android底層先會記錄按鍵的方向,后面DecorView會一層一層往下調用findFocus方法找到當前獲取焦點的View

  • 后面系統又會根據按鍵的方向,執行focusSearch方法來尋找下一個將要獲取焦點的View

  • focusSearch內部其實是通過FocusFinder來查找焦點的。FocusFinder會優先通過View在XML布局設置的下一個焦點的ID來查找焦點。

  • 最終如果找到將要獲取焦點的View,就讓其requestFocus。如果請求無效,將其放在onWindowFocusChanged()這個方法中去請求。這是在Activity尋找到焦點的時候。

我的前一篇文章,主要是介紹了TouchEvent事件分發機制,省略了焦點分發傳遞機制的代碼,這篇文章與此相反。如果將兩個結合起來,太繁雜,冗長了。分開反而有利于您的理解。至此,事件分發機制,你也了解的差不多了,給個粉吧!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容