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