Android TV開發(fā)按鍵與焦點深入分析(三)--按鍵事件轉(zhuǎn)換成焦點移動的過程

上兩篇文章分別單獨分析了KeyEvent在View樹中分發(fā)View獲得焦點的過程,實際上這兩個并不是獨立的,當我們按下按鍵的時候會發(fā)現(xiàn)如果我們不攔截按鍵事件,按鍵事件就會轉(zhuǎn)換成焦點View的切換,現(xiàn)在就開始分析這個轉(zhuǎn)換的過程。

1.ViewRootImpl中的整體過程

第一篇中提到過KeyEvent在View樹中分發(fā)是有Boolean返回值的,代碼注解如下:

View中
/**
    * Dispatch a key event to the next view on the focus path. This path runs
    * from the top of the view tree down to the currently focused view. If this
    * view has focus, it will dispatch to itself. Otherwise it will dispatch
    * the next node down the focus path. This method also fires any key
    * listeners.
    *
    * @param event The key event to be dispatched.
    * @return True if the event was handled, false otherwise.
    */
   public boolean dispatchKeyEvent(KeyEvent event) 

返回true代表這個按鍵事件已經(jīng)被消耗掉了,false代表還沒被消耗。默認返回的是fasle,只有我們在這個過程中想要攔截、處理了按鍵事件,才會返回true。
最后會把這個結(jié)果向上一層一層地反饋到按鍵事件產(chǎn)生的位置,這個位置就是ViewRootImpl的processKeyEvent方法中,如下:

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

            ......

            // Deliver the key to the view hierarchy.
            if (mView.dispatchKeyEvent(event)) {//View樹中分發(fā)按鍵事件
                return FINISH_HANDLED;//被處理了
            }
            //沒被處理  
            .......
            // Handle automatic focus changes.
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                if (groupNavigationDirection != 0) {
                    if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                        return FINISH_HANDLED;
                    }
                } else {//尋找焦點View
                    if (performFocusNavigation(event)) {
                        return FINISH_HANDLED;
                    }
                }
            }
}

mView就是DecorView,是否已被消耗的結(jié)果的終點就是這里。如果是被消耗了就直接返回,沒有,那就調(diào)用performFocusNavigation方法,從方法名字就可以看出這個方法就是要將按鍵事件轉(zhuǎn)換成焦點的移動,方法如下:

ViewRootImpl中
private boolean performFocusNavigation(KeyEvent event) {
            int direction = 0;
            //將按鍵事件的上下左右轉(zhuǎn)換成焦點移動方向的上下左右
            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();//找出此時這個有焦點的View
                if (focused != null) {
                    //調(diào)用它的focusSearch方法,顧名思義尋找這個方向上的下一個獲得焦點的View
                    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);
                        }
                        //調(diào)用它的requestFocus,讓它獲得焦點
                        if (v.requestFocus(direction, mTempRect)) {
                            playSoundEffect(SoundEffectConstants
                                    .getContantForFocusDirection(direction));
                            return true;
                        }
                    }

                    // Give the focused view a last chance to handle the dpad key.
                    if (mView.dispatchUnhandledMove(focused, direction)) {
                        return true;
                    }
                } else {
                    if (mView.restoreDefaultFocus()) {
                        return true;
                    }
                }
            }
            return false;
        }

這個方法中一氣呵成完成了按鍵事件轉(zhuǎn)換成焦點View變化的全部過程,可以概括為以下4個步驟:

  1. 將按鍵事件的上下左右轉(zhuǎn)換成焦點移動方向的上下左右
  2. 找出View樹中有焦點的View
  3. 調(diào)用焦點View的focusSearch方法尋找下一個獲得焦點的View
  4. 調(diào)用下一個獲得焦點的View的requestFocus方法,讓它獲得焦點

下面分別分析第2、3步的過程,第4步請看上一篇的分析

2.尋找有焦點的View

調(diào)用的是View的findFocus方法,ViewGroup和View的findFoucs方法分別如下:

ViewGroup中
@Override
    public View findFocus() {
        if (isFocused()) {
            return this;
        }
        if (mFocused != null) {
            return mFocused.findFocus();
        }
        return null;
    }    
View中
    public boolean isFocused() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0;
    }
