从Android源码的角度理解应用开发(2)-Focus机制

时间:2021-04-16 20:41:54

前言

为什么要有Focus机制

这是因为,如果界面上有两个按钮,假设你按了回车,这时候究竟代表着你按了哪个按钮呢?这时候就需要Focus来帮忙了,因为如果一个View得到焦点,那么这个View就可以处理键盘的输入,做出回应。

两种模式

然后Android的设备现在大多数都是触屏的,键盘非常少,但是还有类似键盘的输入类似TV的DPad。键盘输入与触屏输入是一对有矛盾的交互设计方式。所以Android有两个模式来分别对待这两种交互方式,触摸模式(TouchMode)普通模式,普通模式以键盘按下开始,触屏(Pointer操作,包括触屏,鼠标操作)为结束,而触摸模式相反,触屏为开始,触摸模式以键盘按下结束。

Focus机制必不可少

显然,普通模式是需要Focus机制来支持键盘Dpad等操作,但这并不代表着触摸模式就不需要焦点机制,比如手机需要打字时候软键盘需要对EditText进行输入,EditText就获取了焦点。

触摸模式与普通模式的切换

触摸模式与普通模式是通过ViewRootImpl中的ensureTouchModeLocally(boolean)来进行切换。

/**
* Ensure that the touch mode for this window is set, and if it is changing,
* take the appropriate action.
* @param inTouchMode Whether we want to be in touch mode.
* @return True if the touch mode changed and focus changed was changed as a result
*/
private boolean ensureTouchModeLocally(boolean inTouchMode) {
if (DBG) Log.d("touchmode", "ensureTouchModeLocally(" + inTouchMode + "), current "
+ "touch mode is " + mAttachInfo.mInTouchMode);

if (mAttachInfo.mInTouchMode == inTouchMode) return false;

mAttachInfo.mInTouchMode = inTouchMode;
mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged(inTouchMode);

return
(inTouchMode) ? enterTouchMode() : leaveTouchMode();
}

所以我们只需要找到ensureTouchModeLocally(boolean)在哪些地方切换就能知道什么时候进入触摸模式与退出触摸模式。

初始化模式

新建ViewRootImpl是否进入触摸模式是由WindowManagerService中的mInTouchModemInTouchMode代表着当前系统是否在TouchMode环境下。而mInTouchMode的开机初始化值由R.bool.config_defaultInTouchMode决定,之后将随用户对系统的操作决定。

用户操作:进入触摸模式

用户操作进入触摸模式的情况非常单一,代码在ViewRootImpl中

final class EarlyPostImeInputStage extends InputStage{
···
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
return processPointerEvent(q);
}
}
return FORWARD;
}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
···

// Enter touch mode on down or scroll.
final int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_SCROLL) {
ensureTouchMode(true); //进入触摸模式
}
···
return FORWARD;
}
···
}

可以看到当有Pointer操作(鼠标,触摸)传到EarlyPostImeStage时候,Down操作与Scroll操作将进入触摸模式

用户操作:退出触摸模式

退出方法1

同理,在EarlyPostImeStage中会检测用户是不是使用Dpad或者键盘输入,如果是,也会退出触摸模式

