Android按鍵事件焦點移動源碼分析

在市場上及穗,手機硬件基本上占領(lǐng)android設(shè)備的絕大部分市場绳军,而在TV上,由于人機交互的方式不同所踊,并且當前主流的TV并不具備觸摸屏(雖然目前的觸屏電視已經(jīng)面市,但是該類商顯產(chǎn)品主要還是2B概荷。)秕岛,傳統(tǒng)TV還是通過遙控器的方向按鍵進行操控,在android系統(tǒng)中則是通過焦點的移動標識來展示給用戶當前的控制點。下面就從接收到遙控器的按鍵事件開始继薛,一步步分析下系統(tǒng)中的焦點機制是如何響應(yīng)工作的修壕。(本文基于API 27源碼進行分析)

首先,從底層驅(qū)動接收到遙控器按鍵或者觸摸屏觸摸事件后遏考,通過一步步的轉(zhuǎn)換到android framework中的用戶界面層慈鸠,會回調(diào)給ViewRootImpl中的ViewPostImeInputStage,這個內(nèi)部類的代碼稍長灌具,因為不論是觸屏還是按鍵青团,都是在這里進行初始的分發(fā)處理,在此咖楣,我們只重點關(guān)注按鍵事件以及焦點的處理:

<ViewRootImpl.java>
/**
 * Delivers post-ime input events to the view hierarchy.
 */
final class ViewPostImeInputStage extends InputStage {
    public ViewPostImeInputStage(InputStage next) {
        super(next);
    }

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

    @Override
    protected void onDeliverToNext(QueuedInputEvent q) {
        ...
    }

    private boolean performFocusNavigation(KeyEvent event) {
        ...
    }

    private boolean performKeyboardGroupNavigation(int direction) {
        ...
    }

    private int processKeyEvent(QueuedInputEvent q) {
        ...
    }

    private int processPointerEvent(QueuedInputEvent q) {
        ...
    }

    private void maybeUpdatePointerIcon(MotionEvent event) {
        ...
    }

    private int processTrackballEvent(QueuedInputEvent q) {
        ...
    }

    private int processGenericMotionEvent(QueuedInputEvent q) {
        ...
    }
}

首先我們來看下onProcess回調(diào)中的參數(shù)QueuedInputEvent

<ViewRootImpl.java>
    private static final class QueuedInputEvent {
        public static final int FLAG_DELIVER_POST_IME = 1 << 0;
        public static final int FLAG_DEFERRED = 1 << 1;
        public static final int FLAG_FINISHED = 1 << 2;
        public static final int FLAG_FINISHED_HANDLED = 1 << 3;
        public static final int FLAG_RESYNTHESIZED = 1 << 4;
        public static final int FLAG_UNHANDLED = 1 << 5;
    
        public QueuedInputEvent mNext;

        public InputEvent mEvent;
        public InputEventReceiver mReceiver;
        public int mFlags;
    ...
    }
// InputEvent的兩個子類
public class KeyEvent extends InputEvent implements Parcelable {}
public final class MotionEvent extends InputEvent implements Parcelable {}

觸摸或者按鍵都是一系列的接收事件督笆,QueuedInputEvent實際上是類似Message的一個隊列,mNext變量指向的是下一個事件(單向鏈表的結(jié)構(gòu))诱贿。mEvent變量標記了該事件的類型娃肿,我們可以看到android中,InputEvent只有兩個子類珠十,一個是KeyEvent按鍵事件料扰,另一個是MotionEvent觸摸事件”翰洌回到上面的onProcess方法晒杈,很明顯我們TV端的是KeyEvent事件,進入processKeyEvent進行按鍵事件的處理孔厉。

