一,焦点相关api说明
- 安卓中的焦点其本质是一个被标记的
View
,具有唯一性,记录焦点的View
在ViewGroup
中的定义如下:
// The view contained within this ViewGroup that has or contains focus.
private View mFocused;
View
中定义了一个名为setFocusable
的方法,该方法用于设置该View
是否可以接收到焦点标记,并非主动标记为焦点View
,如下:
/**
* Set whether this view can receive the focus.
*/
public void setFocusable(boolean focusable) {
...
}
View
中的requestFocus
方法定义如下:
/**
* Call this to try to give focus to a specific view or to one of its
* descendants.
* @return Whether this view or one of its descendants actually took focus.
*/
public final boolean requestFocus() {
return requestFocus(View.FOCUS_DOWN);
}
二,分析两个过程
这里我们分析较为常见的两种情况,第一种就是我们主动调用1,requestFocus
去尝试标记焦点View
的过程。第二种就是我们通过键盘或者遥控等输入设备触发系统处理焦点变化的过程。
requestFocus
调用触发焦点变化
- 先来看下
View
中requestFocus
的调用,其依次调用如下方法:
requestFocus()->requestFocus(int direction)->
requestFocus(int direction, Rect previouslyFocusedRect)->
requestFocusNoSearch(int direction, Rect previouslyFocusedRect) ->
handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect)
- 在来看下
handleFocusGainInternal
的定义:
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
...if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
// 调用parent的requestChildFocus
if (mParent != null) {
mParent.requestChildFocus(this, this);
}// 更新dispatchOnGlobalFocusChange
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}// 更新onFocusChanged
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
}
}
至此我们了解到handleFocusGainInternal
有这样的三个处理:
- 1,调用parent
的requestChildFocus
- 2,更新dispatchOnGlobalFocusChange
- 3,更新onFocusChanged
这里我们先说2、3两点。View
中定义了一个名为getViewTreeObserver
方法可以拿到ViewTreeObserver
,该接口用于获取一些view tree
中的全局事件,和焦点相关的有一个名为addOnGlobalFocusChangeListener
的方法,通过该方法我们可以指定一个全局的焦点变化的监听器,当全局的焦点发生变化时,监听器接口的onGlobalFocusChanged
会被更新。而此接口的更新正是在上述handleFocusGainInternal
中完成的。当一个View
自身可以接收焦点标记时,我们通常可以通过重写其内部的onFocusChanged
方法监听到其自身焦点变化,这里的onFocusChanged
方法的更新也位于handleFocusGainInternal
中。至此,我们再来深入了解下handleFocusGainInternal
中的第一个处理意义何在,首先我们来看,ViewGroup
中的requestChildFocus
定义如下:
@Override
public void requestChildFocus(View child, View focused) {
...// 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;
}// 调用parent的requestChildFocus
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
我们注意到,2,输入事件触发焦点变化ViewGroup
中的requestChildFocus
方法内部又调用了parent的requestChildFocus,这样就意味着当view tree
中的一个View
被标记为焦点时,则该view tree
中的从焦点View
到根View
的所有View
的requestChildFocus
都会被调用。这意味着view tree
中所有的View
都会接收到焦点变化的消息。
- 首先我们先来看下
ViewRootImpl
中的一段代码:
/**
* Delivers post-ime input events to the view hierarchy.
*/
final class ViewPostImeInputStage extends InputStage {
...
}
- 再继续往下看:
// Deliver the key to the view hierarchy.
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) {
View v = focused.focusSearch(direction);
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);
}
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
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
View v = focusSearch(null, direction);
if (v != null && v.requestFocus(direction)) {
return FINISH_HANDLED;
}
}
}
我们注意到当输入事件发生时,会先寻找当前视图中被标记为焦点的View
,如果不为空,则会调用焦点View
的focusSearch
方法寻找新的焦点。我们来分析一下这个方法,该方法在View
中的定义如下:
/**
* Find the nearest view in the specified direction that can take focus.
* This does not actually give focus to that view.
*/
public View focusSearch(@FocusRealDirection int direction) {
if (mParent != null) {
return mParent.focusSearch(this, direction);
} else {
return null;
}
}
再看ViewGroup
中的focusSearch
方法:
/**
* Find the nearest view in the specified direction that wants to take focus.
*/
@Override
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;
}
当parent
不为空的时候,就继续调用parent
的focusSearch
方法,这就意味着从当前焦点开始到根View
的所有节点都会接收到寻找焦点的消息,要做处理,只需要重写特定节点的focusSearch
方法即可。当寻找至根View
时,则新的焦点会借助一个辅助类FocusFinder
去寻找。FocusFinder
中的相关逻辑我们稍后再说。
- 我们再回到输入事件这里,这里
View v = focused.focusSearch(direction);
执行完之后,最终还是会调用requestFocus
来尝试标记焦点,至于requestFocus
方法,我们前面已经分析过了。其实View
中提供了一个dispatchUnhandledMove
方法,允许我们在输入事件触发之后焦点分发之前加入自己的焦点处理逻辑。这一点可以由下面代码看出:
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return FINISH_HANDLED;
}
而当
findFocus
方法返回空时,则会通过ViewRootImpl
内部的focusSearch
方法寻找新焦点。该方法内部则还是借助辅助类FocusFinder
去寻找新焦点的。【安卓机顶盒开发中的焦点】待更新…