本文重點(diǎn)針對android TV開發(fā)的同學(xué)孽椰,分析遙控或鍵盤按鍵事件后焦點(diǎn)的分發(fā)機(jī)制难咕。尤其是剛從手機(jī)開發(fā)轉(zhuǎn)向TV開發(fā)的同學(xué)坤检,因?yàn)樵趯?shí)際開發(fā)中總會出現(xiàn)丟焦點(diǎn)或者焦點(diǎn)給到非預(yù)期的View或者ViewGroup匀奏。但此問題實(shí)際開發(fā)情況比較復(fù)雜蛔翅,本文僅限從android基本的分發(fā)機(jī)制出發(fā)瓶殃,對整體流程進(jìn)行梳理充包,并提供一些方法改變默認(rèn)行為以達(dá)到特定需求。如果有機(jī)會后續(xù)還會有更偏向?qū)崙?zhàn)的內(nèi)容更新遥椿。
一.前言
本文源碼基于android 7.0
二.核心流程
首先我們要知道按鍵事件和觸屏事件一樣都是從硬件通過系統(tǒng)驅(qū)動傳遞給android framework層的基矮,當(dāng)然這也不是我們要關(guān)注的重點(diǎn)。事件的入口就是ViewRootImpl的processKeyEvent方法冠场。
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent) q.mEvent;
// ①view樹處理事件消費(fèi)邏輯
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
....//處理ctrl鍵 也就是快捷按鍵相關(guān)邏輯
// 自動尋焦邏輯
if (event.getAction() == KeyEvent.ACTION_DOWN) {
int direction = 0;
....//根據(jù)keycode賦值direction
if (direction != 0) {
// ②尋找當(dāng)前界面的焦點(diǎn)view/viewGroup
View focused = mView.findFocus();
if (focused != null) {
// ③根據(jù)方向按鍵尋找合適的focus view
View v = focused.focusSearch(direction);
if (v != null && v != focused) {
...
// ④請求焦點(diǎn)
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return FINISH_HANDLED;
}
}
// 沒有找到焦點(diǎn) 給view最后一次機(jī)會處理按鍵事件
if (mView.dispatchUnhandledMove(focused, direction)) {
return FINISH_HANDLED;
}
} else {
// 如果當(dāng)前界面沒有焦點(diǎn)走這里
View v = focusSearch(null, direction);
if (v != null && v.requestFocus(direction)) {
return FINISH_HANDLED;
}
}
}
}
return FORWARD;
}
如上面的標(biāo)號家浇,就是尋焦的主要流程。其他的一些判斷代碼由于篇幅限制就不貼出了碴裙。下面分別對上述四個節(jié)點(diǎn)一一分析钢悲。
如果你去看了ViewRootImpl的源碼會發(fā)現(xiàn) 其中有三個內(nèi)部類都有這個方法,分別為:ViewPreImeInputStage舔株,EarlyPostImeInputStage,ViewPostImeInputStage他們都繼承InputStage類莺琳,上面的代碼是ViewPostImeInputStage類中的,從類名可以判斷按鍵的處理跟輸入法相關(guān)载慈,如果輸入法在前臺則會將事件先分發(fā)給輸入法惭等。
2.1 dispatchKeyEvent
想必你已經(jīng)很熟悉android事件分發(fā)機(jī)制了(當(dāng)然這也不是重點(diǎn)),key事件和touch事件原理都是一樣娃肿」径校總得來說就是由根view,這里就是DecorView它是一個FrameLayout它先分發(fā)給activity料扰,之后順序?yàn)閍ctivity-->PhoneWindow-->DecorView-->View樹凭豪,view樹中根據(jù)focusd path分發(fā),也就是從根節(jié)點(diǎn)開始直至focused view 為止的樹晒杈,具體流程可參看Android按鍵事件處理流程 -- KeyEvent嫂伞。 遍歷過程中一旦有節(jié)點(diǎn)返回true即表示消費(fèi)此事件,否則會一直傳遞下去拯钻。之所以說明這些帖努,是想提供一種攔截焦點(diǎn)的思路,如果按鍵事件傳遞過程中被消費(fèi)便不會走尋焦邏輯粪般。具體的流程后續(xù)會分享個大家拼余。
2.2 findFocus
此方法的核心就是找到當(dāng)前持有focus的view。
調(diào)用者mView即DecorView是FrameLayout 布局亩歹,沒復(fù)寫findFocus方法匙监,所以找到ViewGroup中的findFocus方法凡橱。
public View findFocus() {
if (isFocused()) {
return this;
}
if (mFocused != null) {
return mFocused.findFocus();
}
return null;
}
邏輯很簡單如果當(dāng)前view是focused的狀態(tài)直接返回自己,否則調(diào)用內(nèi)部間接持有focus的子view即mFocused亭姥,遍歷查找focused view稼钩。可見此番查找的路徑就是focused tree达罗。
2.3 focusSearch
根據(jù)開篇的核心流程坝撑,如果在上一步中找到了focused view,則會執(zhí)行view的focusSearch(int direction)方法粮揉,否則執(zhí)行focusSearch(View focused, int direction)巡李。這兩個方法分別來自于View和ViewGroup,但核心功能是一致的滔蝉,看代碼击儡。
/**
* 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傳遞,整個流程不受影響吊洼。
重點(diǎn)方法是FocusFinder.getInstance().findNextFocus(this, focused, direction);來看源碼训貌。
/**
* 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;
}
如果當(dāng)前焦點(diǎn)不為空,則先去讀取上層設(shè)置的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的實(shí)現(xiàn)递沪,如果上層給View/ViewGroup設(shè)置了setNextDownId/setNextLeftId/...,則android系統(tǒng)會從root view樹中查找此id對應(yīng)的view并返回综液,此分支尋焦邏輯結(jié)束款慨。可見為View/ViewGroup設(shè)置了nextDownId,nextLeftId等屬性可定向分配焦點(diǎn)。
若沒有設(shè)置上面的屬性谬莹,走下面的流程
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方法調(diào)用者是root附帽,也就是整個view樹都會進(jìn)行遍歷埠戳。FOCUS_BLOCK_DESCENDANTS這個屬性也很熟悉,如果為ViewGroup設(shè)置該屬性則其子view都不會統(tǒng)計到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屬性和當(dāng)前focused view的rect進(jìn)行“距離”的比較屋灌,最終得到“距離”最近的候選者并返回。至此应狱,整個尋焦邏輯結(jié)束共郭。感興趣的同學(xué)可研究內(nèi)部比較的算法。
在整個尋焦過程中疾呻,我們發(fā)現(xiàn)focusSearch方法是public的除嘹,因此可在view樹的某個節(jié)點(diǎn)復(fù)寫此方法并返回期望view從而達(dá)到“攔截”默認(rèn)尋焦的流程。同理岸蜗,addFocusables方法也是public的尉咕,復(fù)寫此方法可縮小比較view的范圍,提高效率璃岳。
2.4 requestFocus
最后一步是請求焦點(diǎn)年缎,根據(jù)代碼條件會出現(xiàn)兩個分支,一個是調(diào)用兩個參數(shù)的requestFocus(int direction, Rect previouslyFocusedRect)铃慷,此方法來自View但是ViewGroup有override单芜;另一個是一個參數(shù)的requestFocus(int direction),來自View且聲明為final犁柜。所以就要分上一步尋找到的focus目標(biāo)是View還是ViewGroup兩種情況進(jìn)行分析洲鸠。
如果是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();
}
}
大致也分為幾步:
- requestChildFocus 將焦點(diǎn)通過遞歸傳遞給父View,父View更新mFocused屬性萤悴,屬性值就是其中包含focused view的子View/ViewGroup瘾腰,這樣focused view tree就更新了。
- 各種回調(diào)focus狀態(tài)給各個監(jiān)聽器稚疹,我們常用的OnFocusChangedListener就是其中一種居灯。
- refreshDrawableState更新drawable狀態(tài)。
再來看如果是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屬性值
看注釋結(jié)合代碼邏輯可知怪嫌,此屬性決定requestFocus事件的傳遞順序。
- FOCUS_BLOCK_DESCENDANTS:子view不處理柳沙,本view直接處理岩灭,這樣就走到了上述View的requestFocus邏輯。
- FOCUS_BEFORE_DESCENDANTS:本view先處理赂鲤,如果消費(fèi)了事件噪径,子View不再處理柱恤,反之再交給子View處理。
- FOCUS_AFTER_DESCENDANTS:同上找爱,只不過順序變?yōu)樽覸iew先處理梗顺。
那這個值的默認(rèn)值是什么呢?其實(shí)在ViewGroup的構(gòu)造方法中調(diào)用了initViewGroup方法车摄,在這個方法中默認(rèn)設(shè)置了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;
}
由此可知吮播,根據(jù)方向鍵決定遍歷順序变屁,遍歷過程只要有一個子View處理了焦點(diǎn)事件便立即返回,整個流程結(jié)束意狠。很多常用的類都復(fù)寫過此方法粟关,比如 RecyclerView,ViewPager等等环戈。
三.總結(jié)
整篇文章源碼分析挺多的闷板,主要是為了找到可對尋焦邏輯有影響的關(guān)鍵節(jié)點(diǎn),實(shí)際上也是Android系統(tǒng)為上層開的"口子"谷市,方便根據(jù)實(shí)際需求改變默認(rèn)行為蛔垢。
- 消費(fèi)按鍵事件,事件在傳遞過程中如果被消費(fèi)便不會走尋焦邏輯迫悠,這是一種攔截焦點(diǎn)的思路鹏漆。
- focusSearch,上層可復(fù)寫此方法返回特定view创泄,來直接中斷尋焦流程艺玲。RecyclerView就復(fù)寫了這個方法,并且為LayoutManager留了一個onInterceptFocusSearch回調(diào)鞠抑,將攔截事件轉(zhuǎn)發(fā)給LayoutManager來實(shí)現(xiàn)特定的攔截焦點(diǎn)邏輯饭聚,比如常用的列表邊界攔截。
- 為View/ViewGroup設(shè)置了nextDownId,nextLeftId等屬性可定向分配焦點(diǎn)搁拙。
- addFocusables秒梳,復(fù)寫此方法可縮小/擴(kuò)大比較view的候選者,間接影響焦點(diǎn)的分配箕速。