如需轉載請評論或簡信,并注明出處,未經允許不得轉載
系列文章
- android tv常見問題(一)焦點查找規律
- android tv常見問題(二)如何監聽ViewGroup子View的焦點狀態
- android tv常見問題(三)RecyclerView的焦點記憶
- android tv常見問題(四)焦點變化時,Recyclerview是如何進行滾動的
github地址
https://github.com/Geekholt/TvFocus
目錄
期望結果
只要ViewGroup的內部或自身存在焦點,ViewGroup就始終保持聚焦樣式。
實際結果
在不做任何處理的情況下,一個頁面只會存在一個聚焦的view。
問題分析
如果我們先不考慮完全重寫Android焦點框架的情況,我們能否做一些特殊處理,來實現我們期望的結果呢?從期望結果描述來看,其實實現邏輯還是比較清晰的,就是我們需要拿到兩個回調:
- 當ViewGroup自身或者內部的View獲得焦點的回調。
- 當ViewGroup自身或者內部的View失去焦點的回調。
這就需要我們來看一下View和ViewGroup在requestFocus的過程中觸發了哪些回調。
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;
}
//實現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) {
//觸發全局OnGlobalFocusChangeListener的回調
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
//觸發將要被聚焦的View的OnFocusChangeListener回調
onFocusChanged(true, direction, previouslyFocusedRect);
//系統焦點樣式變化,比如我們在Drawable中設置了focused_state來區別聚焦或未聚焦樣式
refreshDrawableState();
}
}
ViewGroup#requestChildFocus
public void requestChildFocus(View child, View focused) {
if (DBG) {
System.out.println(this + " requestChildFocus()");
}
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
//被聚焦的ViewGroup先會調用一下View的unFocus方法
super.unFocus(focused);
if (mFocused != child) {
if (mFocused != null) {
//mFocused就是當前ViewGroup下持有焦點的View或者ViewGroup,是串聯整個焦點路徑的屬性
//注意:View的unFocu方法和ViewGroup的unFocus方法實現是不一樣的
mFocused.unFocus(focused);
}
//把當前最新的焦點child賦值給mFocused
mFocused = child;
}
if (mParent != null) {
//繼續往上通知parent
mParent.requestChildFocus(this, focused);
}
}
View的unFocus方法和ViewGroup的unFocus方法實現是不一樣的,這里如果沒有看清楚可能就會對焦點事件的回調的方法出現一些誤會。
ViewGroup#unFocus
這個方法實際上不是失焦的邏輯,而是一個遞歸調用,最終會執行View的unFocus方法。View的unFocus方法才是真正的失焦邏輯。
void unFocus(View focused) {
if (DBG) {
System.out.println(this + " unFocus()");
}
if (mFocused == null) {
super.unFocus(focused);
} else {
//遞歸調用,最終會執行當前聚集的View的unFocus方法
mFocused.unFocus(focused);
mFocused = null;
}
}
View#unFocus
有兩個地方會調用到這個方法:
- 在ViewGroup的unFocus方法中遞歸調用,最終執行當前聚焦的view的unfocus方法。
- 在ViewGroup中調用super.unFocus()。這個是在requestChildFocus方法中進行調用的,用于在子View聚焦之前,先清除一下自身的焦點。
總的來說就是兩種情況,當前聚焦的View失去焦點和下一個要被聚焦的View的ViewGroup清除自身焦點。也就是說:
對于View來說,每次聚焦或者失焦都會觸發View的unFocus方法。
對于ViewGroup來說,當焦點從ViewGroup外進入到ViewGroup內的子View上時,會觸發View的unFocus方法。而ViewGroup內的子View失去焦點時,不會觸發View的unFocus方法。
這就直接關系到ViewGroup的onFocusChanged方法是否執行,具體邏輯看View的clearFocusInternal方法。
void unFocus(View focused) {
if (DBG) {
System.out.println(this + " unFocus()");
}
clearFocusInternal(focused, false, false);
}
View#clearFocusInternal
clearFocusInternal方法還被clearFocus方法所調用,注意區別。clearFocus方法是通過用戶主動調用而失去焦點,而unFocus方法是在新的焦點要被聚焦之前,系統內部調用的。
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
//view存在焦點才會執行這里面的邏輯
//將view的聚焦標識設置為未聚焦
mPrivateFlags &= ~PFLAG_FOCUSED;
if (propagate && mParent != null) {
//只有主動調用clearfocus方法時才會執行
mParent.clearChildFocus(this);
}
//onFocusChanged回調
onFocusChanged(false, 0, null);
//系統的焦點樣式變化
refreshDrawableState();
if (propagate && (!refocus || !rootViewRequestFocus())) {
//只有主動調用clearfocus方法時才會執行全局焦點變化監聽的方法
//這是由于在unFocus之后,handleFocusGainInternal方法中會繼續執行全局焦點變化監 聽,這里沒必要重復執行。
notifyGlobalFocusCleared(this);
}
}
}
View#clearFocus
public void clearFocus() {
if (DBG) {
System.out.println(this + " clearFocus()");
}
clearFocusInternal(null, true, true);
}
requestFocus小結
將要失焦的View:focused
將要失焦的View上層的所有ViewGroup:focusedParent
將要被聚焦的View:next
將要被聚焦的View上層的所有ViewGroup:nextParent
一次聚焦事件回調方法執行的順序是這樣的:
- nextParent.requestChildFocus(focused , focused) ;
- nextParent.onFocusChanged(false, 0, null);
- focused.onFocusChanged(false, 0, null) ;
- mTreeObserver.dispatchOnGlobalFocusChange(focused , next);
- next.onFocusChanged(true, direction, previouslyFocusedRect)
如果我們主動調用了clearFocus方法來失去焦點,那么回調方法的執行順序是這樣的:
- mParent.clearChildFocus(focused);
- focused.onFocusChanged(false, 0, null);
- mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused , null);
聚焦流程基本分析完了,回到我們的問題,我們需要監聽ViewGroup內的View的焦點變化。子View獲取焦點我們可以通過requestChildFocus方法,但是并沒有子View失去焦點的監聽(除非我們主動調用clearFocus方法)
或許我們只能通過ViewTreeObserve的dispatchOnGlobalFocusChange方法方法來監聽這個變化。
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,所以可以監聽多個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的個數。