View中
    public View findFocus() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
    }

尋找的依據(jù)就是上一篇中分析的PFLAG_FOCUSED標志位以及ViewGroup的mFocused成員變量,首先是ViewGroup判斷自己是不是有焦點,然后再判斷自己是不是包含了有焦點的子View,多次按照焦點的路徑遍歷就找出了焦點View。

3.尋找下一個獲得焦點的View

View的focusSearch方法

此刻已經(jīng)找出了焦點View,需要調(diào)用它的focusSearch去尋找下一個焦點View,View的focusSearch方法如下:

View中
    public View focusSearch(@FocusRealDirection int direction) {
        if (mParent != null) {
            return mParent.focusSearch(this, direction);
        } else {
            return null;
        }
    }

直接調(diào)用了它的父View的方法,如下:

ViewGroup中
@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;
    }
    

ViewGroup重寫了這個方法,如果自己是RootNamespace那就調(diào)用FocusFinder去尋找View,但什么時候isRootNamespace()成立,我現(xiàn)在還沒遇到過,所以一般情況下最后會調(diào)用到ViewRootImpl的focusSearch方法,這個方法如下:

ViewRootImpl中 
@Override
    public View focusSearch(View focused, int direction) {
        checkThread();
        if (!(mView instanceof ViewGroup)) {
            return null;
        }
        return FocusFinder.getInstance().findNextFocus((ViewGroup) mView, focused, direction);
    }

殊途同歸,最后還是調(diào)用了FocusFinder去尋找View,看來尋找下一個焦點View的重任全部都封裝在了這個FocusFinder類中。

FocusFinder單例

 private static final ThreadLocal<FocusFinder> tlFocusFinder =
            new ThreadLocal<FocusFinder>() {
                @Override
                protected FocusFinder initialValue() {
                    return new FocusFinder();
                }
            };

    /**
     * Get the focus finder for this thread.
     */
    public static FocusFinder getInstance() {
        return tlFocusFinder.get();
    }

值得注意的是FocusFinder居然是線程單例的,而不是進程單例的,這樣做的原因大概是一方面發(fā)揮單例優(yōu)勢,避免頻繁創(chuàng)建FocusFinder,畢竟這是一個比較基本的功能,節(jié)約資源提高效率;另一方面,萬一其他線程也要調(diào)用FocusFinder去做一些尋找View的事情,如果進程單例那不就影響主線程的效率了?所以線程單例最合適吧。

添加所有可以獲得焦點的View

言歸正傳,繼續(xù)看它的findNextFocus方法,如下:

FocusFinder中
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
       View next = null;
       //找出焦點跳轉(zhuǎn)View的范圍
       ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
       if (focused != null) {//找出在xml中指定的該方向的下一個獲得焦點的View
           next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
       }
       if (next != null) {//指定了直接返回
           return next;
       }
       ArrayList<View> focusables = mTempList;
       try {
           focusables.clear();
           //從最頂點,將所有可以獲得焦點的View添加到focusables中
           effectiveRoot.addFocusables(focusables, direction);
           if (!focusables.isEmpty()) {
               //從中找出下一個可以獲得焦點的View
               next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
           }
       } finally {
           focusables.clear();
       }
       return next;
   }

首先找出焦點跳轉(zhuǎn)的View的范圍,我這里測試時effectiveRoot就是DecorView,也就是整個View樹中View都在考慮的范圍內(nèi)。然后調(diào)用findNextUserSpecifiedFocus方法去獲取我們手動為這個View設(shè)置的上下左右的焦點View,對應(yīng)xml布局中的android:nextFocusXX

            android:nextFocusDown="@id/textView"
            android:nextFocusLeft="@id/textView"
            android:nextFocusRight="@id/textView"
            android:nextFocusUp="@id/textView"

如果設(shè)置了,那就直接返回設(shè)置的View,沒有則繼續(xù)尋找,addFocusables方法把View樹中所有可能獲得焦點的View都放進了focusables這個list中。
ViewGroup中的addFocusables方法如下:

ViewGroup中
    @Override
    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
        final int focusableCount = views.size();

        final int descendantFocusability = getDescendantFocusability();
        final boolean blockFocusForTouchscreen = shouldBlockFocusForTouchscreen();
        final boolean focusSelf = (isFocusableInTouchMode() || !blockFocusForTouchscreen);

        if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) {//攔截了焦點,只判斷、添加自己
            if (focusSelf) {
                super.addFocusables(views, direction, focusableMode);
            }
            return;
        }

        if (blockFocusForTouchscreen) {
            focusableMode |= FOCUSABLES_TOUCH_MODE;
        }
        //在所有子View之前添加自己到views
        if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {
            super.addFocusables(views, direction, focusableMode);
        }

        int count = 0;
        final View[] children = new View[mChildrenCount];
        //挑出可見的View
        for (int i = 0; i < mChildrenCount; ++i) {
            View child = mChildren[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                children[count++] = child;
            }
        }
        //對所有子View排序
        FocusFinder.sort(children, 0, count, this, isLayoutRtl());
        for (int i = 0; i < count; ++i) {//把所有子View按順序添加到views
            children[i].addFocusables(views, direction, focusableMode);
        }

        // When set to FOCUS_AFTER_DESCENDANTS, we only add ourselves if
        // there aren't any focusable descendants.  this is
        // to avoid the focus search finding layouts when a more precise search
        // among the focusable children would be more interesting.
        if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf
                && focusableCount == views.size()) {
            super.addFocusables(views, direction, focusableMode);
        }
    }

如果ViewGroup攔截焦點,那就不用再考慮子View了;如果ViewGroup在子View之前獲得焦點,那就先添加,反之后添加;對于兄弟View,在添加之前還要對它們進行排序,排序的依據(jù)是從上到下、從左到右。

View的addFocusables方法

    public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
            @FocusableMode int focusableMode) {
        if (views == null) {
            return;
        }
        if (!canTakeFocus()) {
            return;
        }
        if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
                && !isFocusableInTouchMode()) {
            return;
        }
        views.add(this);
    }

直接判斷自己是否可以獲得焦點,可以的話就把自己加到views中去。

到這里所有的可以獲得焦點的View 都被添加到了focusables中去了,在這個過程中與方向還沒有關(guān)系,只是枚舉添加了所有可能的View。

尋找最優(yōu)View

有了focusables列表,這時調(diào)用同名方法findNextFocus在focusables找出最合適的那個View,方法如下:

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);
            //將focused的坐標變成rootView下的坐標
            root.offsetDescendantRectToMyCoords(focused, focusedRect);
        } else {
           ......
        }

        switch (direction) {
            case View.FOCUS_FORWARD:
            case View.FOCUS_BACKWARD:
                return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
                        direction);
            case View.FOCUS_UP:
            case View.FOCUS_DOWN:
            case View.FOCUS_LEFT:
            case View.FOCUS_RIGHT:
     
                return findNextFocusInAbsoluteDirection(focusables, root, focused,
                        focusedRect, direction);
            default:
                throw new IllegalArgumentException("Unknown direction: " + direction);
        }
    }

到這里,尋找的才與方向有關(guān),下面的分析以按右鍵為例,對應(yīng)的是View.FOCUS_RIGHT,調(diào)用了findNextFocusInAbsoluteDirection方法:

View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
            Rect focusedRect, int direction) {
        // initialize the best candidate to something impossible
        // (so the first plausible view will become the best choice)
        mBestCandidateRect.set(focusedRect);
        switch(direction) {
            case View.FOCUS_LEFT:
                mBestCandidateRect.offset(focusedRect.width() + 1, 0);
                break;
            case View.FOCUS_RIGHT://向右尋找時,將初始的位置設(shè)為當前View的最左邊
                mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
                break;
            case View.FOCUS_UP:
                mBestCandidateRect.offset(0, focusedRect.height() + 1);
                break;
            case View.FOCUS_DOWN:
                mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
        }

        View closest = null;

        int numFocusables = focusables.size();
        //遍歷所有的可獲得焦點的View
        for (int i = 0; i < numFocusables; i++) {
            View focusable = focusables.get(i);
            //排除自己
            // only interested in other non-root views
            if (focusable == focused || focusable == root) continue;
            //獲得這個View的rect,并把它調(diào)整到和focusedRect一致
            // get focus bounds of other view in same coordinate system
            focusable.getFocusedRect(mOtherRect);
            root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
            //判斷這個View是不是比mBestCandidateRect更優(yōu)
            if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
                //更優(yōu),那么將它設(shè)置成mBestCandidateRect,并將closest賦值
                mBestCandidateRect.set(mOtherRect);
                closest = focusable;
            }
        }
        return closest;
    }