<ViewRootImpl.java>
    @Override
    protected int onProcess(QueuedInputEvent q) {
        if (q.mEvent instanceof KeyEvent) {// 接收到的事件是按鍵事件
            return processKeyEvent(q);// 進入這個分支拯钻,按鍵事件處理
        } else {
            final int source = q.mEvent.getSource();// 手指觸摸的touch事件
            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);
            }
        }
    }

    private int processKeyEvent(QueuedInputEvent q) {
        final KeyEvent event = (KeyEvent)q.mEvent;// 獲取到該按鍵事件信息,我們常見的KeyCode烟馅,Acton说庭,RepeatCount等信息都包含在里面
    
        // Deliver the key to the view hierarchy.
        if (mView.dispatchKeyEvent(event)) {// mView實際上就是DecorView,這里看到如果dispatchKeyEvent返回true郑趁,會直接返回刊驴,這里的按鍵事件分發(fā)后面單獨一篇講解,對比touch事件分發(fā)要簡單不少
            return FINISH_HANDLED;
        }
    
        if (shouldDropInputEvent(q)) {// 是否拋棄該事件寡润,里面主要是判斷View是否初始化或者還未add進來捆憎,window失去焦點(window失去焦點也就是說該window無法交互,所以接收事件也沒用梭纹,直接返回)
            return FINISH_NOT_HANDLED;
        }
    
        int groupNavigationDirection = 0;
        // 根據(jù)tab和shift按鍵判斷導(dǎo)航方向
        if (event.getAction() == KeyEvent.ACTION_DOWN
                && event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
            if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {
                groupNavigationDirection = View.FOCUS_FORWARD;
            } else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
                    KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {
                groupNavigationDirection = View.FOCUS_BACKWARD;
            }
        }
    
        // 設(shè)置了快捷鍵
        // If a modifier is held, try to interpret the key as a shortcut.
        if (event.getAction() == KeyEvent.ACTION_DOWN
                && !KeyEvent.metaStateHasNoModifiers(event.getMetaState())
                && event.getRepeatCount() == 0
                && !KeyEvent.isModifierKey(event.getKeyCode())
                && groupNavigationDirection == 0) {
            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) {
            if (groupNavigationDirection != 0) {
                if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                    return FINISH_HANDLED;
                }
            } else {// 真正開始焦點導(dǎo)航的地方
                if (performFocusNavigation(event)) {
                    return FINISH_HANDLED;
                }
            }
        }
        return FORWARD;
    }

上面經(jīng)過一系列判斷躲惰,包括Tab,Shift和快捷鍵的處理变抽,我們這里重點關(guān)注最后的方向鍵導(dǎo)航處理performFocusNavigation(event)

<ViewRootImpl.java>
    private boolean performFocusNavigation(KeyEvent event) {
        int direction = 0;
        switch (event.getKeyCode()) {// 將按鍵事件的鍵值轉(zhuǎn)換為View的焦點導(dǎo)航方向值
            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();// 從decorview中查找當前的焦點
            if (focused != null) {
                View v = focused.focusSearch(direction);// 根據(jù)方向查找下一個焦點,調(diào)用parent的focusSearch查找
                if (v != null && v != focused) {// 已經(jīng)查找到下一個焦點
                    // do the math the get the interesting rect
                    // of previous focused into the coord system of
                    // newly focused view
                    focused.getFocusedRect(mTempRect);// 獲取下一個焦點的視圖區(qū)域
                    if (mView instanceof ViewGroup) {// 平移視圖讓焦點區(qū)域在當前視圖中完全可見
                        ((ViewGroup) mView).offsetDescendantRectToMyCoords(
                                focused, mTempRect);
                        ((ViewGroup) mView).offsetRectIntoDescendantCoords(
                                v, mTempRect);
                    }
                    if (v.requestFocus(direction, mTempRect)) {// 對查找到的焦點view調(diào)用requestFocus塔沃,清除oldFocus的焦點狀態(tài)
                        playSoundEffect(SoundEffectConstants// 播放焦點移動音效蝠引,處理結(jié)束
                                .getContantForFocusDirection(direction));
                        return true;
                    }
                }
    
                // Give the focused view a last chance to handle the dpad key.
                if (mView.dispatchUnhandledMove(focused, direction)) {// 查找焦點失敗,再提供一個機會去處理該次按鍵事件下view的移動
                    return true;
                }
            } else {// 如果當前都沒有焦點
                if (mView.restoreDefaultFocus()) {// 重新初始化默認焦點蛀柴,處理完畢
                    return true;
                }
        }
        }
        return false;
    }

