如需轉(zhuǎn)載請?jiān)u論或簡信,并注明出處,未經(jīng)允許不得轉(zhuǎn)載

系列文章
- android tv常見問題(一)焦點(diǎn)查找規(guī)律
- android tv常見問題(二)如何監(jiān)聽ViewGroup子View的焦點(diǎn)狀態(tài)
- android tv常見問題(三)RecyclerView的焦點(diǎn)記憶
- android tv常見問題(四)焦點(diǎn)變化時(shí),Recyclerview是如何進(jìn)行滾動(dòng)的
github地址
https://github.com/Geekholt/TvFocus
目錄

期望結(jié)果
只要ViewGroup的內(nèi)部或自身存在焦點(diǎn),ViewGroup就始終保持聚焦樣式。

實(shí)際結(jié)果
在不做任何處理的情況下,一個(gè)頁面只會存在一個(gè)聚焦的view。

問題分析
如果我們先不考慮完全重寫Android焦點(diǎn)框架的情況,我們能否做一些特殊處理,來實(shí)現(xiàn)我們期望的結(jié)果呢?從期望結(jié)果描述來看,其實(shí)實(shí)現(xiàn)邏輯還是比較清晰的,就是我們需要拿到兩個(gè)回調(diào):
- 當(dāng)ViewGroup自身或者內(nèi)部的View獲得焦點(diǎn)的回調(diào)。
- 當(dāng)ViewGroup自身或者內(nèi)部的View失去焦點(diǎn)的回調(diào)。
這就需要我們來看一下View和ViewGroup在requestFocus的過程中觸發(fā)了哪些回調(diào)。
View#requestFocus
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
return requestFocusNoSearch(direction, previouslyFocusedRect);
}
View#requestFocusNoSearch
requestFocusNoSearch校驗(yàn)View的屬性,獲取焦點(diǎn)的前提條件是“可見的”和“可聚焦的”。
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// focusable且visible
if ((mViewFlags & FOCUSABLE) != FOCUSABLE
|| (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
// 如果是觸摸屏,需要focusableInTouchMode屬性為true
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}
// 判斷parent viewGroup是否設(shè)置了FOCUS_BLOCK_DESCENDANTS
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
//實(shí)現(xiàn)View獲取焦點(diǎn)的具體邏輯
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
View#handleFocusGainInternal
這個(gè)是最核心的聚焦邏輯
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " requestFocus()");
}
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
//當(dāng)前view沒有被聚焦才會進(jìn)入下面的邏輯
//將view的聚焦標(biāo)識設(shè)置為已聚焦
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
if (mParent != null) {
//通知父控件即將獲取焦點(diǎn)
mParent.requestChildFocus(this, this);
updateFocusedInCluster(oldFocus, direction);
}
if (mAttachInfo != null) {
//觸發(fā)全局OnGlobalFocusChangeListener的回調(diào)
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
//觸發(fā)將要被聚焦的View的OnFocusChangeListener回調(diào)
onFocusChanged(true, direction, previouslyFocusedRect);
//系統(tǒng)焦點(diǎn)樣式變化,比如我們在Drawable中設(shè)置了focused_state來區(qū)別聚焦或未聚焦樣式
refreshDrawableState();
}
}
ViewGroup#requestChildFocus
public void requestChildFocus(View child, View focused) {
if (DBG) {
System.out.println(this + " requestChildFocus()");
}
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
//被聚焦的ViewGroup先會調(diào)用一下View的unFocus方法
super.unFocus(focused);
if (mFocused != child) {
if (mFocused != null) {
//mFocused就是當(dāng)前ViewGroup下持有焦點(diǎn)的View或者ViewGroup,是串聯(lián)整個(gè)焦點(diǎn)路徑的屬性
//注意:View的unFocu方法和ViewGroup的unFocus方法實(shí)現(xiàn)是不一樣的
mFocused.unFocus(focused);
}
//把當(dāng)前最新的焦點(diǎn)child賦值給mFocused
mFocused = child;
}
if (mParent != null) {
//繼續(xù)往上通知parent
mParent.requestChildFocus(this, focused);
}
}
View的unFocus方法和ViewGroup的unFocus方法實(shí)現(xiàn)是不一樣的,這里如果沒有看清楚可能就會對焦點(diǎn)事件的回調(diào)的方法出現(xiàn)一些誤會。
ViewGroup#unFocus
這個(gè)方法實(shí)際上不是失焦的邏輯,而是一個(gè)遞歸調(diào)用,最終會執(zhí)行View的unFocus方法。View的unFocus方法才是真正的失焦邏輯。
void unFocus(View focused) {
if (DBG) {
System.out.println(this + " unFocus()");
}
if (mFocused == null) {
super.unFocus(focused);
} else {
//遞歸調(diào)用,最終會執(zhí)行當(dāng)前聚集的View的unFocus方法
mFocused.unFocus(focused);
mFocused = null;
}
}
View#unFocus
有兩個(gè)地方會調(diào)用到這個(gè)方法:
- 在ViewGroup的unFocus方法中遞歸調(diào)用,最終執(zhí)行當(dāng)前聚焦的view的unfocus方法。
- 在ViewGroup中調(diào)用super.unFocus()。這個(gè)是在requestChildFocus方法中進(jìn)行調(diào)用的,用于在子View聚焦之前,先清除一下自身的焦點(diǎn)。
總的來說就是兩種情況,當(dāng)前聚焦的View失去焦點(diǎn)和下一個(gè)要被聚焦的View的ViewGroup清除自身焦點(diǎn)。也就是說:
對于View來說,每次聚焦或者失焦都會觸發(fā)View的unFocus方法。
對于ViewGroup來說,當(dāng)焦點(diǎn)從ViewGroup外進(jìn)入到ViewGroup內(nèi)的子View上時(shí),會觸發(fā)View的unFocus方法。而ViewGroup內(nèi)的子View失去焦點(diǎn)時(shí),不會觸發(fā)View的unFocus方法。
這就直接關(guān)系到ViewGroup的onFocusChanged方法是否執(zhí)行,具體邏輯看View的clearFocusInternal方法。
void unFocus(View focused) {
if (DBG) {
System.out.println(this + " unFocus()");
}
clearFocusInternal(focused, false, false);
}
View#clearFocusInternal
clearFocusInternal方法還被clearFocus方法所調(diào)用,注意區(qū)別。clearFocus方法是通過用戶主動(dòng)調(diào)用而失去焦點(diǎn),而unFocus方法是在新的焦點(diǎn)要被聚焦之前,系統(tǒng)內(nèi)部調(diào)用的。
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
//view存在焦點(diǎn)才會執(zhí)行這里面的邏輯
//將view的聚焦標(biāo)識設(shè)置為未聚焦
mPrivateFlags &= ~PFLAG_FOCUSED;
if (propagate && mParent != null) {
//只有主動(dòng)調(diào)用clearfocus方法時(shí)才會執(zhí)行
mParent.clearChildFocus(this);
}
//onFocusChanged回調(diào)
onFocusChanged(false, 0, null);
//系統(tǒng)的焦點(diǎn)樣式變化
refreshDrawableState();
if (propagate && (!refocus || !rootViewRequestFocus())) {
//只有主動(dòng)調(diào)用clearfocus方法時(shí)才會執(zhí)行全局焦點(diǎn)變化監(jiān)聽的方法
//這是由于在unFocus之后,handleFocusGainInternal方法中會繼續(xù)執(zhí)行全局焦點(diǎn)變化監(jiān) 聽,這里沒必要重復(fù)執(zhí)行。
notifyGlobalFocusCleared(this);
}
}
}
View#clearFocus
public void clearFocus() {
if (DBG) {
System.out.println(this + " clearFocus()");
}
clearFocusInternal(null, true, true);
}
requestFocus小結(jié)
將要失焦的View:focused
將要失焦的View上層的所有ViewGroup:focusedParent
將要被聚焦的View:next
將要被聚焦的View上層的所有ViewGroup:nextParent
一次聚焦事件回調(diào)方法執(zhí)行的順序是這樣的:
- nextParent.requestChildFocus(focused , focused) ;
- nextParent.onFocusChanged(false, 0, null);
- focused.onFocusChanged(false, 0, null) ;
- mTreeObserver.dispatchOnGlobalFocusChange(focused , next);
- next.onFocusChanged(true, direction, previouslyFocusedRect)
如果我們主動(dòng)調(diào)用了clearFocus方法來失去焦點(diǎn),那么回調(diào)方法的執(zhí)行順序是這樣的:
- mParent.clearChildFocus(focused);
- focused.onFocusChanged(false, 0, null);
- mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused , null);
聚焦流程基本分析完了,回到我們的問題,我們需要監(jiān)聽ViewGroup內(nèi)的View的焦點(diǎn)變化。子View獲取焦點(diǎn)我們可以通過requestChildFocus方法,但是并沒有子View失去焦點(diǎn)的監(jiān)聽(除非我們主動(dòng)調(diào)用clearFocus方法)
或許我們只能通過ViewTreeObserve的dispatchOnGlobalFocusChange方法方法來監(jiān)聽這個(gè)變化。
ViewTreeObserve
使用方法,在ViewGroup中注冊:
getViewTreeObserver().addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
if (hasFocus()) {
//焦點(diǎn)進(jìn)入ViewGroup
} else {
//焦點(diǎn)移出ViewGroup
}
}
});
addOnGlobalFocusChangeListener方法
public void addOnGlobalFocusChangeListener(OnGlobalFocusChangeListener listener) {
checkIsAlive();
if (mOnGlobalFocusListeners == null) {
mOnGlobalFocusListeners = new CopyOnWriteArrayList<OnGlobalFocusChangeListener>();
}
mOnGlobalFocusListeners.add(listener);
}
dispatchOnGlobalFocusChange方法
final void dispatchOnGlobalFocusChange(View oldFocus, View newFocus) {
final CopyOnWriteArrayList<OnGlobalFocusChangeListener> listeners = mOnGlobalFocusListeners;
if (listeners != null && listeners.size() > 0) {
for (OnGlobalFocusChangeListener listener : listeners) {
listener.onGlobalFocusChanged(oldFocus, newFocus);
}
}
}
這里的mOnGlobalFocusListeners是一個(gè)ArrayList,所以可以監(jiān)聽多個(gè)view的焦點(diǎn)變化。但是在使用的時(shí)候需要注意一個(gè)問題,注冊的listener在不使用的時(shí)候要及時(shí)的remove,不然會非常影響性能。
解決方案
這里提供大致的思路,具體的方案可以看我寫的demo。demo中還提供了聚焦后的焦點(diǎn)框以及放大的動(dòng)畫效果。
新建一個(gè)類繼承自ViewGroup的子類(我這里繼承了FrameLayout),分別在onAttachedToWindow方法中進(jìn)行注冊,在onDetachedFromWindow方法中進(jìn)行解綁。
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
onGlobalFocusChangeListener = new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
//判斷是否自身被聚焦或者存在子view被聚焦
if (hasFocus()) {
focusEnter();
} else {
focusLeave();
}
}
};
getViewTreeObserver().addOnGlobalFocusChangeListener(onGlobalFocusChangeListener);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//主要要及時(shí)remove
getViewTreeObserver().removeOnGlobalFocusChangeListener(onGlobalFocusChangeListener);
}
使用這種方式,mOnGlobalFocusListeners的size等于RecyclerVIew中當(dāng)前可見的繼承于該ViewGroup的item的個(gè)數(shù)。