上兩篇文章分別單獨(dú)分析了KeyEvent在View樹中分發(fā)和View獲得焦點(diǎn)的過(guò)程炊汤,實(shí)際上這兩個(gè)并不是獨(dú)立的躬贡,當(dāng)我們按下按鍵的時(shí)候會(huì)發(fā)現(xiàn)如果我們不攔截按鍵事件抵拘,按鍵事件就會(huì)轉(zhuǎn)換成焦點(diǎn)View的切換祖凫,現(xiàn)在就開始分析這個(gè)轉(zhuǎn)換的過(guò)程。
1.ViewRootImpl中的整體過(guò)程
第一篇中提到過(guò)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代表這個(gè)按鍵事件已經(jīng)被消耗掉了谋竖,false代表還沒被消耗。默認(rèn)返回的是fasle承匣,只有我們?cè)谶@個(gè)過(guò)程中想要攔截蓖乘、處理了按鍵事件,才會(huì)返回true韧骗。
最后會(huì)把這個(gè)結(jié)果向上一層一層地反饋到按鍵事件產(chǎn)生的位置嘉抒,這個(gè)位置就是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 {//尋找焦點(diǎn)View
if (performFocusNavigation(event)) {
return FINISH_HANDLED;
}
}
}
}
mView就是DecorView袍暴,是否已被消耗的結(jié)果的終點(diǎn)就是這里些侍。如果是被消耗了就直接返回,沒有政模,那就調(diào)用performFocusNavigation方法岗宣,從方法名字就可以看出這個(gè)方法就是要將按鍵事件轉(zhuǎn)換成焦點(diǎn)的移動(dòng),方法如下:
ViewRootImpl中
private boolean performFocusNavigation(KeyEvent event) {
int direction = 0;
//將按鍵事件的上下左右轉(zhuǎn)換成焦點(diǎn)移動(dòng)方向的上下左右
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();//找出此時(shí)這個(gè)有焦點(diǎn)的View
if (focused != null) {
//調(diào)用它的focusSearch方法淋样,顧名思義尋找這個(gè)方向上的下一個(gè)獲得焦點(diǎn)的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耗式,讓它獲得焦點(diǎn)
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;
}
這個(gè)方法中一氣呵成完成了按鍵事件轉(zhuǎn)換成焦點(diǎn)View變化的全部過(guò)程,可以概括為以下4個(gè)步驟:
- 將按鍵事件的上下左右轉(zhuǎn)換成焦點(diǎn)移動(dòng)方向的上下左右
- 找出View樹中有焦點(diǎn)的View
- 調(diào)用焦點(diǎn)View的focusSearch方法尋找下一個(gè)獲得焦點(diǎn)的View
- 調(diào)用下一個(gè)獲得焦點(diǎn)的View的requestFocus方法习蓬,讓它獲得焦點(diǎn)
下面分別分析第2纽什、3步的過(guò)程措嵌,第4步請(qǐng)看上一篇的分析
2.尋找有焦點(diǎn)的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標(biāo)志位以及ViewGroup的mFocused成員變量,首先是ViewGroup判斷自己是不是有焦點(diǎn)企巢,然后再判斷自己是不是包含了有焦點(diǎn)的子View枫慷,多次按照焦點(diǎn)的路徑遍歷就找出了焦點(diǎn)View。
3.尋找下一個(gè)獲得焦點(diǎn)的View
View的focusSearch方法
此刻已經(jīng)找出了焦點(diǎn)View,需要調(diào)用它的focusSearch去尋找下一個(gè)焦點(diǎn)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重寫了這個(gè)方法,如果自己是RootNamespace那就調(diào)用FocusFinder去尋找View誉裆,但什么時(shí)候isRootNamespace()成立顿颅,我現(xiàn)在還沒遇到過(guò)舔腾,所以一般情況下最后會(huì)調(diào)用到ViewRootImpl的focusSearch方法砰奕,這個(gè)方法如下:
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尼夺,看來(lái)尋找下一個(gè)焦點(diǎn)View的重任全部都封裝在了這個(gè)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居然是線程單例的绍些,而不是進(jìn)程單例的,這樣做的原因大概是一方面發(fā)揮單例優(yōu)勢(shì)耀鸦,避免頻繁創(chuàng)建FocusFinder柬批,畢竟這是一個(gè)比較基本的功能,節(jié)約資源提高效率袖订;另一方面氮帐,萬(wàn)一其他線程也要調(diào)用FocusFinder去做一些尋找View的事情,如果進(jìn)程單例那不就影響主線程的效率了洛姑?所以線程單例最合適吧揪漩。
添加所有可以獲得焦點(diǎn)的View
言歸正傳,繼續(xù)看它的findNextFocus方法吏口,如下:
FocusFinder中
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
//找出焦點(diǎn)跳轉(zhuǎn)View的范圍
ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
if (focused != null) {//找出在xml中指定的該方向的下一個(gè)獲得焦點(diǎn)的View
next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
}
if (next != null) {//指定了直接返回
return next;
}
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
//從最頂點(diǎn)奄容,將所有可以獲得焦點(diǎn)的View添加到focusables中
effectiveRoot.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
//從中找出下一個(gè)可以獲得焦點(diǎn)的View
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
首先找出焦點(diǎn)跳轉(zhuǎn)的View的范圍,我這里測(cè)試時(shí)effectiveRoot就是DecorView产徊,也就是整個(gè)View樹中View都在考慮的范圍內(nèi)昂勒。然后調(diào)用findNextUserSpecifiedFocus方法去獲取我們手動(dòng)為這個(gè)View設(shè)置的上下左右的焦點(diǎn)View,對(duì)應(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樹中所有可能獲得焦點(diǎn)的View都放進(jìn)了focusables這個(gè)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) {//攔截了焦點(diǎn)塘娶,只判斷、添加自己
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;
}
}
//對(duì)所有子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攔截焦點(diǎn)痊夭,那就不用再考慮子View了刁岸;如果ViewGroup在子View之前獲得焦點(diǎn),那就先添加她我,反之后添加虹曙;對(duì)于兄弟View迫横,在添加之前還要對(duì)它們進(jìn)行排序,排序的依據(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);
}
直接判斷自己是否可以獲得焦點(diǎn),可以的話就把自己加到views中去疏哗。
到這里所有的可以獲得焦點(diǎn)的View 都被添加到了focusables中去了呛讲,在這個(gè)過(guò)程中與方向還沒有關(guān)系,只是枚舉添加了所有可能的View返奉。
尋找最優(yōu)View
有了focusables列表圣蝎,這時(shí)調(diào)用同名方法findNextFocus在focusables找出最合適的那個(gè)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的坐標(biāo)變成rootView下的坐標(biāo)
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)徘公,下面的分析以按右鍵為例,對(duì)應(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í)关面,將初始的位置設(shè)為當(dāng)前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();
//遍歷所有的可獲得焦點(diǎn)的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;
//獲得這個(gè)View的rect,并把它調(diào)整到和focusedRect一致
// get focus bounds of other view in same coordinate system
focusable.getFocusedRect(mOtherRect);
root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
//判斷這個(gè)View是不是比mBestCandidateRect更優(yōu)
if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
//更優(yōu)十厢,那么將它設(shè)置成mBestCandidateRect等太,并將closest賦值
mBestCandidateRect.set(mOtherRect);
closest = focusable;
}
}
return closest;
}
實(shí)際上尋找的過(guò)程就是在比較各個(gè)View的占用區(qū)域的相對(duì)關(guān)系,這里首先設(shè)置了一個(gè)mBestCandidateRect代表最合適的View的區(qū)域蛮放,對(duì)于向右缩抡,是焦點(diǎn)View左邊間距一像素的同等大小的一個(gè)區(qū)域,顯然這是最不合適的區(qū)域包颁,這只是一個(gè)初始值瞻想。然后就開始遍歷所有可能的View,調(diào)用isBetterCandidate方法去判斷娩嚼,這里就不展開這個(gè)方法蘑险,太啰嗦了,用下面的圖代替岳悟。
假如下圖中View1此時(shí)有焦點(diǎn)佃迄,按下右鍵時(shí)會(huì)怎么樣呢?
實(shí)際上判斷的依據(jù)和下圖中的畫的各種虛線有關(guān)
按下右鍵贵少,初始的最佳區(qū)域就是紅色虛線框的位置呵俏,顯示是最不適合的,然后再遍歷所有的可能的View滔灶,滿足以下兩點(diǎn)才可以擊敗紅色虛線框的位置成為待選的View:
- 以紫色虛線作為標(biāo)準(zhǔn)普碎,View的右邊線必須在紫色虛線的右邊
- 以黑色虛線作為標(biāo)準(zhǔn),View的左邊線必須在黑色虛線的右邊
顯然View2和View3都滿足宽气,這時(shí)就需要進(jìn)一步的比較了随常,進(jìn)一步比較需要參考View的上下兩邊,滿足以下兩點(diǎn)的最優(yōu):
- View的下邊線在上面的那條藍(lán)色虛線之下
- View的上邊線在下面的那條藍(lán)色虛線之上
也就是這個(gè)View的與焦點(diǎn)View在上下兩邊上有重疊的區(qū)域就可以了萄涯,所以View3是最優(yōu)的位置绪氛,View3將獲得焦點(diǎn)。
還有一種情況涝影,要是兩個(gè)View都與焦點(diǎn)View在上下兩邊有重疊的區(qū)域枣察,那誰(shuí)更優(yōu)呢?如下:
對(duì)于向右尋找焦點(diǎn)燃逻,這時(shí)判斷的依據(jù)就和圖中的兩個(gè)距離箭頭major和minor的長(zhǎng)度有關(guān)序目,計(jì)算的公式,distance越小越有優(yōu)伯襟,13是一個(gè)常量系數(shù)猿涨,表示以左右間距作為主要的判斷依據(jù),但不排除上下間距逆襲的可能姆怪,所以越靠近焦點(diǎn)View的中心叛赚,就越有可能獲得焦點(diǎn)。
調(diào)試的布局如下稽揭,可以微微調(diào)整bias俺附,驗(yàn)證結(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>