這里面首先將按鍵的鍵值轉(zhuǎn)換為焦點導(dǎo)航方向螃概,主要有6個:FOCUS_BACKWARD,FOCUS_FORWARD,FOCUS_LEFT,FOCUS_UP,FOCUS_RIGHT,FOCUS_DOWN,接著通過findFocus查找到當前視圖中的焦點鸽疾。然后通過focusSearch方法(這個方法是查找焦點的關(guān)鍵方法吊洼,一些定制化邏輯可以通過修改此方法實現(xiàn)),根據(jù)當前焦點根據(jù)導(dǎo)航方向肮韧,去尋找下一個應(yīng)該聚焦的View:

<View.java>
    public View focusSearch(@FocusRealDirection int direction) {
        if (mParent != null) {
            return mParent.focusSearch(this, direction);// 通過parent父View去查找下一個焦點
        } else {
            return null;
        }
    }

<ViewGroup.java>
    @Override
    public View focusSearch(View focused, int direction) {
        if (isRootNamespace()) {// 當前view==decorView融蹂,一般我們最終會走到這個分支
            // 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;
    }

View.focusSearch實際上是調(diào)用了parent的focusSearch旺订,一層一層往上弄企,最終也就是走到根布局DecorView的focusSearch,通過FocusFinder來進行焦點查找:

<FocusFinder.java>
    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        View next = null;// 下一個焦點
        ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
        if (focused != null) {
            next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);// 當前焦點不為null区拳,首先判斷用戶對當前焦點的view在該方向上是否指定id拘领,也就是我們通常xml中寫的nextFocusLeft這種
        }
        if (next != null) {
            return next;// 如果用戶指定了下個焦點id,直接返回該id對應(yīng)的view
        }
        ArrayList<View> focusables = mTempList;// 這個集合是用來裝所有可獲得焦點的View
        try {
            focusables.clear();
            effectiveRoot.addFocusables(focusables, direction);// 查找可獲得焦點的view樱调,添加進集合
            if (!focusables.isEmpty()) {// 存在可獲得焦點的view
                next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);// 繼續(xù)在所有可獲得焦點的view集合中查找下一個焦點
            }
        } finally {
            focusables.clear();// 查找完畢后清理數(shù)據(jù)约素,釋放內(nèi)存
        }
        return next;// 返回下一個焦點
    }
  • 首先會去判斷用戶有沒有手動在xml中指定該方向的下一個焦點view的id,如果指定了直接返回該view作為下一個焦點笆凌,流程結(jié)束圣猎。對于findNextUserSpecifiedFocus方法邏輯還是比較好理解,在此不做展開分析乞而。
  • 接著會查找所有可獲得焦點的view送悔,將它們添加到focusables集合中,縮小焦點查找范圍爪模。這里有個關(guān)鍵方法:addFocusables欠啤,這個方法在平時定制化開發(fā)中可以用于焦點記憶,例如leanback視圖中每一行recyclerView中的焦點記憶屋灌。
<View.java>
    public void addFocusables(ArrayList<View> views, @FocusDirection int direction) {
        addFocusables(views, direction, isInTouchMode() ? FOCUSABLES_TOUCH_MODE : FOCUSABLES_ALL);
    }

    public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
            @FocusableMode int focusableMode) {
        if (views == null) {
            return;
        }
        if (!isFocusable()) {// 不可聚焦洁段,直接返回
            return;
        }
        if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
                && !isFocusableInTouchMode()) {// 觸摸模式下,但是focusInTouchMode設(shè)置為false共郭,直接返回
            return;
        }
        views.add(this);// 將自己添加到集合中
    }

