android:nextFocusUp="@id/下一個控件的id"
android:nextFocusDown=""
android:nextFocusLeft=""
android:nextFocusRight=""
分別對應該控件按下↑、↓、←、→鍵對應的下一個控件。java
翻看各大博客,對與AndroidTV焦點控制的理解都大同小異,接下來是我對與焦點控制的理解:android
Activity -> Window -> ViewGroup -> View算法
包含攔截、分發、響應:數組
攔截發生在: onInterceptTouchEvent()方法中,當用戶觸發event事件後,由上層傳入,當此方法返回true時,則被攔截不會繼續往子view傳遞,由當前view的 onTouchEvent()來響該事件。app
返回false時,不會被攔截,事件將繼續傳遞 ,由子view調用當前view的 dispatchTouchEvent() 去分發, 最後由具體的控件去消費此事件。ide
分發:函數
如圖A:表明activity,B:表明ViewGroup(如:佈局),C:表明View(如:button)佈局
點擊屏幕上的C時整個事件將會由A—B --C —B—A這樣的順序進行分發。this
具體狀況以下:spa
KeyEvent:位於android.view下,KeyEvent主要有如下事件類型:
KeyEvent.KEYCODE_DPAD_UP; 上
KeyEvent.KEYCODE_DPAD_DOWN; 下
KeyEvent.KEYCODE_DPAD_LEFT;左
KeyEvent.KEYCODE_DPAD_RIGHT;右
KeyEvent.KEYCODE_DPAD_CENTER;肯定鍵
KeyEvent.KEYCODE_DPAD_RIGHT; 右
KeyEvent.KEYCODE_XXX:數字鍵 (xx表示你按了數字幾)
KeyEvent.KEYCODE_BACK; 返回鍵
KeyEvent.KEYCODE_HOME;房子鍵
KeyEvent.KEYCODE_A: A-Z,26個字母
KeyEvent.KEYCODE_MENU菜單鍵。
首先看事件分發圖:
如上圖:
首先,KeyEvent會流轉到ViewRootImpl中開始進行處理,具體方法是內部類ViewPostImeInputStage中的processKeyEvent。
代碼以下:
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
...
// Deliver the key to the view hierarchy.
// 1. 先去執行mView的dispatchKeyEvent
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
...
// Handle automatic focus changes.
if (event.getAction() == KeyEvent.ACTION_DOWN) {
int direction = 0;
...
if (direction != 0) {
View focused = mView.findFocus();
if (focused != null) {
// 2. 以後會經過focusSearch去找下一個焦點視圖
View v = focused.focusSearch(direction);
if (v != null && v != focused) {
...
if (v.requestFocus(direction, mTempRect)) {
...
return FINISH_HANDLED;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return FINISH_HANDLED;
}
} else {
// find the best view to give focus to in this non-touch-mode with no-focus
// 3. 若是當前原本就沒有焦點視圖,也會經過focusSearch找一個視圖
View v = focusSearch(null, direction);
if (v != null && v.requestFocus(direction)) {
return FINISH_HANDLED;
}
}
}
}
return FORWARD;
}
看上面的代碼能夠了解:
先執行mView的dispatchKeyEvent()方法,再經過focusSearch()去找下一個焦點視圖,若是當前沒由焦點視圖也會執行focusSearch()找一個視圖。
DecorView →Activity→ViewGroup→view。
DecorView 的 dispatchKeyEvent ():
public boolean dispatchKeyEvent(KeyEvent event) { ... ... if (!mWindow.isDestroyed()) { // Activity實現了Window.Callback接口,具體能夠參考 Activity.java 源碼. final Window.Callback cb = mWindow.getCallback(); // mFeatureId < 0,表示爲 application 的 DecorView. // cb.dispatchKeyEven 調用的是 Activity 的 dispatchKeyEven. final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event) : super.dispatchKeyEvent(event); // 是否消耗掉事件. if (handled) { return true; } } return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event) : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event); }
這裏將會調用Activty的dispatchKeyEvent();
Activity 的 dispatchKeyEvent ():
// 補充知識點: // 這就是爲什麼在 Activity 直接 return true,事件被消耗,就不執行焦點搜索等等操做了. // 因此這裏也是能夠作 焦點控制的,最好是在 event.getAction() == KeyEvent.ACTION_DOWN 進行. // 由於android 的 ViewRootlmpl 的 processKeyEvent 焦點搜索與請求的地方 進行了判斷 // if (event.getAction() == KeyEvent.ACTION_DOWN) public boolean dispatchKeyEvent(KeyEvent event) { ... ... Window win = getWindow(); // 調用 PhoneWindow 的 superDispatchKeyEvent // 裏面又調用 mDecor.superDispatchKeyEvent(event) // mDecor 爲 DecorView. if (win.superDispatchKeyEvent(event)) { return true; } View decor = mDecor; if (decor == null) decor = win.getDecorView(); // onKeyDown,onKeyUp,onKeyLongPress 等等回調的處理. // 只有 onKeyDown return true 能夠進行焦點控制, // 由於android 的 ViewRootlmpl 的 processKeyEvent 焦點搜索與請求的地方 進行了判斷 // if (event.getAction() == KeyEvent.ACTION_DOWN) return event.dispatch(this, decor != null ? decor.getKeyDispatcherState() : null, this); }
ViewGroup的dispatchKeyEvent():
@Override public boolean dispatchKeyEvent(KeyEvent event) { ... if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) { // 1.1 以View的身份處理KeyEvent if (super.dispatchKeyEvent(event)) { return true; } } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS) == PFLAG_HAS_BOUNDS) { // 1.2 以ViewGroup的身份把KeyEvent交給mFocused處理 if (mFocused.dispatchKeyEvent(event)) { return true; } } ... return false; }
經過flag的判斷,有兩個處理路徑,也能夠看到在處理keyEvent時,ViewGroup扮演兩個角色:
View的角色,也就是此時keyEvent須要在本身與其餘View之間流轉。:調用自身的dispathKeyEvent()。
ViewGroup的角色,此時keyEvent須要在本身的子View之間流轉 。:調用當前焦點子View的dispatchKeyEvent()。
再來看看view的dispatchKeyEvent():
public boolean dispatchKeyEvent(KeyEvent event) { ... ListenerInfo li = mListenerInfo; // 1.3 若是設置了mOnKeyListener,則優先走onKey方法 if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) { return true; } // 1.4 把View本身看成參數傳入,調用KeyEvent的dispatch方法 if (event.dispatch(this, mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null, this)) { return true; } ... return false; }
View這裏,會優先處理OnKeyListener的onKey回調。而後纔可能會走KeyEvent的dispatch,最終走到View的OnKeyDown或者OnKeyUp。
大致的流轉順序總結以下圖:
其中任何一步均可以經過return true的方式來消費掉這個KeyEvent,結束這個分發過程。
按鍵事件分發結束,接下來讓咱們看看如和查找焦點。
若是dispatchKeyEvent沒有消耗掉KeyEvent,會由系統來處理焦點移動。
經過view的focusSearch方法找到下一個獲取焦點的View,而後調用requestFocus設置焦點。
// View.java public View focusSearch(@FocusRealDirection int direction) { if (mParent != null) { return mParent.focusSearch(this, direction); } else { return null; } }
由上面的代碼能夠看出,View不會直接去查找,而是會交給其parent的focusSearch方法去查找,也就是ViewGroup的focusSearch()方法去查找。
ViewGroup的focusSearch()方法:
// ViewGroup.java 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; }
這裏會判斷是否爲根佈局,也就是頂層佈局,若是是則最後交給FocusFinder去查找。
若是不是則會接調用上層parent的focusSearch()。
isRootNamespace的()
/** * {@hide} * * @param isRoot true if the view belongs to the root namespace, false * otherwise */ public void setIsRootNamespace(boolean isRoot) { if (isRoot) { mPrivateFlags |= PFLAG_IS_ROOT_NAMESPACE; } else { mPrivateFlags &= ~PFLAG_IS_ROOT_NAMESPACE; } }
位於頂層的ViewGroup把本身和當前焦點(View)以及方向傳入。
// FocusFinder.java public final View findNextFocus(ViewGroup root, View focused, int direction) { return findNextFocus(root, focused, null, direction); } private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { View next = null; if (focused != null) { // 2.1 優先從xml或者代碼中指定focusid的View中找 next = findNextUserSpecifiedFocus(root, focused, direction); } if (next != null) { return next; } ArrayList<View> focusables = mTempList; try { focusables.clear(); root.addFocusables(focusables, direction); if (!focusables.isEmpty()) { // 2.2 其次,根據算法去找,原理就是找在方向上最近的View next = findNextFocus(root, focused, focusedRect, direction, focusables); } } finally { focusables.clear(); } return next; }
這裏root是上面isRootNamespace()爲true的ViewGroup,focused是當前焦點視圖
優先找開發者指定的下一個focus的視圖 ,就是在xml或者代碼中指定NextFocusDirection Id的視圖。
其次,根據算法去找,原理就是找在方向上最近的視圖。
先看下DecoreView的流程圖:
上圖ViewRootImpl類中 performTraversals方法:
.. ... if (mFirst) { if (mView != null) { if (!mView.hasFocus()) { // 調用 View 的 requestFocus(int direction) mView.requestFocus(View.FOCUS_FORWARD); } ... ... } ... ...
總體的過程:
ViewRootlmpl.performTraversals→DecoreView.requestFocus→ActionBarOverlayLayout.requestFocus→FrameLayout(android:id/content).requestFocus→FrameLayout(activity_test.xml).requestFocus→Button1(activity_test.xml).requestFocus
代碼步驟:
View.java public final boolean requestFocus(int direction) { // 由於 DecoreView 繼承 ViewGroup // ViewGroup 重寫了此函數, // 會調用 ViewGroup 的 requestFocus(int direction, Rect previouslyFocusedRect) return requestFocus(direction, null); } ViewGroup.java public boolean requestFocus(int direction, Rect previouslyFocusedRect) { // 關注內容: // 處理 DescendantFocusabilit // 1)FOCUS_AFTER_DESCENDANTS 先分發給Child View進行處理,若是全部的Child View都沒有處理,則本身再處理 // 2)FOCUS_BEFORE_DESCENDANTS ViewGroup先對焦點進行處理,若是沒有處理則分發給child View進行處理 // 3)FOCUS_BLOCK_DESCENDANTS ViewGroup自己進行處理,不論是否處理成功,都不會分發給ChildView進行處理 // setDescendantFocusability 能夠設置. int descendantFocusability = getDescendantFocusability(); switch (descendantFocusability) { case FOCUS_BLOCK_DESCENDANTS: return super.requestFocus(direction, previouslyFocusedRect); case FOCUS_BEFORE_DESCENDANTS: { // 其它的 ActionBarOverlayLayout,Content等繼承ViewGroup // 默認進入 FOCUS_BEFORE_DESCENDANTS,由於 ViewGroup 初始化的時候設置了 // setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS); // mViewFlags 判斷 FOCUSABLE_MASK,FOCUSABLE_IN_TOUCH_MODE. // Button 以上的父佈局,不知足以上條件判斷,所有都是 直接 return false. final boolean took = super.requestFocus( direction, previouslyFocusedRect); // took=false, 調用 onRequestFocusInDescendants 遍歷子控件進行請求 return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect); } case FOCUS_AFTER_DESCENDANTS: { // DecoreView 進入這裏,由於 PhoneWindow 給 DecoreView 初始化 設置 // setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); // setIsRootNamespace(true); // 像 RecyclerView, Leanback 也會進入這裏. // 遍歷子控件進行請求 final boolean took = onRequestFocusInDescendants( direction, previouslyFocusedRect); // took=true,子控件有焦點,不調用 super.request...,反之. return took ? took : super.requestFocus( direction, previouslyFocusedRect); } ... ... } } View.java public boolean requestFocus(int direction, Rect previouslyFocusedRect) { return requestFocusNoSearch(direction, previouslyFocusedRect); } ViewGroup.java // 補充知識點: onRequestFocusInDescendants 是能夠作焦點記憶控制的. protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { .. ... 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; }
Button1獲取焦點:
關鍵代碼是 View.java 的函數 handleFocusGainInternal : mPrivateFlags |= PFLAG_FOCUSED 和 mParent.requestChildFocus(this, this)
View.java private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { // need to be focusable // Button 默認 android:focusable="true" // button1 以上的父佈局都沒有設置此類屬性,進入這裏,直接就 return false. if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE || (mViewFlags & VISIBILITY_MASK) != VISIBLE) { return false; } // need to be focusable in touch mode if in touch mode // 當 button1 沒有設置 android:focusableInTouchMode="true" 的時候, // 直接 return false,那麼界面上是沒有任何控件獲取到焦點的. // 鼠標|觸摸支持的屬性. 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; } void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) { if ((mPrivateFlags & PFLAG_FOCUSED) == 0) { // 關鍵代碼,設置 有焦點的標誌位. // 這個時候 button1 已經標誌上焦點 mPrivateFlags |= PFLAG_FOCUSED; // 獲取父佈局的老焦點. View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null; // 調用此函數,告訴上一層父佈局,讓它作一些事情. if (mParent != null) { mParent.requestChildFocus(this, this); } // 此函數是全局焦點監聽的回調. // 調用方式: View.getViewTreeObserver().addOnGlobalFocusChangeListener if (mAttachInfo != null) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this); } // 回調處理. onFocusChanged(true, direction, previouslyFocusedRect); // 刷新按鍵的 selector drawable state狀態 refreshDrawableState(); } } ViewGroup.java public void requestChildFocus(View child, View focused) { 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); } // 保存上一級的焦點view. mFocused = child; } // 一層層調用回去父佈局,至關於 // FrameLayout(activity_test.xml) 的 mFocused 是 Button1. // FrameLayout(android:id/content) 的 mFocused 是 FrameLayout(activity_test.xml) // ActionBarOverlayLayout 的 mFocused 是 FrameLayout(android:id/content) // 最後 DecoreView 的 mFocused 是 ActionBarOverlayLayout // 在最後的後面,ViewRootImpl 會調用 // requestChildFocus,又會再次調用 // performTraversals刷新界面.(再執行 layout, draw) // 造成了一個關聯, dispatchKeyEvent 的 mFocused 也在使用. if (mParent != null) { mParent.requestChildFocus(this, focused); } } // ViewRootImpl.java @Override public void requestChildFocus(View child, View focused) { checkThread(); scheduleTraversals(); }
初步獲取焦點已經瞭解,接下來看看焦點是如何從 view2 →view2的。
focusView(2) 按下右鍵後:由上面的3.焦點查找方法能夠得出下圖:
在沒有消耗 dispatchKeyEvent的狀況下:
FocusSearch 一層層上去,調用 FocusFinder.getInstance().findNextFocus… … 後,在 …addFocusables 下,將全部帶焦點屬性的 view 所有加到數組裏面去,而後通用方向,位置等查找相近的view. 最後找到的是 focusView(3).
private int processKeyEvent(QueuedInputEvent q) { ... ... // 以上代碼不消耗事件. // 判斷 action 爲 ACTION_DOWN 才處理焦點搜索以及請求. if (event.getAction() == KeyEvent.ACTION_DOWN) { // 根據按鍵判斷,設置 direction 屬性. if (direction != 0) { // 一層層查找(根據mFocused),最後獲取到 button1. View focused = mView.findFocus(); if (focused != null) { // button1_view 調用 focusSearch(), 右鍵,direction=66 View v = focused.focusSearch(direction); // 最終返回 v = button2 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); } // button2 View 調用 requestFocus // 這裏的過程 和 第一次獲取焦點button1請求是同樣的. if (v.requestFocus(direction, mTempRect)) { // 播放音效 playSoundEffect(SoundEffectConstants .getContantForFocusDirection(direction)); return FINISH_HANDLED; } } // 進行最後的垂死掙扎, // 這裏其實能夠處理一些焦點問題或者滾動翻頁問題. // 滾動翻頁的demo能夠參考 原生 Launcher 的 Workspace.java // Give the focused view a last chance to handle the dpad key. if (mView.dispatchUnhandledMove(focused, direction)) { return FINISH_HANDLED; } } else { // 這裏處理第一次無焦點 view 的狀況. // 基本上和有焦點view 的狀況差很少. View v = focusSearch(null, direction); if (v != null && v.requestFocus(direction)) { return FINISH_HANDLED; } } } } ... ... }
button1下一個焦點搜索流程圖:
View v = focused.focusSearch(direction); # focused=>button1 direction=>66
Button1_View→focusSearch(int direction)→FrameLayout(activity_test.xml)_ViewGroup→focusSearch(View focused, int direction)→。。。→FrameLayout(activity_test.xml)_ViewGroup→
focusSearch(View focused, int direction)→DecoreView_ViewGroup→FocusFinder.getInstance().findNextFocus(this, focused, direction)→FocusFinder.findNextFocus()→ViewGroup.addFocusables()->。。。
代碼流程:
View.java public View focusSearch(@FocusRealDirection int direction) { if (mParent != null) { // button1 的父佈局ViewGroup調用 focusSearch return mParent.focusSearch(this, direction); } else { return null; } } ViewGroup.java // 像 RecyclerView 會重寫 focusSearch 進行焦點搜索. // 也是調用的 FocusFinder.getInstance().findNextFocus // leanback 的 GridLayoutmanger 也重寫了 onAddFocusables. public View focusSearch(View focused, int direction) { // 只有 DecoreView 設置了 setIsRootNamespace // 最終由 DecoreView 進入這裏. if (isRootNamespace()) { // 傳入參數(this: DecoreView focused: button1 direction: 66) return FocusFinder.getInstance().findNextFocus(this, focused, direction); } else if (mParent != null) { return mParent.focusSearch(focused, direction); } return null; } FocusFinder.java findNextFocus(ViewGroup root, View focused, int direction)->findNextFocus(root, focused, null, direction)-> private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { View next = null; if (focused != null) { // 關於XML佈局中的 android:nextFocusRight 等等的查找. next = findNextUserSpecifiedFocus(root, focused, direction); } if (next != null) { return next; } ArrayList<View> focusables = mTempList; try { focusables.clear(); // 要進行 findNextFocus,關鍵在於 addFocusables,一層層調用下去. // DecorView_View.addFocusables // DecorView_ViewGroup.addFocusables // ActionBarOverlayLayout_ViewGroup.addFocusables // FrameLayout(android:id/content)_ViewGroup.addFocusables // FrameLayout(activity_test.xml)_ViewGroup.addFocusables // 到最後 button1, button2 添加到 views 數組中,也就是 focusables . root.addFocusables(focusables, direction); if (!focusables.isEmpty()) { // 關鍵函數 findNextFocus,想深刻了解是如何查找到下一個焦點的, // 能夠去看看源碼,這裏不進行過多篇幅的講解. // focusables 數組有 button1, button2 // 內部調用 findNextFocusInAbsoluteDirection,這裏進行了一些判斷,查找某個方向比較近的view. next = findNextFocus(root, focused, focusedRect, direction, focusables); } } finally { focusables.clear(); } return next; } ViewGroup.java public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { final int focusableCount = views.size(); final int descendantFocusability = getDescendantFocusability(); ... ... for (int i = 0; i < count; i++) { final View child = children[i]; // 循環 child view 調用 addFocusables,一層層調用下去,將知足條件的添加進 views 數組. if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { child.addFocusables(views, direction, focusableMode); } } } if ... ... // 調用 view 的 addFocusables,父佈局是不知足條件的,直接返回了. super.addFocusables(views, direction, focusableMode); } } View.java public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { if (views == null) { return; } if (!isFocusable()) { return; } if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && isInTouchMode() && !isFocusableInTouchMode()) { return; } // button1 以上條件知足,加入views數組. // button2 以上條件也知足,加入views數組. // 同理,焦點記憶的原理就很簡單了,後續會講解. views.add(this); }