Android 按鍵的焦點分發處理機制

本文重點針對android TV開發的同窗,分析遙控或鍵盤按鍵事件後焦點的分發機制。尤爲是剛從手機開發轉向TV開發的同窗,由於在實際開發中總會出現丟焦點或者焦點給到非預期的View或者ViewGroup。但此問題實際開發狀況比較複雜,本文僅限從android基本的分發機制出發,對總體流程進行梳理,並提供一些方法改變默認行爲以達到特定需求。若是有機會後續還會有更偏向實戰的內容更新。html

一.前言

本文源碼基於android 7.0java

二.核心流程

首先咱們要知道按鍵事件和觸屏事件同樣都是從硬件經過系統驅動傳遞給android framework層的,固然這也不是咱們要關注的重點。事件的入口就是ViewRootImpl的processKeyEvent方法。android

private int processKeyEvent(QueuedInputEvent q) {
    final KeyEvent event = (KeyEvent) q.mEvent;
    // ①view樹處理事件消費邏輯
    if (mView.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    if (shouldDropInputEvent(q)) {
        return FINISH_NOT_HANDLED;
    }
    ....//處理ctrl鍵 也就是快捷按鍵相關邏輯

    // 自動尋焦邏輯
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        int direction = 0;
        ....//根據keycode賦值direction
        if (direction != 0) {
            // ②尋找當前界面的焦點view/viewGroup
            View focused = mView.findFocus();
            if (focused != null) {
                // ③根據方向按鍵尋找合適的focus view
                View v = focused.focusSearch(direction);
                if (v != null && v != focused) {
                    ...
                    // ④請求焦點
                    if (v.requestFocus(direction, mTempRect)) {
                        playSoundEffect(SoundEffectConstants
                                .getContantForFocusDirection(direction));
                        return FINISH_HANDLED;
                    }
                }

                // 沒有找到焦點 給view最後一次機會處理按鍵事件
                if (mView.dispatchUnhandledMove(focused, direction)) {
                    return FINISH_HANDLED;
                }
            } else {
                // 若是當前界面沒有焦點走這裏
                View v = focusSearch(null, direction);
                if (v != null && v.requestFocus(direction)) {
                    return FINISH_HANDLED;
                }
            }
        }
    }
    return FORWARD;
}
複製代碼

如上面的標號,就是尋焦的主要流程。其餘的一些判斷代碼因爲篇幅限制就不貼出了。下面分別對上述四個節點一一分析。算法

若是你去看了ViewRootImpl的源碼會發現 其中有三個內部類都有這個方法,分別爲:ViewPreImeInputStage,EarlyPostImeInputStage,ViewPostImeInputStage他們都繼承InputStage類,上面的代碼是ViewPostImeInputStage類中的,從類名能夠判斷按鍵的處理跟輸入法相關,若是輸入法在前臺則會將事件先分發給輸入法。bash

2.1 dispatchKeyEvent

想必你已經很熟悉android事件分發機制了(固然這也不是重點),key事件和touch事件原理都是同樣。總得來講就是由根view,這裏就是DecorView它是一個FrameLayout它先分發給activity,以後順序爲activity-->PhoneWindow-->DecorView-->View樹,view樹中根據focusd path分發,也就是從根節點開始直至focused view 爲止的樹,具體流程可參看Android按鍵事件處理流程 -- KeyEvent。 遍歷過程當中一旦有節點返回true即表示消費此事件,不然會一直傳遞下去。**之因此說明這些,是想提供一種攔截焦點的思路,若是按鍵事件傳遞過程當中被消費便不會走尋焦邏輯。**具體的流程後續會分享個你們。 ####2.2 findFocus 此方法的核心就是找到當前持有focus的view。 調用者mView即DecorView是FrameLayout 佈局,沒複寫findFocus方法,因此找到ViewGroup中的findFocus方法。markdown

public View findFocus() {
        if (isFocused()) {
            return this;
        }
        if (mFocused != null) {
            return mFocused.findFocus();
        }
        return null;
    }
複製代碼

邏輯很簡單若是當前view是focused的狀態直接返回本身,不然調用內部間接持有focus的子view即mFocused,遍歷查找focused view。可見此番查找的路徑就是focused tree。app

2.3 focusSearch

根據開篇的核心流程,若是在上一步中找到了focused view,則會執行view的focusSearch(int direction)方法,不然執行focusSearch(View focused, int direction)。這兩個方法分別來自於View和ViewGroup,但核心功能是一致的,看代碼。ide