<ViewGroup.java>
    @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) {// 如果設(shè)置了攔截焦點
            if (focusSelf) {
                super.addFocusables(views, direction, focusableMode);// 調(diào)用View的addFocusables將自己添加進集合
            }
            return;// 直接返回祠丝,不再添加自己view數(shù)結(jié)構(gòu)下面的子View
        }

        if (blockFocusForTouchscreen) {
            focusableMode |= FOCUSABLES_TOUCH_MODE;
        }

        if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {
            super.addFocusables(views, direction, focusableMode);// 調(diào)用View的addFocusables將自己添加進集合
        }

        int count = 0;
        final View[] children = new View[mChildrenCount];
        for (int i = 0; i < mChildrenCount; ++i) {// 遍歷當前viewGroup下的所有子View
            View child = mChildren[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {// view處于可見狀態(tài)
                children[count++] = child;// 賦值給children數(shù)組
            }
        }
        FocusFinder.sort(children, 0, count, this, isLayoutRtl());// 根據(jù)方向排序
        for (int i = 0; i < count; ++i) {
            children[i].addFocusables(views, direction, focusableMode);// 如果children[i]這個子view是viewGroup的話疾呻,遞歸調(diào)用繼續(xù)查找該child viewGroup下的子View,直到查找所有最下層的子view写半,最終調(diào)用View.addFocusables判斷是否可聚焦罐韩,可聚焦則添加進集合
        }
        // 走到這里,views中已經(jīng)保存了所有可聚焦的子View

        // 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()) {// 如果是FOCUS_AFTER_DESCENDANTS污朽,除了子view判斷外散吵,最后將自己也添加進去
            super.addFocusables(views, direction, focusableMode);
        }
    }

經(jīng)過上面的addFocusables已經(jīng)將所有可見狀態(tài)并且可以聚焦的view全部收集到了focusables這個集合中,接著在該集合中去查找下一個焦點:

<FocusFinder.java>
    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        ...
            if (!focusables.isEmpty()) {// 存在可獲得焦點的view
                next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);// 繼續(xù)在所有可獲得焦點的view集合中查找下一個焦點
            }
        ...
        return next;// 返回下一個焦點
    }


    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
            int direction, ArrayList<View> focusables) {
        if (focused != null) {// 當前焦點不為null
            if (focusedRect == null) {
                focusedRect = mFocusedRect;
            }
            // fill in interesting rect from focused
            focused.getFocusedRect(focusedRect);// 獲取到當前焦點的rect區(qū)域
            root.offsetDescendantRectToMyCoords(focused, focusedRect);// 考慮scroll滑動狀態(tài)蟆肆,即把視框拉伸至滑動到屏幕外的視圖也可見狀態(tài)矾睦,統(tǒng)一坐標系便于下面焦點查找計算
        } 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);// 當前焦點為null,將滑動后的左上角作為尋找起始點(scrollX炎功,scrollY)枚冗,走到這里的話這個focusedRect實際上是個點
                        break;
                    case View.FOCUS_FORWARD:
                        if (root.isLayoutRtl()) {// 會根據(jù)rtl區(qū)分(某些國家語言是從右往左書寫習慣)
                            setFocusBottomRight(root, focusedRect);
                        } else {
                            setFocusTopLeft(root, focusedRect);
                        }
                        break;

                    case View.FOCUS_LEFT:
                    case View.FOCUS_UP:
                        setFocusBottomRight(root, focusedRect);// 當前焦點為null,將滑動后的右下角作為尋找起始點(scrollX蛇损,scrollY)赁温,走到這里的話這個focusedRect實際上是個點
                        break;
                    case View.FOCUS_BACKWARD:
                        if (root.isLayoutRtl()) {// 會根據(jù)rtl區(qū)分是否將坐標反轉(zhuǎn)
                            setFocusTopLeft(root, focusedRect);
                        } else {
                            setFocusBottomRight(root, focusedRect);
                        break;
                    }
                }
            }
        }

        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:// 我們重點只關(guān)注這方向鍵的焦點查找算法
                return findNextFocusInAbsoluteDirection(focusables, root, focused,
                        focusedRect, direction);
            default:
                throw new IllegalArgumentException("Unknown direction: " + direction);
        }
    }
  • 如果當前焦點不為null,先獲取當前焦點的rect視圖區(qū)域淤齐,考慮到scroll狀態(tài)股囊,將當前焦點的rect坐標系進行轉(zhuǎn)換。
  • 如果當前焦點為null更啄,根據(jù)導(dǎo)航方向稚疹,設(shè)置一個左上角或者右下角的rect為默認的起始參考點,根據(jù)這個點再結(jié)合方向去計算下一個焦點祭务。
    這里我們重點看下上下左右移動的這個方法findNextFocusInAbsoluteDirection内狗,大致看下它內(nèi)部查找焦點算法:
<FocusFinder.java>
    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);// 將當前焦點的rect賦值給mBestCandidateRect
        switch(direction) {// 在反方向上偏移一個width或者height+1個像素點,虛構(gòu)出來的下一個候補焦點(優(yōu)先級應(yīng)該是最低的义锥,因為是反方向平移)
            case View.FOCUS_LEFT:
                mBestCandidateRect.offset(focusedRect.width() + 1, 0);
                break;
            case View.FOCUS_RIGHT:
                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();
        for (int i = 0; i < numFocusables; i++) {// 開始遍歷所有可聚焦的子view
            View focusable = focusables.get(i);

            // only interested in other non-root views
            if (focusable == focused || focusable == root) continue;// 如果集合中的view是當前的焦點或者viewGroup柳沙,直接跳過繼續(xù)查找下一個

            // get focus bounds of other view in same coordinate system
            focusable.getFocusedRect(mOtherRect);
            root.offsetDescendantRectToMyCoords(focusable, mOtherRect);// 將該view也進行坐標系轉(zhuǎn)換,和當前焦點在同一個坐標系進行計算

            if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
                mBestCandidateRect.set(mOtherRect);// 如果找到一個符合的拌倍,則將其的區(qū)域賦值給虛構(gòu)的候補焦點赂鲤,參照物變了之后,繼續(xù)遍歷看有沒有更優(yōu)的
                closest = focusable;// 這個closest會不斷刷新贰拿,因為每次進入該分支蛤袒,最新的focusable符合條件都會優(yōu)于上一個候補焦點
            }
        }
        return closest;
    }

先獲取當前焦點的視圖區(qū)域rect,然后將該區(qū)域按照導(dǎo)航方向的反方向偏移1個像素+當前焦點的width或者height膨更,得到一個虛構(gòu)的焦點區(qū)域mBestCandidateRect妙真。接著就開始遍歷之前收集到的所有可見可聚焦的view集合,如果當前遍歷的view就是當前焦點或者rootView荚守,直接忽略跳過繼續(xù)往下遍歷查找珍德。遍歷的時候练般,會將遍歷的view坐標轉(zhuǎn)換,只有轉(zhuǎn)換坐標后和當前焦點在同一個坐標系锈候,這樣才能為下面算法提供準確參數(shù):

