在觸摸屏出現在手機上之前,焦點是手機上人機互動中最重要的一個概念。焦點即使用者目前的關注點(或區域),手機上将該區域以某種形式高亮顯示,人們通過上、下、左、右方向鍵可以移動焦點,按确認鍵後手機将打開(或呈顯)與目前焦點關聯的内容;觸摸屏的出現大大地簡化了人機互動,觸摸事件(TouchEvent)成了核心,焦點的存在感就很小了。
但是對于電視來說,其顯示屏面積大,人機距離遠,觸摸屏的方案顯然不合理。是以目前Android電視的人機互動仍舊使用遙控器為主,焦點的重要性在電視上又顯現出來了。通過遙控器将方向鍵或确認鍵信号(或資訊)發送到電視端後,轉換為标準按鍵事件(KeyEvent),而按鍵事件分發最終目标就是焦點。
1、初識View之焦點
View是UI元件的基本建構,也自然就是焦點的承載者。View是否可聚焦,由FOCUSABLE和FOCUSABLE_IN_TOUCH_MODE(觸摸模式下也可以有焦點)兩個FLAG辨別。
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
……
case com.android.internal.R.styleable.View_focusable:
if (a.getBoolean(attr, false)) {
viewFlagValues |= FOCUSABLE;
viewFlagMasks |= FOCUSABLE_MASK;
}
break;
case com.android.internal.R.styleable.View_focusableInTouchMode:
if (a.getBoolean(attr, false)) {
viewFlagValues |= FOCUSABLE_IN_TOUCH_MODE | FOCUSABLE;
viewFlagMasks |= FOCUSABLE_IN_TOUCH_MODE | FOCUSABLE_MASK;
}
break;
……
}
}
……
}
從上面 View 的建構方法上看,在 xml 裡即可為其設定是否可聚焦,以 Button 舉個栗子,
public class Button extends TextView {
……
public Button(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.buttonStyle);
}
……
}
Button設定了一個預設的style,我們找出源碼看看,
<stylename="Widget.Button">
<itemname="background">@drawable/btn_default</item>
<itemname="focusable">true</item>
<itemname="clickable">true</item>
<itemname="textAppearance">?attr/textAppearanceSmallInverse</item>
<itemname="textColor">@color/primary_text_light</item>
<itemname="gravity">center_vertical|center_horizontal</item>
</style>
聚焦後, Button 背景将發生改變,向使用者表示該 View 已聚焦。我們可以打開該 style 設定的 background 的源檔案 btn_default 看看,
<selectorxmlns:android="http://schemas.android.com/apk/res/android">
......
<itemandroid:state_focused="true"
android:drawable="@drawable/btn_default_normal_disable_focused"/>
<item
android:drawable="@drawable/btn_default_normal_disable"/>
</selector>
可以看到,這是個 selector,狀态變成已聚焦後,使用另一 drawable做為背景(這個過程具體是怎麼實作的,我們後面分析)。從上面分析看, TextView變成 Button隻需要為其 style 設定幾個關鍵的屬性即可,最主要的是 clickable,focusable, background,以下 TextView即相當于 Button了,
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="true"
android:clickable="true"
android:background=”@drawable/btn_default” />
對于設定是否可聚焦, View還提供以下方法 :
public void setFocusable(boolean focusable) ;
public void setFocusableInTouchMode(boolean focusableInTouchMode);
2、請求焦點
2.1 View的焦點請求
焦點的請求,View提供了以下幾個方法,
public final boolean requestFocus();
public final boolean requestFocus(int direction);
public boolean requestFocus(int direction, Rect previouslyFocusedRect);
我們打開源碼看,這些方法都做了些什麼
[File]android/view/View.java
public final boolean requestFocus() {
return requestFocus(View.FOCUS_DOWN);
}
public final boolean requestFocus(int direction) {
return requestFocus(direction, null);
}
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 ||
(mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
// need to be focusable in touch mode if in touch mode
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;
}
可以看到,前兩個重載方法最終都走到第三個方法内,對于 View來講,關鍵就是看這個私有方法 requestFocusNoSearch ,這個方法主要做了以下4 件事:
1)檢查View 是否可聚焦,是否可見。聚焦前提是 FOCUSABLE并且VISIBLE
2)如果是觸摸模式,則檢查該模式下是否可聚焦(FOCUSABLE_IN_TOUCH_MODE)
3)檢查是否被上一層(ViewGroup)屏蔽焦點
4)目前View擷取焦點,處理焦點變動