/** * Find the nearest view in the specified direction that can take focus. * This does not actually give focus to that view. * * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT * * @return The nearest focusable in the specified direction, or null if none * can be found. */
    public View focusSearch(@FocusRealDirection int direction) {
        if (mParent != null) {
            return mParent.focusSearch(this, direction);
        } else {
            return null;
        }
    }

    /** * Find the nearest view in the specified direction that wants to take * focus. * * @param focused The view that currently has focus * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and * FOCUS_RIGHT, or 0 for not applicable. */
    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傳遞,若是這個過程上層一直沒有干涉則會遍歷到頂層DecorView。回到上面的分水嶺,若是findFocus沒有找到focused view,即把null 賦值給focused傳遞,整個流程不受影響。 重點方法是**FocusFinder.getInstance().findNextFocus(this, focused, direction);**來看源碼。oop

/**
     * Find the next view to take focus in root's descendants, starting from the view * that currently is focused. * @param root Contains focused. Cannot be null. * @param focused Has focus now. * @param direction Direction to look. * @return The next focusable view, or null if none exists. */ 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; } 複製代碼

若是當前焦點不爲空,則先去讀取上層設置的specifiedFocusId。源碼分析

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

    /**
     * If a user manually specified the next view id for a particular direction,
     * use the root to look up the view.
     * @param root The root view of the hierarchy containing this view.
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD,
     * or FOCUS_BACKWARD.
     * @return The user specified next view, or null if there is none.
     */
    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;
    }
複製代碼

有木有很熟悉findUserSetNextFocus的實現,若是上層給View/ViewGroup設置了setNextDownId/setNextLeftId/...,則android系統會從root view樹中查找此id對應的view並返回,此分支尋焦邏輯結束。可見爲View/ViewGroup設置了nextDownId,nextLeftId等屬性可定向分配焦點。 若沒有設置上面的屬性,走下面的流程

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;
複製代碼
@Override
    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
        final int focusableCount = views.size();

        final int descendantFocusability = getDescendantFocusability();

        if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
            if (shouldBlockFocusForTouchscreen()) {
                focusableMode |= FOCUSABLES_TOUCH_MODE;
            }

            final int count = mChildrenCount;
            final View[] children = mChildren;

            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    child.addFocusables(views, direction, focusableMode);
                }
            }
        }
        ...
    }
複製代碼

邏輯也出來了,用一個集合存儲那些focusable而且可見的view,注意到addFocusables方法調用者是root,也就是整個view樹都會進行遍歷。FOCUS_BLOCK_DESCENDANTS這個屬性也很熟悉,若是爲ViewGroup設置該屬性則其子view都不會統計到focusable範圍中。 最終findNextFocus方法:

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 {
      ...
    }

    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);
    }
}
複製代碼

正常流程到這裏focused不爲空,focusedRect爲空,鍵值通常爲上下左右方向按鍵,所以走絕對方向。

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:
            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 focusable = focusables.get(i);

        // only interested in other non-root views
        if (focusable == focused || focusable == root) continue;

        // get focus bounds of other view in same coordinate system
        focusable.getFocusedRect(mOtherRect);
        root.offsetDescendantRectToMyCoords(focusable, mOtherRect);

        if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
            mBestCandidateRect.set(mOtherRect);
            closest = focusable;
        }
    }
    return closest;
}
複製代碼

核心算法就在這裏了,遍歷focusables集合,拿出每一個view的rect屬性和當前focused view的rect進行「距離」的比較,最終獲得「距離」最近的候選者並返回。至此,整個尋焦邏輯結束。感興趣的同窗可研究內部比較的算法。

在整個尋焦過程當中,咱們發現focusSearch方法是public的,所以可在view樹的某個節點複寫此方法並返回指望view從而達到「攔截」默認尋焦的流程。同理,addFocusables方法也是public的,複寫此方法可縮小比較view的範圍,提升效率。

2.4 requestFocus

最後一步是請求焦點,根據代碼條件會出現兩個分支,一個是調用兩個參數的requestFocus(int direction, Rect previouslyFocusedRect),此方法來自View可是ViewGroup有override;另外一個是一個參數的requestFocus(int direction),來自View且聲明爲final。因此就要分上一步尋找到的focus目標是View仍是ViewGroup兩種狀況進行分析。

若是是View,來看View的requestFocus源碼

public final boolean requestFocus(int direction) {
    return requestFocus(direction, null);
}

public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    return requestFocusNoSearch(direction, previouslyFocusedRect);
}

private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
    // need to be focusable
    if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
            (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
        return false;
    }

    // need to be focusable in touch mode if in touch mode
    if (isInTouchMode() &&
        (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
           return false;
    }

    // need to not have any parents blocking us
    if (hasAncestorThatBlocksDescendantFocus()) {
        return false;
    }

    handleFocusGainInternal(direction, previouslyFocusedRect);
    return true;
}
複製代碼

看來最終都會走到requestFocusNoSearch方法,並且其中的核心方法一看就知道是handleFocusGainInternal。

void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
    if (DBG) {
        System.out.println(this + " requestFocus()");
    }

    if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
        mPrivateFlags |= PFLAG_FOCUSED;

        View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

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

        if (mAttachInfo != null) {
            mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
        }

        onFocusChanged(true, direction, previouslyFocusedRect);
        refreshDrawableState();
    }
}
複製代碼