<FocusFinder.java>
    // 幾個參數(shù)含義: direction方向薄料,source當前焦點,rect1當前對比的view泵琳,rect2虛構(gòu)的候補焦點(如果有符合的摄职,rect2會刷新為當前符合條件的view區(qū)域,即如果成立获列,rect1會賦值給下次該方法的rect2)
    // 這幾個參數(shù)命名比較容易弄混谷市,尤其是下面調(diào)用算法的時候又改名了,要區(qū)分清楚
    boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {

        // to be a better candidate, need to at least be a candidate in the first
        // place :)
        if (!isCandidate(source, rect1, direction)) {// 先將當前遍歷的view與當前焦點比較
            return false;
        }
        // 走到這里說明上面的isCandidate返回true击孩,也就是當前遍歷的rect1符合條件迫悠。例如direction為左,說明rect1在當前焦點的左側(cè)巩梢,符合條件创泄,加入候選,進行下一步判斷
        // we know that rect1 is a candidate.. if rect2 is not a candidate,
        // rect1 is better
        if (!isCandidate(source, rect2, direction)) {// 第一次走到這的話這個isCandidate肯定返回false括蝠,因為rect2第一次是我們之前虛構(gòu)的候補焦點鞠抑,是在導(dǎo)航的反方向,肯定為false又跛,直接返回true碍拆。再后面的話若治,相當于上一個候補和當前焦點進行比較慨蓝,肯定返回true,繼續(xù)下一步判斷
            return true;
        }

        // if rect1 is better by beam, it wins
        if (beamBeats(direction, source, rect1, rect2)) {// 當前遍歷的view也符合條件端幼,將它和上一個候補進行比較
            return true;// 當前遍歷的view優(yōu)于上一個候補礼烈,將當前遍歷的賦值給最新的closest,也就是目前遍歷過程中最優(yōu)焦點
        }

        // if rect2 is better, then rect1 cant' be :)
        if (beamBeats(direction, source, rect2, rect1)) {// 上一個候補優(yōu)于當前遍歷的
            return false;
        }

        // otherwise, do fudge-tastic comparison of the major and minor axis
        return (getWeightedDistanceFor(// 計算rect1和rect2相對于當前焦點的距離
                        majorAxisDistance(direction, source, rect1),
                        minorAxisDistance(direction, source, rect1))
                < getWeightedDistanceFor(
                        majorAxisDistance(direction, source, rect2),
                        minorAxisDistance(direction, source, rect2)));
    }

重點算法1婆跑,計算是否在導(dǎo)航的那側(cè)此熬,在導(dǎo)航方向上允許有重疊。這個算法都是比較xy方向的邊界大小滑进,相對于下面的算法2真的是容易理解很多犀忱,稍微畫幾個圖就能理解了。

<FocusFinder.java>
    // srcRect當前焦點扶关,destRect比較的view阴汇,direction方向
    boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
        switch (direction) {
            case View.FOCUS_LEFT:// 向左:只比較left和right,就是dest是否整體在src的左側(cè)节槐,這里說的是整體搀庶,dest可以與src有交集拐纱,但是dest的左右邊界都不能超過src的右邊界
                return (srcRect.right > destRect.right || srcRect.left >= destRect.right) 
                        && srcRect.left > destRect.left;
            case View.FOCUS_RIGHT:// 向右:只比較left和right,就是dest是否整體在src的右側(cè)哥倔,這里說的是整體秸架,dest可以與src有交集,但是src的左右邊界都不能超過dest的右邊界
                return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
                        && srcRect.right < destRect.right;
            case View.FOCUS_UP:// 向上:只比較top和bottom咆蒿,就是dest是否整體在src的上面东抹,這里說的是整體,dest可以與src有交集沃测,但是dest的上下邊界都不能超過src的下邊界
                return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
                        && srcRect.top > destRect.top;
            case View.FOCUS_DOWN:// 向下:只比較top和bottom府阀,就是dest是否整體在src的下面,這里說的是整體芽突,dest可以與src有交集试浙,但是src的上下邊界都不能超過dest的下邊界
                return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
                        && srcRect.bottom < destRect.bottom;
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }

重點算法2,這算法看著真的很亂寞蚌,官方的注釋也不好理解田巴,不好描述,還是自己畫幾張圖按流程跑一下去理解吧挟秤。