實際上尋找的過程就是在比較各個View的占用區(qū)域的相對關(guān)系,這里首先設(shè)置了一個mBestCandidateRect代表最合適的View的區(qū)域,對于向右,是焦點View左邊間距一像素的同等大小的一個區(qū)域,顯然這是最不合適的區(qū)域,這只是一個初始值。然后就開始遍歷所有可能的View,調(diào)用isBetterCandidate方法去判斷,這里就不展開這個方法,太啰嗦了,用下面的圖代替。
假如下圖中View1此時有焦點,按下右鍵時會怎么樣呢?


實際上判斷的依據(jù)和下圖中的畫的各種虛線有關(guān)
foucs_right

按下右鍵,初始的最佳區(qū)域就是紅色虛線框的位置,顯示是最不適合的,然后再遍歷所有的可能的View,滿足以下兩點才可以擊敗紅色虛線框的位置成為待選的View:

  1. 以紫色虛線作為標準,View的右邊線必須在紫色虛線的右邊
  2. 以黑色虛線作為標準,View的左邊線必須在黑色虛線的右邊

顯然View2和View3都滿足,這時就需要進一步的比較了,進一步比較需要參考View的上下兩邊,滿足以下兩點的最優(yōu):

  1. View的下邊線在上面的那條藍色虛線之下
  2. View的上邊線在下面的那條藍色虛線之上

也就是這個View的與焦點View在上下兩邊上有重疊的區(qū)域就可以了,所以View3是最優(yōu)的位置,View3將獲得焦點。
還有一種情況,要是兩個View都與焦點View在上下兩邊有重疊的區(qū)域,那誰更優(yōu)呢?如下:


foucs_right

對于向右尋找焦點,這時判斷的依據(jù)就和圖中的兩個距離箭頭major和minor的長度有關(guān),計算的公式distance=13*major^2+minor^2,distance越小越有優(yōu),13是一個常量系數(shù),表示以左右間距作為主要的判斷依據(jù),但不排除上下間距逆襲的可能,所以越靠近焦點View的中心,就越有可能獲得焦點。
調(diào)試的布局如下,可以微微調(diào)整bias,驗證結(jié)論。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
        android:layout_height="match_parent"
        android:layout_width="match_parent">


    <TextView
            android:text="View1"
            android:background="@drawable/bg"
            android:focusable="true"
            android:gravity="center"
            android:layout_width="55dp"
            android:layout_height="344dp"
            android:id="@+id/textView"
            app:layout_constraintEnd_toStartOf="@id/guideline2"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintHorizontal_bias="0.99"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>
    <TextView
            android:text="View2"
            android:background="@drawable/bg"
            android:focusable="true"
            android:layout_width="60dp"
            android:gravity="center"
            android:layout_height="50dp"
            android:id="@+id/textView2"
            app:layout_constraintStart_toStartOf="@+id/guideline2"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintHorizontal_bias="0.1"
            app:layout_constraintBottom_toTopOf="@+id/guideline"
            app:layout_constraintVertical_bias="0.7"/>
    <TextView
            android:text="View3"
            android:background="@drawable/bg"
            android:focusable="true"
            android:layout_width="60dp"
            android:gravity="center"
            android:layout_height="50dp"
            android:id="@+id/textView3"
            app:layout_constraintStart_toStartOf="@+id/guideline2"
            app:layout_constraintVertical_bias="0.3"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="@+id/guideline"
            app:layout_constraintHorizontal_bias="0.1"
            app:layout_constraintBottom_toBottomOf="parent"/>
    <android.support.constraint.Guideline
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintGuide_percent="0.3"
            android:id="@+id/guideline2"
            android:orientation="vertical"/>
    <android.support.constraint.Guideline
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintGuide_percent="0.5"
            android:id="@+id/guideline"
            android:orientation="horizontal"/>
</android.support.constraint.ConstraintLayout>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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