```
final class EarlyPostImeInputStage extends InputStage{
···
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
return processPointerEvent(q);
}
}
return FORWARD;
}
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;

// If the key's purpose is to exit touch mode then we consume it
// and consider it handled.
if (checkForLeavingTouchModeAndConsume(event)) {
return FINISH_HANDLED;
}

// Make sure the fallback event policy sees all keys that will be
// delivered to the view hierarchy.
mFallbackEventHandler.preDispatchKeyEvent(event);
return FORWARD;
}

···
}

private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) {
// Only relevant in touch mode.
if (!mAttachInfo.mInTouchMode) {
return false;
}

// Only consider leaving touch mode on DOWN or MULTIPLE actions, never on UP.
final int action = event.getAction();
if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) {
return false;
}

// Don't leave touch mode if the IME told us not to.
if ((event.getFlags() & KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) {
return false;
}

// 1.导航键退出触摸模式
if (isNavigationKey(event)) {
return ensureTouchMode(false);
}

// 2.键盘输入退出触摸模式
if (isTypingKey(event)) {
ensureTouchMode(false);
return false;
}

return false;
}

退出方法2

退出TouchMode还可能通过辅助功能来退出,当用辅助功能转移焦点时候,就会退出触摸模式

//View.java
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
···
switch (action) {
···
case AccessibilityNodeInfo.ACTION_FOCUS: {
if (!hasFocus()) {
// Get out of touch mode since accessibility
// wants to move focus around.
getViewRootImpl().ensureTouchMode(false);
return requestFocus();
}
} break;
···
}
···
}

退出方法3

还有第三种退出触摸模式的方式:通过requestFocusFromTouch()这个方法相当于退出触摸模式后再调用一次requestFocus()

public final boolean requestFocusFromTouch() {
// Leave touch mode if we need to
if (isInTouchMode()) {
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null) {
viewRoot.ensureTouchMode(false);
}
}
return requestFocus(View.FOCUS_DOWN);
}

因为有些大多数View只设置了Focusable属性,但是没有设置FocusableInTouchMode属性,在触摸模式情况下,只设置Focusable属性没有设置FocusableInTouchMode属性的View是无法获取焦点的,所以调用requestFocus会无效。所以必须调用requestFocusFromTouch先退出触摸模式后获取焦点。

焦点查找

ViewRootImpl部分

对于轨迹球的上下左右,键盘的上下左右,tab,shift-tab,或者Dpad的上下左右,如果当前焦点没有消费完事件,会触发系统自动寻找下个焦点。关键代码如下:

/**
* Delivers post-ime input events to the view hierarchy.
*/

final class ViewPostImeInputStage extends InputStage {
···
@Override
protected int onProcess(QueuedInputEvent q) {
//1. 如果事件是KeyEvent,会走这里,接下来触发焦点转移(虽然轨迹球上下左右不是KeyEvent,但是可能会在人工合成阶段转化成KeyEvent)
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
···
}
}

private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;

// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}

if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}

// If the Control modifier is held, try to interpret the key as a shortcut.
if (event.getAction() == KeyEvent.ACTION_DOWN
&& event.isCtrlPressed()
&& event.getRepeatCount() == 0
&& !KeyEvent.isModifierKey(event.getKeyCode())) {
if (mView.dispatchKeyShortcutEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
}

// Apply the fallback event policy.
if (mFallbackEventHandler.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}

// 2.KeyEvent如果没有被焦点消费的话,走这里
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;
}
// 3.焦点主要逻辑部分
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;
}
}
}
}
return FORWARD;
}
}

从第3点焦点的主要逻辑部分可以看到,主要通过调用View(ViewGroup)的findFocus来做一次树高度的查找,从上到下找到当前焦点,再通过ViewRootImpl,ViewGroup或View的focusSearch查找,从下到上查到下一个焦点,并且对下一个焦点进行对焦。

//View.java
public View focusSearch(@FocusRealDirection int direction) {
if (mParent != null) {
return mParent.focusSearch(this, direction);
} else {
return null;
}
}

//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;
}
//ViewRootImpl.java
public View focusSearch(View focused, int direction) {
checkThread();
if (!(mView instanceof ViewGroup)) {
return null;
}
return FocusFinder.getInstance().findNextFocus((ViewGroup) mView, focused, direction);
}

FocusFinder部分

从上面逻辑可以看出,最后都将调用到FocusFinder的findNextFocus(viewgroup,view,direction),对于第一个参数viewgroup,就是window中addView时候添加的View,也就是decorView。对于第二个参数view,如果没有当前没有焦点则为null,否则就是焦点。所以接下来看findNextFocus(viewgroup,view,direction)的实现。

//FocusFinder.java
public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
mFocusedRect.set(focusedRect);
return findNextFocus(root, null, mFocusedRect, direction);
}

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
if (focused != null) {
next = findNextUserSpecifiedFocus(root, focused, direction);
}
if (next != null) {
return next;
}
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
root.addFocusables(focusables, direction);//1.添加可能获取到焦点的View
if (!focusables.isEmpty()) {
next = findNextFocus(root, focused, focusedRect, direction, focusables);//2.确定焦点区域 }
} finally {
focusables.clear();
}
return next;
}

以上代码比较关键的逻辑就是1,2
对于第1点,添加可能的焦点集合,对于addFocusable的逻辑主要跟desendantFocusability属性有关,分为三种,block,after,before。addFocusable会遍历整个View树,如果某个节点是block,则不会添加次节点的子节点;如果当前节点是before,则会添加子节点与当前节点;如果当前节点是after,则只会在子节点全都不能获取焦点的情况下添加当前节点。注意这里能不能获取到焦点跟当前焦点的模式有关,如果是触摸模式,只有focusableInTouchMode的节点来能获取焦点,如果是普通模式,则只需要focusable即可获取焦点。

对于第2点,终于到了确定焦点区域。

    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
int direction, ArrayList<View> focusables) {
//1. 确定焦点区域
if (focused != null) {
if (focusedRect == null) {
focusedRect = mFocusedRect;
}
// fill in interesting rect from focused
focused.getFocusedRect(focusedRect);
root.offsetDescendantRectToMyCoords(focused, focusedRect);
} else {
if (focusedRect == null) {
focusedRect = mFocusedRect;
// make up a rect at top left or bottom right of root
switch (direction) {
case View.FOCUS_RIGHT:
case View.FOCUS_DOWN:
setFocusTopLeft(root, focusedRect);
break;
case View.FOCUS_FORWARD:
if (root.isLayoutRtl()) {
setFocusBottomRight(root, focusedRect);
} else {
setFocusTopLeft(root, focusedRect);
}
break;

case View.FOCUS_LEFT:
case View.FOCUS_UP:
setFocusBottomRight(root, focusedRect);
break;
case View.FOCUS_BACKWARD:
if (root.isLayoutRtl()) {
setFocusTopLeft(root, focusedRect);
} else {
setFocusBottomRight(root, focusedRect);
break;
}
}
}
}
//2 确定算法
switch (direction) {
case View.FOCUS_FORWARD:
case View.FOCUS_BACKWARD:
return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
direction);
case View.FOCUS_UP:
case View.FOCUS_DOWN:
case View.FOCUS_LEFT:
case View.FOCUS_RIGHT:
return findNextFocusInAbsoluteDirection(focusables, root, focused,
focusedRect, direction);
default:
throw new IllegalArgumentException("Unknown direction: " + direction);
}
}

可以知道,如果当前有焦点,则焦点区域的矩形为焦点,如果当前没有焦点,当按下“下”“右”时候,焦点区域为DecorView左上的端点,当按下“左”“上”焦点区域为DecorView右下的断电。然后根据按下的按键,选择findNextFocusInRelativeDirection或者findNextFocusInAbsoluteDirection算法,我们在此只分析findNextFocusInAbsoluteDirection算法:

findNextFocusInAbsoluteDirection

View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
Rect focusedRect, int direction) {
//1.先把匹配矩形设置成最坏的情况,这样在接下来的比较中,总能把这种最坏的情况淘汰掉。
mBestCandidateRect.set(focusedRect);
switch(direction) {
case View.FOCUS_LEFT:
mBestCandidateRect.offset(focusedRect.width() + 1, 0);
break;
case View.FOCUS_RIGHT:
mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
break;
case View.FOCUS_UP:
mBestCandidateRect.offset(0, focusedRect.height() + 1);
break;
case View.FOCUS_DOWN:
mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
}

View closest = null;

int numFocusables = focusables.size();
//2.遍历Focusables
for (int i = 0; i < numFocusables; i++) {
View focusable = focusables.get(i);

// only interested in other non-root views
if (focusable == focused || focusable == root) continue;

// get focus bounds of other view in same coordinate system
focusable.getFocusedRect(mOtherRect);
root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
//3. 使用比较算法isBetterCandidate来求得最好的匹配结果
if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
mBestCandidateRect.set(mOtherRect);
closest = focusable;
}
}
return closest;
}

可以看到,通过isBetterCandidate来比较所有focusables,选取最好的情况来作为下一个焦点。

isBetterCandidate

//是否rect1更加匹配
boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {

//Candidate算法用于方向判断,如果rect1方向不对,那就不能淘汰rect2
if (!isCandidate(source, rect1, direction)) {
return false;
}
//如果rect2方向不对,但是rect1方向对,那么rect1更加匹配
if (!isCandidate(source, rect2, direction)) {
return true;
}

// beamBeats算法用于比较rect1,rect2主要通过在direction方向上是否重叠以及距离来比较
if (beamBeats(direction, source, rect1, rect2)) {
return true;
}

// if rect2 is better, then rect1 cant' be :)
if (beamBeats(direction, source, rect2, rect1)) {
return false;
}

// 以上都比较不了,那么就用主次轴方向上距离的比较来算出结果
return (getWeightedDistanceFor(
majorAxisDistance(direction, source, rect1),
minorAxisDistance(direction, source, rect1))
< getWeightedDistanceFor(
majorAxisDistance(direction, source, rect2),
minorAxisDistance(direction, source, rect2)));
}