大體也分爲幾步:

  1. requestChildFocus 將焦點經過遞歸傳遞給父View,父View更新mFocused屬性,屬性值就是其中包含focused view的子View/ViewGroup,這樣focused view tree就更新了。
  2. 各類回調focus狀態給各個監聽器,咱們經常使用的OnFocusChangedListener就是其中一種。
  3. refreshDrawableState更新drawable狀態。

再來看若是是ViewGroup,ViewGroup的requestFocus源碼以下:

/**
 * {@inheritDoc}
 *
 * Looks for a view to give focus to respecting the setting specified by
 * {@link #getDescendantFocusability()}.
 *
 * Uses {@link #onRequestFocusInDescendants(int, android.graphics.Rect)} to
 * find focus within the children of this group when appropriate.
 *
 * @see #FOCUS_BEFORE_DESCENDANTS
 * @see #FOCUS_AFTER_DESCENDANTS
 * @see #FOCUS_BLOCK_DESCENDANTS
 * @see #onRequestFocusInDescendants(int, android.graphics.Rect) 
 */
@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    if (DBG) {
        System.out.println(this + " ViewGroup.requestFocus direction="
                + direction);
    }
    int descendantFocusability = getDescendantFocusability();

    switch (descendantFocusability) {
        case FOCUS_BLOCK_DESCENDANTS:
            return super.requestFocus(direction, previouslyFocusedRect);
        case FOCUS_BEFORE_DESCENDANTS: {
            final boolean took = super.requestFocus(direction, previouslyFocusedRect);
            return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
        }
        case FOCUS_AFTER_DESCENDANTS: {
            final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
            return took ? took : super.requestFocus(direction, previouslyFocusedRect);
        }
        default:
            throw new IllegalStateException("descendant focusability must be "
                    + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
                    + "but is " + descendantFocusability);
    }
}
複製代碼

這裏必需要清楚descendantFocusability屬性值 看註釋結合代碼邏輯可知,此屬性決定requestFocus事件的傳遞順序。

  • FOCUS_BLOCK_DESCENDANTS:子view不處理,本view直接處理,這樣就走到了上述View的requestFocus邏輯。
  • FOCUS_BEFORE_DESCENDANTS:本view先處理,若是消費了事件,子View再也不處理,反之再交給子View處理。
  • FOCUS_AFTER_DESCENDANTS:同上,只不過順序變爲子View先處理。

那這個值的默認值是什麼呢?其實在ViewGroup的構造方法中調用了initViewGroup方法,在這個方法中默認設置了descendantFocusability的屬性爲FOCUS_BEFORE_DESCENDANTS,也就是本View先處理。 最後看下onRequestFocusInDescendants的源碼:

/**
 * Look for a descendant to call {@link View#requestFocus} on.
 * Called by {@link ViewGroup#requestFocus(int, android.graphics.Rect)}
 * when it wants to request focus within its children.  Override this to
 * customize how your {@link ViewGroup} requests focus within its children.
 * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
 * @param previouslyFocusedRect The rectangle (in this View's coordinate system) * to give a finer grained hint about where focus is coming from. May be null * if there is no hint. * @return Whether focus was taken. */ @SuppressWarnings({"ConstantConditions"}) protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { int index; int increment; int end; int count = mChildrenCount; if ((direction & FOCUS_FORWARD) != 0) { index = 0; increment = 1; end = count; } else { index = count - 1; increment = -1; end = -1; } final View[] children = mChildren; for (int i = index; i != end; i += increment) { View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { if (child.requestFocus(direction, previouslyFocusedRect)) { return true; } } } return false; } 複製代碼

由此可知,根據方向鍵決定遍歷順序,遍歷過程只要有一個子View處理了焦點事件便當即返回,整個流程結束。不少經常使用的類都複寫過此方法,好比 RecyclerView,ViewPager等等。

三.總結

整篇文章源碼分析挺多的,主要是爲了找到可對尋焦邏輯有影響的關鍵節點,實際上也是Android系統爲上層開的"口子",方便根據實際需求改變默認行爲。

  • 消費按鍵事件,事件在傳遞過程當中若是被消費便不會走尋焦邏輯,這是一種攔截焦點的思路。
  • focusSearch,上層可複寫此方法返回特定view,來直接中斷尋焦流程。RecyclerView就複寫了這個方法,而且爲LayoutManager留了一個onInterceptFocusSearch回調,將攔截事件轉發給LayoutManager來實現特定的攔截焦點邏輯,好比經常使用的列表邊界攔截。
  • 爲View/ViewGroup設置了nextDownId,nextLeftId等屬性可定向分配焦點。
  • addFocusables,複寫此方法可縮小/擴大比較view的候選者,間接影響焦點的分配。
相關文章
相關標籤/搜索