<FocusFinder.java>
    // direction方向壹哺,source當前焦點,rect1比較的view1艘刚,rect2比較的view2(rect1和rect2具體看上面算法調(diào)用的順序)
    // 第一次調(diào)用:rect1當前遍歷的view管宵,rect2上一次符合條件的候補焦點
    // 第二次調(diào)用:rect1上一次符合條件的候補焦點,rect2當前遍歷的view
    boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
        final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);// rect1和當前焦點在相對于導(dǎo)航方向的垂直方向是否有重疊攀甚,導(dǎo)航方向為左右x軸時比較y軸重疊
        final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);// rect2和當前焦點在相對于導(dǎo)航方向的垂直方向是否有重疊箩朴,導(dǎo)航方向為上下y軸時比較x軸重疊

        // if rect1 isn't exclusively in the src beam, it doesn't win
        if (rect2InSrcBeam || !rect1InSrcBeam) {// rect2有重疊,或者rect1沒有重疊
            // 第一次調(diào)用:上一次符合條件的候補焦點與當前焦點有重疊秋度,或者當前遍歷的view與當前焦點沒有重疊
            return false;// 如果第一次進入此return false炸庞,下次進來肯定跳過這里
        }

        // we know rect1 is in the beam, and rect2 is not

        // if rect1 is to the direction of, and rect2 is not, rect1 wins.
        // for example, for direction left, if rect1 is to the left of the source
        // and rect2 is below, then we always prefer the in beam rect1, since rect2
        // could be reached by going down.
        if (!isToDirectionOf(direction, source, rect2)) {
            return true;
        }

        // for horizontal directions, being exclusively in beam always wins
        if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
            return true;
        }        

        // for vertical directions, beams only beat up to a point:
        // now, as long as rect2 isn't completely closer, rect1 wins
        // e.g for direction down, completely closer means for rect2's top
        // edge to be closer to the source's top edge than rect1's bottom edge.
        return (majorAxisDistance(direction, source, rect1)
                < majorAxisDistanceToFarEdge(direction, source, rect2));
    }
  • 計算相對于導(dǎo)航方向的垂直方向上是否有重疊
<FocusFinder.java>
    // direction方向,rect1當前焦點荚斯,rect2待比較的view
    boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
        switch (direction) {
            case View.FOCUS_LEFT:
            case View.FOCUS_RIGHT:
                return (rect2.bottom > rect1.top) && (rect2.top < rect1.bottom);// 左右按鍵時比較y方向是否重疊
            case View.FOCUS_UP:
            case View.FOCUS_DOWN:
                return (rect2.right > rect1.left) && (rect2.left < rect1.right);// 上下按鍵時比較x方向是否重疊
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }
  • 計算是否完全在當前src的一側(cè)
