如果Activity里有EditText玖姑,那么打開Activity后端朵,EditText會自動獲取焦點社付。
為什么呢承疲,很多時候我們不想要這個效果,參照網(wǎng)上的方法將father layout設(shè)置成獲取焦點就解決問題鸥咖。知其然知其所以然燕鸽,翻了一下代碼,答案隱藏在ViewRootImpl.performTraversals方法中啼辣,就是那個view繪制的核心方法啊研,中間有一段:
private void performTraversals() {
//...
if (mFirst) {
// handle first focus request
if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: mView.hasFocus()="
+ mView.hasFocus());
if (mView != null) {
if (!mView.hasFocus()) {
mView.requestFocus(View.FOCUS_FORWARD);
if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: requested focused view="
+ mView.findFocus());
} else {
if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: existing focused view="
+ mView.findFocus());
}
}
}
//...
}
當(dāng)是第一個view時,會調(diào)用requestFocus獲取焦點熙兔。ViewRootImpl相關(guān)內(nèi)容自行看android的窗口機制悲伶,這個不是今日的目標(biāo),本文要講的是:
- requestFocus和背后的焦點分發(fā)機制住涉;
- clearFocus真的無效嗎麸锉?
- 如果讓焦點按意志移動。
寫了個測試用的demo舆声,上面很多EditText啦花沉,還有上下左右前后等焦點的控制鍵柳爽。
View是否能獲取焦點
讓View獲取焦點,直接調(diào)用requestFocus碱屁,最終會調(diào)用到requestFocusNoSearch:
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校驗View的屬性磷脯,獲取焦點的前提條件是“可見的”和“可聚焦的”,并且“可聚焦的”需要同時符合:
android:focusable="true"
android:focusableInTouchMode="true"
接著調(diào)用了hasAncestorThatBlocksDescendantFocus娩脾,這個需要了解View的descendantFocusability屬性赵誓。這對我來說是新概念,以前沒有用過柿赊,后文還會涉及俩功,現(xiàn)在先儲備知識。
- beforeDescendants:ViewGroup會優(yōu)先其子view而獲取到焦點
- afterDescendants:ViewGroup只有當(dāng)其子view不需要獲取焦點時才獲取焦點
- blocksDescendants:ViewGroup會覆蓋子view而直接獲得焦點
private boolean hasAncestorThatBlocksDescendantFocus() {
final boolean focusableInTouchMode = isFocusableInTouchMode();
ViewParent ancestor = mParent;
while (ancestor instanceof ViewGroup) {
final ViewGroup vgAncestor = (ViewGroup) ancestor;
if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS
|| (!focusableInTouchMode && vgAncestor.shouldBlockFocusForTouchscreen())) {
return true;
} else {
ancestor = vgAncestor.getParent();
}
}
return false;
}
hasAncestorThatBlocksDescendantFocus就很好理解碰声,如果有祖先ViewGroup設(shè)置成blocksDescendants诡蜓,那么它的子孫View都不能獲取焦點。
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();
}
}
handleFocusGainInternal實現(xiàn)View獲取焦點的具體邏輯胰挑,所以requestFocusNoSearch默認(rèn)返回true蔓罚。handleFocusGainInternal里面最重要的是調(diào)用了mParent.requestChildFocus,通知它的父view處理焦點瞻颂。mParent的類型是ViewParent豺谈,每一個view都會保存它的父view,基本上實現(xiàn)類就是ViewGroup蘸朋。
然后觸發(fā)onFocusChanged這個listener核无,最后觸發(fā)invalidate進行ui更新。
在繼續(xù)探究requestChildFocus的代碼前团南,先認(rèn)真講講焦點的分發(fā)過程。
焦點分發(fā)過程
有個大家族吐根,已經(jīng)經(jīng)歷多代,族人角色可以這樣定義:
- 成員:View
- 有子女的成員:ViewGroup
- 輩分最高的長老:DecorView
家族中有一件寶貝拷橘,持有在一名成員手上。別的家族想?yún)⒂^冗疮,首先需要找長老。
長老不會一個個成員問檩帐,而是先找大兒子問,再找二兒子問湃密,如此類推四敞。兒子們也是這樣問自己的兒子拔妥,過程也是如此類推忿危。一層層地問,直到最后找到寶貝的持有人没龙,再一層層向上通知铺厨。
寶貝就是焦點,尋找寶貝的過程就是焦點分發(fā)的過程兜畸。
ViewGroup對焦點的處理
看回handleFocusGainInternal里的requestChildFocus努释,view如果需要獲取焦點,需要通知它的父view處理咬摇,所以我們來看ViewGroup的requestChildFocus:
@Override
public void requestChildFocus(View child, View focused) {
if (DBG) {
System.out.println(this + " requestChildFocus()");
}
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
// Unfocus us, if necessary
super.unFocus(focused);
// We had a previous notion of who had focus. Clear it.
if (mFocused != child) {
if (mFocused != null) {
mFocused.unFocus(focused);
}
mFocused = child;
}
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
首先會調(diào)用unFocus清除自己的焦點,mFocused表示ViewGroup內(nèi)部是否持有焦點煞躬,如果mFocused不是目標(biāo)獲取焦點的child肛鹏,那么再清除當(dāng)前mFocused的焦點,并將child賦給mFocused恩沛。
最后繼續(xù)通過mParent遞歸調(diào)用requestChildFocus在扰,直到頂層view,保證焦點唯一雷客。
ViewGroup也可以獲取焦點芒珠,和上面View的requestFocus方法不同:
@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屬性的鋪墊,ViewGroup的requestFocus很容易理解搅裙。block狀態(tài)時皱卓,焦點查找交還給父View;before狀態(tài)時部逮,優(yōu)先自己獲取焦點娜汁;after狀態(tài)時,優(yōu)先子view獲取焦點兄朋。
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;
}
onRequestFocusInDescendants方法就是向子view詢問焦點的邏輯掐禁,區(qū)分正反兩種查找方向。只要有一個view成功獲取到焦點颅和,就返回true傅事。
清除焦點
上面沒有講view失去焦點的處理,現(xiàn)在來看下ViewGroup的unFocus峡扩,還要探究一下clearFocus“無效”的背后原理蹭越。
@Override
void unFocus(View focused) {
if (DBG) {
System.out.println(this + " unFocus()");
}
if (mFocused == null) {
super.unFocus(focused);
} else {
mFocused.unFocus(focused);
mFocused = null;
}
}
ViewGroup的unFocus,最終調(diào)用了View的unFocus有额。
void unFocus(View focused) {
if (DBG) {
System.out.println(this + " unFocus()");
}
clearFocusInternal(focused, false, false);
}
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
mPrivateFlags &= ~PFLAG_FOCUSED;
if (propagate && mParent != null) {
mParent.clearChildFocus(this);
}
onFocusChanged(false, 0, null);
refreshDrawableState();
if (propagate && (!refocus || !rootViewRequestFocus())) {
notifyGlobalFocusCleared(this);
}
}
}
clearFocusInternal是真正操作焦點失去的地方般又,通過mParent調(diào)用ViewGroup的clearChildFocus。
@Override
public void clearChildFocus(View child) {
if (DBG) {
System.out.println(this + " clearChildFocus()");
}
mFocused = null;
if (mParent != null) {
mParent.clearChildFocus(this);
}
}
clearChildFocus將當(dāng)前mFocused置空寄悯,通過遞歸向上處理直到頂層view猜旬,保證整顆view樹失去焦點洒擦。
注意熟嫩,unFocus我們并不能調(diào)用掸茅,View提供clearFocus柠逞,內(nèi)部同樣調(diào)用clearFocusInternal板壮,它們不同的地方是refocus傳入不同。
boolean rootViewRequestFocus() {
final View root = getRootView();
return root != null && root.requestFocus();
}
refocus的不同撒璧,決定是否會觸發(fā)rootViewRequestFocus沪悲,因此clearFocus“無效”的問題很好理解殿如。如果一個頁面只有一個EditText涉馁,使用clearFocus清除焦點烤送,馬上地糠悯,焦點又被設(shè)置上啦,所以會有清除無效的錯覺试和。因此阅悍,讓父view自動獲取焦點是很好的解決方法节视。
焦點查找
@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;
}
View和ViewGroup提供了focusSearch方法進行焦點查找寻行,入?yún)⑹钱?dāng)前獲取焦點的view和目標(biāo)查找方向拌蜘,返回下一個應(yīng)該獲取焦點的view。focusSearch調(diào)用的是FocusFinder類,直接來看FocusFinder最常用的findNextFocus:
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
//1
View next = null;
if (focused != null) {
next = findNextUserSpecifiedFocus(root, focused, direction);
}
if (next != null) {
return next;
}
//2
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;
}
1贞滨、預(yù)設(shè)焦點
看標(biāo)記1晓铆,調(diào)用了findNextUserSpecifiedFocus骄噪,查找用戶預(yù)設(shè)不同方向獲取焦點的View链蕊。
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;
}
里面調(diào)用了View.findUserSetNextFocus滔韵,在xml文件中陪蜻,我們可以使用android:nextFocusLeft贱鼻、android:nextFocusRight滋将、android:nextFocusUp随闽、android:nextFocusDown齿兔、android:nextFocusForward指定對應(yīng)的View分苇。
2医寿、自動查找焦點
如果沒有預(yù)設(shè),就由程序自動查找须眷。標(biāo)記2收集root下所有能獲取焦點的view沟突,調(diào)用重載版本的findNextFocus方法。
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
int direction, ArrayList<View> focusables) {
//1
//...
//2
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);
}
}
這里我省略了標(biāo)記1一大段代碼扩劝,大約邏輯是計算焦點的矩形范圍职辅,如果當(dāng)前已經(jīng)有view得到焦點域携,直接通過view計算即可秀鞭;如果沒有,那么通過root和方向計算拆内,比較簡單麸恍,就不貼出來占地方。
標(biāo)記2根據(jù)查找方向使用不同算法刻肄,前項和后項使用findNextFocusInRelativeDirection融欧,上下左右使用findNextFocusInAbsoluteDirection噪馏。
對于前項和后項這種按序的查找欠肾,很容易想到需要對view進行排序,這里使用了內(nèi)部類SequentialFocusComparator粹淋,根據(jù)view矩形的高低左右比較桃移。
對于上下左右方向借杰,需要在能獲取焦點view中比較出最適合的一個进泼。首先會設(shè)置一個差的結(jié)果,然后對每一個可以獲取焦點的view調(diào)用isBetterCandidate,找到方向上離自己最近最合適的一個刷袍。算法比較復(fù)雜樊展,有興趣自行研究专缠。
private fun doFocusUp() {
currentFocus?.let {
currentFocus.focusSearch(View.FOCUS_UP)?.requestFocus()
}
}
private fun doFocusDown() {
currentFocus?.let {
currentFocus.focusSearch(View.FOCUS_DOWN)?.requestFocus()
}
}
private fun doFocusLeft() {
currentFocus?.let {
currentFocus.focusSearch(View.FOCUS_LEFT)?.requestFocus()
}
}
private fun doFocusRight() {
currentFocus?.let {
currentFocus.focusSearch(View.FOCUS_RIGHT)?.requestFocus()
}
}
private fun doFocusForward() {
val focusView = currentFocus ?: return
FocusFinder.getInstance().findNextFocus(rv_list, focusView, View.FOCUS_BACKWARD)?.requestFocus()
}
private fun doFocusNext() {
val focusView = currentFocus ?: return
FocusFinder.getInstance().findNextFocus(rv_list, focusView, View.FOCUS_FORWARD)?.requestFocus()
}
demo里上下左右前后六個方向就是使用FocusFinder實現(xiàn)涝婉。focusSearch限制了只能使用上下左右四個方向,前后兩個方向直接調(diào)用FocusFinder吩跋。
小結(jié)
本文總結(jié)了android焦點常用的方法和原理锌钮,有建議或疑問可以交流一下。