上兩篇文章分別單獨分析了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個步驟:
- 將按鍵事件的上下左右轉(zhuǎn)換成焦點移動方向的上下左右
- 找出View樹中有焦點的View
- 調(diào)用焦點View的focusSearch方法尋找下一個獲得焦點的View
- 調(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)

按下右鍵,初始的最佳區(qū)域就是紅色虛線框的位置,顯示是最不適合的,然后再遍歷所有的可能的View,滿足以下兩點才可以擊敗紅色虛線框的位置成為待選的View:
- 以紫色虛線作為標準,View的右邊線必須在紫色虛線的右邊
- 以黑色虛線作為標準,View的左邊線必須在黑色虛線的右邊
顯然View2和View3都滿足,這時就需要進一步的比較了,進一步比較需要參考View的上下兩邊,滿足以下兩點的最優(yōu):
- View的下邊線在上面的那條藍色虛線之下
- View的上邊線在下面的那條藍色虛線之上
也就是這個View的與焦點View在上下兩邊上有重疊的區(qū)域就可以了,所以View3是最優(yōu)的位置,View3將獲得焦點。
還有一種情況,要是兩個View都與焦點View在上下兩邊有重疊的區(qū)域,那誰更優(yōu)呢?如下:

對于向右尋找焦點,這時判斷的依據(jù)就和圖中的兩個距離箭頭major和minor的長度有關(guān),計算的公式,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>