如需轉(zhuǎn)載請評論或簡信从撼,并注明出處嘶炭,未經(jīng)允許不得轉(zhuǎn)載
系列文章
- android tv常見問題(一)焦點查找規(guī)律
- android tv常見問題(二)如何監(jiān)聽ViewGroup子View的焦點狀態(tài)
- android tv常見問題(三)RecyclerView的焦點記憶
- android tv常見問題(四)焦點變化時绵患,Recyclerview是如何進行滾動的
github地址
https://github.com/Geekholt/TvFocus
目錄
期望結(jié)果
只要ViewGroup的內(nèi)部或自身存在焦點,ViewGroup就始終保持聚焦樣式账胧。
實際結(jié)果
在不做任何處理的情況下宁改,一個頁面只會存在一個聚焦的view。
問題分析
如果我們先不考慮完全重寫Android焦點框架的情況似踱,我們能否做一些特殊處理盾舌,來實現(xiàn)我們期望的結(jié)果呢墓臭?從期望結(jié)果描述來看,其實實現(xiàn)邏輯還是比較清晰的妖谴,就是我們需要拿到兩個回調(diào):
- 當ViewGroup自身或者內(nèi)部的View獲得焦點的回調(diào)窿锉。
- 當ViewGroup自身或者內(nèi)部的View失去焦點的回調(diào)。
這就需要我們來看一下View和ViewGroup在requestFocus的過程中觸發(fā)了哪些回調(diào)膝舅。
View#requestFocus
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
return requestFocusNoSearch(direction, previouslyFocusedRect);
}
View#requestFocusNoSearch
requestFocusNoSearch校驗View的屬性嗡载,獲取焦點的前提條件是“可見的”和“可聚焦的”。
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是否設置了FOCUS_BLOCK_DESCENDANTS
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
//實現(xiàn)View獲取焦點的具體邏輯
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
View#handleFocusGainInternal
這個是最核心的聚焦邏輯
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " requestFocus()");
}
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
//當前view沒有被聚焦才會進入下面的邏輯
//將view的聚焦標識設置為已聚焦
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
if (mParent != null) {
//通知父控件即將獲取焦點
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)焦點樣式變化洼滚,比如我們在Drawable中設置了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就是當前ViewGroup下持有焦點的View或者ViewGroup,是串聯(lián)整個焦點路徑的屬性
//注意:View的unFocu方法和ViewGroup的unFocus方法實現(xiàn)是不一樣的
mFocused.unFocus(focused);
}
//把當前最新的焦點child賦值給mFocused
mFocused = child;
}
if (mParent != null) {
//繼續(xù)往上通知parent
mParent.requestChildFocus(this, focused);
}
}
View的unFocus方法和ViewGroup的unFocus方法實現(xiàn)是不一樣的技潘,這里如果沒有看清楚可能就會對焦點事件的回調(diào)的方法出現(xiàn)一些誤會遥巴。
ViewGroup#unFocus
這個方法實際上不是失焦的邏輯,而是一個遞歸調(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í)行當前聚集的View的unFocus方法
mFocused.unFocus(focused);
mFocused = null;
}
}
View#unFocus
有兩個地方會調(diào)用到這個方法:
- 在ViewGroup的unFocus方法中遞歸調(diào)用摆霉,最終執(zhí)行當前聚焦的view的unfocus方法。
- 在ViewGroup中調(diào)用super.unFocus()奔坟。這個是在requestChildFocus方法中進行調(diào)用的携栋,用于在子View聚焦之前,先清除一下自身的焦點咳秉。
總的來說就是兩種情況婉支,當前聚焦的View失去焦點和下一個要被聚焦的View的ViewGroup清除自身焦點。也就是說:
對于View來說滴某,每次聚焦或者失焦都會觸發(fā)View的unFocus方法磅摹。
對于ViewGroup來說滋迈,當焦點從ViewGroup外進入到ViewGroup內(nèi)的子View上時霎奢,會觸發(fā)View的unFocus方法。而ViewGroup內(nèi)的子View失去焦點時饼灿,不會觸發(fā)View的unFocus方法幕侠。
這就直接關系到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方法是通過用戶主動調(diào)用而失去焦點,而unFocus方法是在新的焦點要被聚焦之前舞箍,系統(tǒng)內(nèi)部調(diào)用的舰褪。
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
//view存在焦點才會執(zhí)行這里面的邏輯
//將view的聚焦標識設置為未聚焦
mPrivateFlags &= ~PFLAG_FOCUSED;
if (propagate && mParent != null) {
//只有主動調(diào)用clearfocus方法時才會執(zhí)行
mParent.clearChildFocus(this);
}
//onFocusChanged回調(diào)
onFocusChanged(false, 0, null);
//系統(tǒng)的焦點樣式變化
refreshDrawableState();
if (propagate && (!refocus || !rootViewRequestFocus())) {
//只有主動調(diào)用clearfocus方法時才會執(zhí)行全局焦點變化監(jiān)聽的方法
//這是由于在unFocus之后,handleFocusGainInternal方法中會繼續(xù)執(zhí)行全局焦點變化監(jiān) 聽疏橄,這里沒必要重復執(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)
如果我們主動調(diào)用了clearFocus方法來失去焦點,那么回調(diào)方法的執(zhí)行順序是這樣的:
- mParent.clearChildFocus(focused);
- focused.onFocusChanged(false, 0, null);
- mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused , null);
聚焦流程基本分析完了捎迫,回到我們的問題晃酒,我們需要監(jiān)聽ViewGroup內(nèi)的View的焦點變化。子View獲取焦點我們可以通過requestChildFocus方法窄绒,但是并沒有子View失去焦點的監(jiān)聽(除非我們主動調(diào)用clearFocus方法)
或許我們只能通過ViewTreeObserve的dispatchOnGlobalFocusChange方法方法來監(jiān)聽這個變化贝次。
ViewTreeObserve
使用方法,在ViewGroup中注冊:
getViewTreeObserver().addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
if (hasFocus()) {
//焦點進入ViewGroup
} else {
//焦點移出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是一個ArrayList彰导,所以可以監(jiān)聽多個view的焦點變化蛔翅。但是在使用的時候需要注意一個問題,注冊的listener在不使用的時候要及時的remove螺戳,不然會非常影響性能搁宾。
解決方案
這里提供大致的思路,具體的方案可以看我寫的demo倔幼。demo中還提供了聚焦后的焦點框以及放大的動畫效果盖腿。
新建一個類繼承自ViewGroup的子類(我這里繼承了FrameLayout),分別在onAttachedToWindow方法中進行注冊损同,在onDetachedFromWindow方法中進行解綁翩腐。
@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();
//主要要及時remove
getViewTreeObserver().removeOnGlobalFocusChangeListener(onGlobalFocusChangeListener);
}
使用這種方式,mOnGlobalFocusListeners的size等于RecyclerVIew中當前可見的繼承于該ViewGroup的item的個數(shù)膏燃。