通過上篇的介紹,我們知道在對KeyEvent的處理中有非常重要的一環,那就是KeyEvent在focus view的path上自上而下的分發,
換句話說只有focus的view才有資格參與KeyEvent的處理,所以說focused view在KeyEvent的處理中很重要,我們需要弄清楚明白
focus view是如何設置以及改變的。
通過Android官方文檔http://developer.android.com/reference/android/view/View.html中關於Focus Handling的介紹,
我們知道framework會根據用戶的輸入處理常規的focus移動,包括當刪除、隱藏或添加新的view時改變focus。一個view有資格獲得
focus的前提是isFocusable()方法返回true,你可以通過setFocusable(boolean)方法來設置它。另外當在touch mode下的時候,還
需要isFocusableInTouchMode()也返回true,你也可以通過setFocusableInTouchMode(boolean)來設置它。focus的移動是基於這
樣的算法,它嘗試在某個給定的方向上找最臨近的view,設置它為新的focus。在極個別情況,如果默認的算法不符合你的需求,你也可以
在xml布局文件中通過顯式指定nextFocusDown/Left/Right/Up這些屬性來表明focus移動的順序。在運行時刻,你也可以通過調用
View.requestFocus()方法來動態地讓某個view獲得focus。作為開始,我們先看看這幾個具備獲得焦點前提的方法,如下:
/** * Returns whether this View is able to take focus. * * @return True if this view can take focus, or false otherwise. * @attr ref android.R.styleable#View_focusable */ @ViewDebug.ExportedProperty(category = "focus") public final boolean isFocusable() { return FOCUSABLE == (mViewFlags & FOCUSABLE_MASK); // 各種位操作,不熟悉、習慣的同學可以翻本C語言的書看看, } // 這里順便推薦下《C Primer Plus》,一本足矣,而且里面 // 有一章是專門介紹bit操作的應用的,非常贊!!! /** * When a view is focusable, it may not want to take focus when in touch mode. * For example, a button would like focus when the user is navigating via a D-pad * so that the user can click on it, but once the user starts touching the screen, * the button shouldn't take focus * @return Whether the view is focusable in touch mode. * @attr ref android.R.styleable#View_focusableInTouchMode */ @ViewDebug.ExportedProperty public final boolean isFocusableInTouchMode() { return FOCUSABLE_IN_TOUCH_MODE == (mViewFlags & FOCUSABLE_IN_TOUCH_MODE); } /** * Set whether this view can receive the focus. * * Setting this to false will also ensure that this view is not focusable * in touch mode. * * @param focusable If true, this view can receive the focus. * * @see #setFocusableInTouchMode(boolean) * @attr ref android.R.styleable#View_focusable */ public void setFocusable(boolean focusable) { if (!focusable) { // 注意:是false的時候,會順便保證在touch mode下也不能獲得focus setFlags(0, FOCUSABLE_IN_TOUCH_MODE); } setFlags(focusable ? FOCUSABLE : NOT_FOCUSABLE, FOCUSABLE_MASK); // 設置FOCUSABLEB位 } /** * Set whether this view can receive focus while in touch mode. * * Setting this to true will also ensure that this view is focusable. * * @param focusableInTouchMode If true, this view can receive the focus while * in touch mode. * * @see #setFocusable(boolean) * @attr ref android.R.styleable#View_focusableInTouchMode */ public void setFocusableInTouchMode(boolean focusableInTouchMode) { // Focusable in touch mode should always be set before the focusable flag // otherwise, setting the focusable flag will trigger a focusableViewAvailable() // which, in touch mode, will not successfully request focus on this view // because the focusable in touch mode flag is not set setFlags(focusableInTouchMode ? FOCUSABLE_IN_TOUCH_MODE : 0, FOCUSABLE_IN_TOUCH_MODE); if (focusableInTouchMode) { // 如果是true順便打開FOCUSABLE位 setFlags(FOCUSABLE, FOCUSABLE_MASK); } }
接下來我們就看看本文的重點View.requestFocus()等相關方法,代碼如下:
/** * Call this to try to give focus to a specific view or to one of its * descendants. * * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns * false), or if it is focusable and it is not focusable in touch mode * ({@link #isFocusableInTouchMode}) while the device is in touch mode. * * See also {@link #focusSearch(int)}, which is what you call to say that you * have focus, and you want your parent to look for the next one. * * This is equivalent to calling {@link #requestFocus(int, Rect)} with arguments * {@link #FOCUS_DOWN} and <code>null</code>. * * @return Whether this view or one of its descendants actually took focus. */ public final boolean requestFocus() { return requestFocus(View.FOCUS_DOWN); } /** * Call this to try to give focus to a specific view or to one of its * descendants and give it a hint about what direction focus is heading. * * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns * false), or if it is focusable and it is not focusable in touch mode * ({@link #isFocusableInTouchMode}) while the device is in touch mode. * * See also {@link #focusSearch(int)}, which is what you call to say that you * have focus, and you want your parent to look for the next one. * * This is equivalent to calling {@link #requestFocus(int, Rect)} with * <code>null</code> set for the previously focused rectangle. * * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT * @return Whether this view or one of its descendants actually took focus. */ public final boolean requestFocus(int direction) { return requestFocus(direction, null); } /** * Call this to try to give focus to a specific view or to one of its descendants * and give it hints about the direction and a specific rectangle that the focus * is coming from. The rectangle can help give larger views a finer grained hint * about where focus is coming from, and therefore, where to show selection, or * forward focus change internally. * * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns * false), or if it is focusable and it is not focusable in touch mode * ({@link #isFocusableInTouchMode}) while the device is in touch mode. * * A View will not take focus if it is not visible. * * A View will not take focus if one of its parents has * {@link android.view.ViewGroup#getDescendantFocusability()} equal to * {@link ViewGroup#FOCUS_BLOCK_DESCENDANTS}. * * See also {@link #focusSearch(int)}, which is what you call to say that you * have focus, and you want your parent to look for the next one. * * You may wish to override this method if your custom {@link View} has an internal * {@link View} that it wishes to forward the request to. * * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT * @param previouslyFocusedRect The rectangle (in this View's coordinate system) * to give a finer grained hint about where focus is coming from. May be null * if there is no hint. * @return Whether this view or one of its descendants actually took focus. */ public boolean requestFocus(int direction, Rect previouslyFocusedRect) { return requestFocusNoSearch(direction, previouslyFocusedRect); } private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { // 此方法就是最終被調用的版本 // need to be focusable if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE || // 不是FOCUSABLE,即沒資格獲取焦點 (mViewFlags & VISIBILITY_MASK) != VISIBLE) { // 或者不是VISIBLE的,都直接返回false,表示請求獲取焦點失敗 return false; // 所以除了上文提到的2個獲取focus的前提,其實這里的VISIBLE也應該算是第3個前提吧! } // need to be focusable in touch mode if in touch mode if (isInTouchMode() && // 同樣在touch mode下,也要檢測FOCUSABLE_IN_TOUCH_MODE標志 (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) { return false; // 不滿足也直接返回false,表示失敗 } // need to not have any parents blocking us if (hasAncestorThatBlocksDescendantFocus()) { // parents阻止我們獲得焦點的話,我們也只能以失敗告終 return false; } // 以上重重關卡都通過了,才會走到這里,真正設置focus handleFocusGainInternal(direction, previouslyFocusedRect); return true; } /** * Give this view focus. This will cause * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called. * * Note: this does not check whether this {@link View} should get focus, it just * gives it focus no matter what. It should only be called internally by framework * code that knows what it is doing, namely {@link #requestFocus(int, Rect)}. * * @param direction values are {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT}. This is the direction which * focus moved when requestFocus() is called. It may not always * apply, in which case use the default View.FOCUS_DOWN. * @param previouslyFocusedRect The rectangle of the view that had focus * prior in this View's coordinate system. */ void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) { if (DBG) { System.out.println(this + " requestFocus()"); } if ((mPrivateFlags & PFLAG_FOCUSED) == 0) { // 只有當前View不是focused view時才會發生一系列操作,否則do nothing mPrivateFlags |= PFLAG_FOCUSED; // 如果沒focus的話,先設置此view的focused標志,isFocused,hasFocus等方法會檢測此標志 View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null; // 找到之前的focus if (mParent != null) { mParent.requestChildFocus(this, this); // 如果有mParent,則將此新focus請求向上傳遞 } if (mAttachInfo != null) { // callback接口,將focus change事件notify出去 mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this); } onFocusChanged(true, direction, previouslyFocusedRect); refreshDrawableState(); } }
接着我們看下關於PFLAG_FOCUSED標志位相關的幾個方法,如下:
/** * Returns true if this view has focus * * @return True if this view has focus, false otherwise. */ @ViewDebug.ExportedProperty(category = "focus") public boolean isFocused() { return (mPrivateFlags & PFLAG_FOCUSED) != 0; } /** * Find the view in the hierarchy rooted at this view that currently has * focus. * * @return The view that currently has focus, or null if no focused view can * be found. */ public View findFocus() { return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null; } /** * Returns true if this view has focus iteself, or is the ancestor of the * view that has focus. * * @return True if this view has or contains focus, false otherwise. */ @ViewDebug.ExportedProperty(category = "focus") public boolean hasFocus() { // 對View來說,hasFocus和isFocus是相同的,ViewGroup類重載了此方法 return (mPrivateFlags & PFLAG_FOCUSED) != 0; }
最后,我們看看ViewParent接口(以及其實現ViewGroup)的requestChildFocus()實現,代碼如下:
/** * Called when a child of this parent wants focus * * @param child The child of this ViewParent that wants focus. This view * will contain the focused view. It is not necessarily the view that * actually has focus. * @param focused The view that is a descendant of child that actually has * focus */ public void requestChildFocus(View child, View focused); // parent中的某個child請求獲得focus,child要么是focused, // 要么是focused的parent // 我們可以看到其實現類有ViewGroup、ScrollView等,這里我們看下ViewGroup類的,其他的有興趣的同學可以自行研究 @Override public void requestChildFocus(View child, View focused) { if (DBG) { System.out.println(this + " requestChildFocus()"); } if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) { return; // 如果此ViewGroup被設置為阻止任何children獲得focus,則直接返回 } // Unfocus us, if necessary super.unFocus(); // 先unFocus 此ViewGroup // We had a previous notion of who had focus. Clear it. if (mFocused != child) { // 如果mFocused不同於傳遞進來的child,則更新mFocused if (mFocused != null) { mFocused.unFocus(); // 讓舊的放棄focus } mFocused = child; // 更新mFocused } if (mParent != null) { // 接着沿着focus path往上傳遞(遞歸調用) mParent.requestChildFocus(this, focused); // 注意這里的第2個參數,一直是傳遞進來的focused不變 } }
我們注意到只有ViewGroup才有mFocused字段,表示focus path上的一個節點。我們看看與之相關的代碼:
// The view contained within this ViewGroup that has or contains focus. private View mFocused; // 此ViewGroup中的child view,它要么是focused view本身要么包含focused view @Override void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) { // 此方法重載了View中的 if (mFocused != null) { // 添加了對mFocused的處理 mFocused.unFocus(); // 讓mFocused unFocus在這種情況下 mFocused = null; } super.handleFocusGainInternal(direction, previouslyFocusedRect); } /** * {@inheritDoc} */ public void clearChildFocus(View child) { if (DBG) { System.out.println(this + " clearChildFocus()"); } mFocused = null; // 清空 if (mParent != null) { // 將事件告訴parent mParent.clearChildFocus(this); } } /** * {@inheritDoc} */ @Override public void clearFocus() { if (DBG) { System.out.println(this + " clearFocus()"); } if (mFocused == null) { // 如果沒有mFocused,則ViewGroup自身clearFocus super.clearFocus(); } else { // 否則,讓mFocused clearFocus,並且重置為null View focused = mFocused; mFocused = null; focused.clearFocus(); } } /** * {@inheritDoc} */ @Override void unFocus() { // 大體同clearFocus,只是調的是unFocus方法 if (DBG) { System.out.println(this + " unFocus()"); } if (mFocused == null) { super.unFocus(); } else { mFocused.unFocus(); mFocused = null; } } /** * Returns the focused child of this view, if any. The child may have focus * or contain focus. * * @return the focused child or null. */ public View getFocusedChild() { // 返回這個字段,供客戶端代碼使用 return mFocused; } /** * Returns true if this view has or contains focus * * @return true if this view has or contains focus */ @Override public boolean hasFocus() { // ViewGroup自己是focused或者其子、孫后代包含focused view return (mPrivateFlags & PFLAG_FOCUSED) != 0 || mFocused != null; } /* * (non-Javadoc) * * @see android.view.View#findFocus() */ @Override public View findFocus() { if (DBG) { System.out.println("Find focus in " + this + ": flags=" + isFocused() + ", child=" + mFocused); } if (isFocused()) { // 自己是focused,直接返回this return this; } if (mFocused != null) { // 否則,mFocused不為空,則沿着這條線往下繼續找 return mFocused.findFocus(); } return null; } /** * {@inheritDoc} */ @Override public boolean hasFocusable() { if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) { return false; } if (isFocusable()) { return true; } final int descendantFocusability = getDescendantFocusability(); if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { final View child = children[i]; if (child.hasFocusable()) { return true; } } } return false; }
接着我們看下View自己的unFocus、clearFocus實現,代碼如下:
/** * Called internally by the view system when a new view is getting focus. * This is what clears the old focus. * <p> * <b>NOTE:</b> The parent view's focused child must be updated manually * after calling this method. Otherwise, the view hierarchy may be left in * an inconstent state. */ void unFocus() { if (DBG) { System.out.println(this + " unFocus()"); } clearFocusInternal(false, false); } /** * Called when this view wants to give up focus. If focus is cleared * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} is called. * <p> * <strong>Note:</strong> When a View clears focus the framework is trying * to give focus to the first focusable View from the top. Hence, if this * View is the first from the top that can take focus, then all callbacks * related to clearing focus will be invoked after wich the framework will * give focus to this view. * </p> */ public void clearFocus() { if (DBG) { System.out.println(this + " clearFocus()"); } clearFocusInternal(true, true); } /** * Clears focus from the view, optionally propagating the change up through * the parent hierarchy and requesting that the root view place new focus. * * @param propagate whether to propagate the change up through the parent * hierarchy * @param refocus when propagate is true, specifies whether to request the * root view place new focus */ void clearFocusInternal(boolean propagate, boolean refocus) { if ((mPrivateFlags & PFLAG_FOCUSED) != 0) { // 如果當前是focused,先清掉PFLAG_FOCUSED位 mPrivateFlags &= ~PFLAG_FOCUSED; if (propagate && mParent != null) { mParent.clearChildFocus(this); // 如果向上傳播的話,調用parent.clearChildFocus方法 } onFocusChanged(false, 0, null); // 調用callback方法 refreshDrawableState(); // 刷新drawable狀態 if (propagate && (!refocus || !rootViewRequestFocus())) { notifyGlobalFocusCleared(this); } } }
通過上一篇的介紹,我們知道KeyEvent的派發就是在view層次結構的focus path上自上而下發生的,具體參見View.dispatchKeyEvent
的方法doc。剛開始我一直不明白這里的focus path是怎么形成的,怎么按着這個鏈傳遞的。這里為了幫助大家理解,我舉一個典型的例子,
通過例子可以很清楚的看到傳遞過程。比方說我們的view層次結構是這樣的,C是個Button,B是C的parent,LinearLayout,A是B的parent,
FrameLayout。這里我們先假設C、B、A都是有資格且其parent都不阻止它獲得焦點,當我們在代碼里調用C.requestFocus()時發生的調用
序列如下:
1. --> B.requestChildFocus(C, C); 當此方法發生后產生的結果是:B.mFocused = C;接着產生2調用;
2. --> A.requestChildFocus(B, C); 同樣的,當此方法發生后,A.mFocused = B; 接着往上傳遞直到parent為空時停止。
當C.requestFocus()調用結束時,如果沒有各種失敗的case發生,那么C就是當前view層次結構中的focus了,也就是C.isFocused()方法
此時會返回true。看到了嗎?通過這個遞歸調用,focus path的鏈就形成了,從最頂層的A能通過其mFocused字段找到B,從找到的B能通過
其mFocused字段找到C,以此類推。為了加深這個印象,我們最后再看眼ViewGroup.dispatchKeyEvent()方法:
@Override public boolean dispatchKeyEvent(KeyEvent event) { /// 2.2.1.1... if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onKeyEvent(event, 1); } if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) { // ViewGroup是focused,則優先交給它自己處理 /// 2.2.1.2. if (super.dispatchKeyEvent(event)) { return true; } } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS) == PFLAG_HAS_BOUNDS) { // 否則就沿着mFocused形成的focus path向下傳遞 /// 2.2.1.3. if (mFocused.dispatchKeyEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 1); } /// 2.2.1.4. return false; }
現在再回過頭來看這里的邏輯,是不是感覺特別簡單呢?那是因為你已經完全弄清楚了mFocused的由來以及各種變化過程。至此view層次
結構中關於focus的變化過程已經全部分析完畢了,enjoy。