<FocusFinder.java>
    // src當前焦點埠居,dest待比較的view
    boolean isToDirectionOf(int direction, Rect src, Rect dest) {// 比較dest是否完全在當前焦點的左/右/上/下
        switch (direction) {
            case View.FOCUS_LEFT:
                return src.left >= dest.right;
            case View.FOCUS_RIGHT:
                return src.right <= dest.left;
            case View.FOCUS_UP:
                return src.top >= dest.bottom;
            case View.FOCUS_DOWN:
                return src.bottom <= dest.top;
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }
  • 計算主軸方向距離
<FocusFinder.java>
    // 計算主軸方向距離
    static int majorAxisDistance(int direction, Rect source, Rect dest) {
        return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
    }

    static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
        switch (direction) {
            case View.FOCUS_LEFT:
                return source.left - dest.right;
            case View.FOCUS_RIGHT:
                return dest.left - source.right;
            case View.FOCUS_UP:
                return source.top - dest.bottom;
            case View.FOCUS_DOWN:
                return dest.top - source.bottom;
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }
  • 計算相對于主軸方向的垂直方向距離
<FocusFinder.java>
    // 計算次軸方向距離
    static int minorAxisDistance(int direction, Rect source, Rect dest) {
        switch (direction) {
            case View.FOCUS_LEFT:
            case View.FOCUS_RIGHT:
                // the distance between the center verticals
                return Math.abs(
                        ((source.top + source.height() / 2) -
                        ((dest.top + dest.height() / 2))));
            case View.FOCUS_UP:
            case View.FOCUS_DOWN:
                // the distance between the center horizontals
                return Math.abs(
                        ((source.left + source.width() / 2) -
                        ((dest.left + dest.width() / 2))));
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }
  • 計算相對距離,以FOCUS_LEFT為例事期,majorAxisDistance相當于當前焦點左側(cè)與比較view的右側(cè)的x軸距離滥壕,minorAxisDistance相當于在y軸方向上,當前焦點中心點與比較view的中心點的距離兽泣。計算13 * x2 * y2绎橘,這個13的權(quán)重系數(shù)不知道google是如何制定的,這里就理解為主軸的權(quán)重優(yōu)先級更高吧撞叨。(如果是我設(shè)計的話金踪,應(yīng)該會直接計算x和y的距離平方根進行比較了浊洞。)
<FocusFinder.java>
    int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
        return 13 * majorAxisDistance * majorAxisDistance
                + minorAxisDistance * minorAxisDistance;
    }
majorAxisDistance.jpg
  • 唉,這方法又得和上面的majorAxisDistance進行區(qū)分胡岔,以FOCUS_LEFT為例法希,同樣是計算x軸方向,但是majorAxisDistance計算的是souce的左側(cè)和待比較view的右側(cè)距離靶瘸,這個方法計算的是source的左側(cè)和待比較view的左側(cè)的距離:
<FocusFinder.java>
    static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
        return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
    }

    // 也是計算主軸方向苫亦,但是和majorAxisDistance有區(qū)別
    static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {
        switch (direction) {
            case View.FOCUS_LEFT:
                return source.left - dest.left;
            case View.FOCUS_RIGHT:
                return dest.right - source.right;
            case View.FOCUS_UP:
                return source.top - dest.top;
            case View.FOCUS_DOWN:
                return dest.bottom - source.bottom;
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }
majorAxisDistanceToFarEdge.jpg

遍歷過程中,每次進入isBetterCandidate成立后怨咪,closest都會更新為下一個焦點的最優(yōu)解屋剑,遍歷結(jié)束后,這個closest就是計算出來的下一個焦點诗眨,直接返回給上面的ViewRootImpl.performFocusNavigation唉匾,至此尋焦結(jié)束,接著用該查找出來的焦點view調(diào)用requestFocus匠楚,requestFocus之前已經(jīng)分析過巍膘,主要就是清除上一個焦點的狀態(tài),刷新當前焦點芋簿,流程結(jié)束峡懈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市与斤,隨后出現(xiàn)的幾起案子肪康,更是在濱河造成了極大的恐慌,老刑警劉巖撩穿,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件磷支,死亡現(xiàn)場離奇詭異,居然都是意外死亡冗锁,警方通過查閱死者的電腦和手機齐唆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冻河,“玉大人,你說我怎么就攤上這事茉帅∵缎穑” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵堪澎,是天一觀的道長擂错。 經(jīng)常有香客問我,道長樱蛤,這世上最難降的妖魔是什么钮呀? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任剑鞍,我火速辦了婚禮,結(jié)果婚禮上爽醋,老公的妹妹穿的比我還像新娘蚁署。我一直安慰自己,他們只是感情好蚂四,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布光戈。 她就那樣靜靜地躺著,像睡著了一般遂赠。 火紅的嫁衣襯著肌膚如雪久妆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天跷睦,我揣著相機與錄音筷弦,去河邊找鬼。 笑死抑诸,一個胖子當著我的面吹牛奸笤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哼鬓,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼监右,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了异希?” 一聲冷哼從身側(cè)響起健盒,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎称簿,沒想到半個月后扣癣,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡憨降,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年父虑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片授药。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡士嚎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出悔叽,到底是詐尸還是另有隱情莱衩,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布娇澎,位于F島的核電站笨蚁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜括细,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一伪很、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奋单,春花似錦锉试、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至匾七,卻和暖如春絮短,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昨忆。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工丁频, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人邑贴。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓席里,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拢驾。 傳聞我的和親對象是個殘疾皇子奖磁,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355