2.2 ViewGroup的焦點請求
ViewGroup是可以包含其它View 的一種特殊的 View,各種Layout均是它的子類;對于焦點請求,與View不同的是:
1)它可以優先讓下層View請求焦點,失敗後再自己請求
2)可以優先于下層View請求焦點,失敗後再下層View請求
3)可以屏蔽下層View請求焦點
這三種對下一層請求焦點的控制,分别用了三個FLAG記錄于mGroupFlags中,依次對應為
1)FOCUS_AFTER_DESCENDANTS
2)FOCUS_BEFORE_DESCENDANTS
3)FOCUS_BLOCK_DESCENDANTS
設定這個控制的方法和屬性為:
public void setDescendantFocusability(int focusability);
android:descendantFocusability
設定好後,那麼它具體是怎麼控制的呢?我們分以下幾種情況來分析:
1)ViewGroup的下層View請求焦點: 按上一節說的,View請求焦點需要檢查是否被上層屏蔽的,實際就是檢查上層是否設定了FOCUS_BLOCK_DESCENDANTS這個FLAG,我們回到View.java檢視hasAncestorThatBlocksDescendantFocus這個檢查方法,
private boolean hasAncestorThatBlocksDescendantFocus() {
final boolean focusableInTouchMode = isFocusableInTouchMode();
ViewParent ancestor = mParent;
while (ancestor instanceof ViewGroup) {
final ViewGroup vgAncestor = (ViewGroup) ancestor;
if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS
|| (!focusableInTouchMode && vgAncestor.shouldBlockFocusForTouchscreen())) {
return true;
} else {
ancestor = vgAncestor.getParent();
}
}
return false;
}
這個方法中,一層層往上找,看是否有ViewGroup 設定了FOCUS_BLOCK_DESCENDANTS 。
2)ViewGroup請求焦點:ViewGroup重寫了requestFocus方法以實作控制優先級,
@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
int descendantFocusability = getDescendantFocusability();
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
return super.requestFocus(direction, previouslyFocusedRect);
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
return took ? took : super.requestFocus(direction, previouslyFocusedRect);
}
……
}
}
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;
}
2.3焦點的變更
2.1中提到View請求焦點最後一步是處理焦點變動,我們來細看下裡面都做了些什麼
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;//标記已聚焦
if (mParent != null) {
mParent.requestChildFocus(this, this);//告知上層ViewGroup自己已聚焦
}
if (mAttachInfo != null) {
//通知OnGlobalFocusChangeListener
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
onFocusChanged(true, direction, previouslyFocusedRect);//回調OnFocusChangeListener
refreshDrawableState();//更新drawable 狀态,包括foreground以及前面提及的background
}
}
至此,焦點請求到顯示更新已經明了,但還有個問題, 同一個界面上隻可以有一個焦點,當一個 View 擷取焦點,應當讓前一個焦點失焦。這意味着必須有個地方記錄目前焦點, 擔此重任的即是ViewGroup 裡私有變量mFocused ,
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
……
// The view contained within this ViewGroup that has or contains focus.
private View mFocused;
……
}
這個變量指向的可能是:
1)下一層有焦點的View(或ViewGroup)
2)焦點在其下層的ViewGroup
3)null,焦點不在它的下層
舉個例子:
很明顯,如果界面上有焦點的話,從上層往下一層層找,就能找到。View/ViewGroup提供findFocus方法,用于找到目前範圍内的焦點,
[File]View.java
public View findFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;//傳回自己如果已聚焦
}
[File]ViewGroup.java
@Override
public View findFocus() {
if (isFocused()) {
return this;//傳回自己如果已聚焦
}
if (mFocused != null) {
return mFocused.findFocus();//焦點在下層,傳回下層findFocus結果
}
return null;//無焦點
}
那麼問題來了,這個 mFocused 是怎麼更新的呢,又是怎麼讓它失焦呢?關鍵就在于 handleFocusGainInternal 中的這個調用:
mParent.requestChildFocus(this, this);//告知上層ViewGroup自己已聚焦
[File] 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);//讓自己範圍内已聚焦的失焦
}
mFocused = child;//更新為包含焦點的child
}
if (mParent != null) {
mParent.requestChildFocus(this, focused);//告知上層ViewGroup自己包含焦點
}
}
我 們可以看 requestChildFocus 這個方法會一層層往上調用,讓 mFocused 失焦,然後更新為新的 child ;具體地,前一焦點是怎麼被清除的呢,我們來看下 unFocus 這個方法,
[File]View.java
void unFocus(View focused) {
clearFocusInternal(focused, false, false);//去除聚焦标志,通知listener, 更新Drawable 狀态
}
[File]ViewGroup.java
@Override
void unFocus(View focused) {
if (mFocused == null) {
super.unFocus(focused);
} else {
mFocused.unFocus(focused);
mFocused = null;
}
}
對于 ViewGroup 來說,如果 mFocused 有記錄,則調用其 unFocus 方法,最後将其置為 null 。這樣就做到了一層層住下更新mFocused, 最終調用焦點View 的 clearFocusInternal 。至此,焦點的請求到更新 的邏輯就應該了然于胸了。
2.4 <requestFocus/> 标簽
這個标簽用于布局檔案中,如:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn0"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<requestFocus/>
</Button>
</LinearLayout>
添加了該标簽的可聚焦的 View ,如上布局中的 btn1, 将在加載的時候(LayoutInflater#inflate)調用它的 requestFocus 方法,
public abstract class LayoutInflater {
......
private static final String TAG_REQUEST_FOCUS = "requestFocus";
......
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
......
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
......
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
}
......
}
......
}
private void parseRequestFocus(XmlPullParser parser, View view)
throws XmlPullParserException, IOException {
view.requestFocus();//請求焦點
......
}
......
}
3. 按鍵事件(KeyEvent)與焦點查找
KeyEvent的分發與 TouchEvent 的分發,大緻類似,從ViewRootImpl 開始一層層往下分發,
ViewRootImpl.java (API 25)
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {//調用頂層View(一般為ViewGroup)的 dispatchKeyEvent
return FINISH_HANDLED;
}
…...
// Handle automatic focus changes.
//如果前面都沒有消費掉這個事件,下面将自動根據按鍵方向查找焦點
if (event.getAction() == KeyEvent.ACTION_DOWN) {
int direction = 0;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT://左
if (event.hasNoModifiers()) {
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT://右
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
break;
case KeyEvent.KEYCODE_DPAD_UP://上
if (event.hasNoModifiers()) {
direction = View.FOCUS_UP;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN://下
if (event.hasNoModifiers()) {
direction = View.FOCUS_DOWN;
}
break;
case KeyEvent.KEYCODE_TAB:
if (event.hasNoModifiers()) {
direction = View.FOCUS_FORWARD;
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
direction = View.FOCUS_BACKWARD;
}
break;
}
if (direction != 0) {
View focused = mView.findFocus();//找到聚焦的View
if (focused != null) {//已有焦點
View v = focused.focusSearch(direction);//從已聚焦的View查找下一可聚焦的view
if (v != null && v != focused) {
……
if (v.requestFocus(direction, mTempRect)) {
//播放按鍵音效
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return FINISH_HANDLED;
}
}
// 沒找到新焦點,最後給mView 一次處理焦點移動的機會
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
View v = focusSearch(null, direction);//從頂層開始查找下一可聚焦的view
if (v != null && v.requestFocus(direction)) {//請求焦點
return FINISH_HANDLED;
}
}
}
}
return FORWARD;
}
可以 看到,dispatchKeyEvent 如果沒有消費掉,将自動查找焦點。
3.1 KeyEvent分發
如果不重寫dispatchKeyEvent,KeyEvent分發的最終目标是目前焦點View/ViewGroup。還是以下面這個圖為例,分發的路徑是RootViewGroup-->ViewGroup2-->view2
實作較TouchEvent的分發簡單許多,就是根據前面提到的ViewGroup中mFocused來定位,我們來看下ViewGroup的dispatchKeyEvent的實作,
[File]ViewGroup.java
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 1);
}
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
if (super.dispatchKeyEvent(event)) {//如果ViewGroup自己聚焦了,則進分發給自己處理
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {//焦點在mFocused中,繼續往下分發
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
最終分發到焦點View上,将回調 OnKeyListener 或 KeyEvent.Callback,
[File]View.java
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 0);
}
// 回調OnKeyListener 的 onKey 方法
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
// View 實作了KeyEvent.Callback,包含onKeyDown,onKeyUp,onKeyLongPress等方法
// 這裡将分發給這個callback
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
可 以看到預設的 ,ViewGroup 分發 KeyEvent 過程不會找焦點, 不消費方向鍵, 而是由ViewRootImpl 來處理。那麼另一個重要的按鍵 “确認鍵”呢 ? 如果目前有焦點,然後按 下确認鍵可能需要産生點選事件,這件事就是在 View 的 onKeyDown,onKeyUp 中處理的,
[File]View.java
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (KeyEvent.isConfirmKey(keyCode)) {//如果是确認鍵
if ((mViewFlags & ENABLED_MASK) == DISABLED) {
return true;
}
// Long clickable items don't necessarily have to be clickable.
if (((mViewFlags & CLICKABLE) == CLICKABLE
|| (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
&& (event.getRepeatCount() == 0)) {
// For the purposes of menu anchoring and drawable hotspots,
// key events are considered to be at the center of the view.
final float x = getWidth() / 2f;
final float y = getHeight() / 2f;
setPressed(true, x, y);//設定狀态為已按下
checkForLongClick(0, x, y);
return true;
}
}
return false;
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (KeyEvent.isConfirmKey(keyCode)) {
if ((mViewFlags & ENABLED_MASK) == DISABLED) {
return true;
}
if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {
setPressed(false);//設定狀态為未按下
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
return performClick();//回調OnClickListener
}
}
}
return false;
}
3.2焦點查找
前面提到ViewRootImpl裡可能會根據按鍵方向查找焦點,如果已有聚焦的View,就調用 View 的focusSearch,從該View開始查找,否則調用自己的focusSearch 方法從頂層開始查找。我們先來看 View 的這個方法,
[File]View.java
public View focusSearch(@FocusRealDirection int direction) {
if (mParent != null) {
return mParent.focusSearch(this, direction);
} else {
return null;
}
}
View 簡單地讓上一層ViewGroup 來查找,再來看ViewGroup 的這個方法,
[File]ViewGroup.java
public View focusSearch(View focused, int direction) {
if (isRootNamespace()) {// installDecor時設定mDecor.setIsRootNamespace(true)
// 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;
}
一直調用上一層 ViewGroup 的 focusSearch ,直到目前是rootView, 使用 FocusFinder 在rootView 範圍内開始查找,實際上 ViewRootImpl 裡也同樣是使用FocusFinder 來查找,我們下面看下 findNextFocus 這個方法,
[File]FocusFinder.java
public final View findNextFocus(ViewGroup root, View focused, int direction) {
if (focused != null) {
// check for user specified next focus//查找使用者指定的下一個焦點
View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
if (userSetNextFocus != null &&
userSetNextFocus.isFocusable() &&
(!userSetNextFocus.isInTouchMode() ||
userSetNextFocus.isFocusableInTouchMode())) {
return userSetNextFocus;
}
// fill in interesting rect from focused
……
//将 mFocusedRect 設成focused的區域
} else {
// make up a rect at top left or bottom right of root
//将 mFocusedRect 設成root的區域
……
}
return findNextFocus(root, focused, mFocusedRect, direction);//根據區域和方向查找
}
如果已經存在焦點,并且該焦點 View 設定了某方向的下一焦點 View的 ID,那麼根據 ID 找出這個 View 即可;否則根據目前焦點區域按方向查找,這個算法這裡就暫不介紹了。