1.前言
在 Android 设备里,点击上下左右按键的时候,UI 会随着焦点的改变在底部显示一个阴影,当点击 Enter 键的时候会触发对应 View 的点击事件。本文从源码角度分析一下整个流程,焦点是如何移动的以及点击是如何触发的。
2.Android 方向按键处理
2.1 方向按键
以下是点击了方向按键时相关的调用栈
java.lang.Exception: requestFocusNoSearch direction:130
at android.view.View.requestFocusNoSearch(View.java:13542)
at android.view.View.requestFocus(View.java:13538)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3328)
at android.view.View.requestFocus(View.java:13505)
at android.view.View.restoreDefaultFocus(View.java:13483)
at android.view.ViewGroup.restoreDefaultFocus(ViewGroup.java:3390)
at android.view.ViewRootImpl.leaveTouchMode(ViewRootImpl.java:5687)
at android.view.ViewRootImpl.ensureTouchModeLocally(ViewRootImpl.java:5620)
at android.view.ViewRootImpl.ensureTouchMode(ViewRootImpl.java:5602)
at android.view.ViewRootImpl.checkForLeavingTouchModeAndConsume(ViewRootImpl.java:7626)
at android.view.ViewRootImpl.access$2400(ViewRootImpl.java:225)
at android.view.ViewRootImpl$EarlyPostImeInputStage.processKeyEvent(ViewRootImpl.java:6139)
at android.view.ViewRootImpl$EarlyPostImeInputStage.onProcess(ViewRootImpl.java:6123)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5729)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5786)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5752)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:5950)
at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:6108)
at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:3159)
at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:2723)
at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:2714)
at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:3136)
at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:154)
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:335)
at android.os.Looper.loopOnce(Looper.java:161)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7870)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
从调用栈可以看到,按键是 InputMethodManager$ImeInputEventSender 接收到,然后交由 ViewRootImpl 处理,ViewRootImpl 经过一系类的 State 的链式调用,到了 ViewGroup 的 requestFocus 方法,又最终调用的 View 的 requestFocus 方法。这里我们发现按键事件是从输入法(ImeInputEventSender 是应用用于接收输入法的按键事件的)传过来的,而不是 ViewRootImpl 接收到处理的。
2.2 普通按键分发流程
我们知道 ViewRootImpl 中 WindowInputEventReceiver 能够接受键盘的输入事件,然后交给 View.dispatchKeyEvent 进行分发,一般会分发到 Activity 的 onKeyDown 与 onKeyUp 方法。调用栈如下:
at com.example.myapplication.MainActivity.onKeyDown(MainActivity.kt:106)
at android.view.KeyEvent.dispatch(KeyEvent.java:2875)
at android.app.Activity.dispatchKeyEvent(Activity.java:4250)
at androidx.core.app.ComponentActivity.superDispatchKeyEvent(ComponentActivity.java:122)
at androidx.core.view.KeyEventDispatcher.dispatchKeyEvent(KeyEventDispatcher.java:84)
at androidx.core.app.ComponentActivity.dispatchKeyEvent(ComponentActivity.java:140)
at androidx.appcompat.app.AppCompatActivity.dispatchKeyEvent(AppCompatActivity.java:599)
at androidx.appcompat.view.WindowCallbackWrapper.dispatchKeyEvent(WindowCallbackWrapper.java:59)
at androidx.appcompat.app.AppCompatDelegateImpl$AppCompatWindowCallback.dispatchKeyEvent(AppCompatDelegateImpl.java:3090)
at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:411)
at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:6381)
、、、
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5733)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:8700)
at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:8651)
at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:8620)
at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:8823)
at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:259)
2.3 系统功能按键处理
实际上并不是所有的按键都会分发给应用,大部分的例如 a、b、c 之类的字母按键会分发给 APP 的 Activity 处理,但是部分功能按键(例如亮度调节按键、音量键、媒体按键等)系统会直接处理,不交给 View 分发。还有些按键(例如上下左右按键、Enter 按键)系统会先交给输入法,输入法处理后再传给与输入法绑定的 window。这部分定义在 InputMethodManager 中。
[InputMethodManager.java]
private final class ImeInputEventSender extends InputEventSender {
public ImeInputEventSender(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
@Override
public void onInputEventFinished(int seq, boolean handled) {
finishedInputEvent(seq, handled, false);
}
}
void finishedInputEvent(int seq, boolean handled, boolean timeout) {
final PendingEvent p;
synchronized (mH) {
int index = mPendingEvents.indexOfKey(seq);
、、、
}
invokeFinishedInputEventCallback(p, handled);
}
void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
p.mHandled = handled;
if (p.mHandler.getLooper().isCurrentThread()) {
p.run();
} else {
、、、
}
}
private final class PendingEvent implements Runnable {
、、、
@Override
public void run() {
mCallback.onFinishedInputEvent(mToken, mHandled);
、、、
}
}
此处 mCallback 的实现类是 ViewRootImpl$ImeInputStage
[ViewRootImpl.java]
final class ImeInputStage extends AsyncInputStage
implements InputMethodManager.FinishedInputEventCallback {
@Override
public void onFinishedInputEvent(Object token, boolean handled) {
QueuedInputEvent q = (QueuedInputEvent)token;
if (handled) {
finish(q, true);
return;
}
forward(q);
}
}
此时如果 handled 为 true 表示输入法已经处理过,走到 ViewRootImpl.finish,否则走到 ViewRootImpl.forward 方法。这两个方法都会触发 ViewRootImpl 内的调用链进行处理,一般不会分发给 Activity。
3.View 焦点获取
3.1 View.requestFocus
[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);
}
我们发现无论是通过按键移动焦点还是手动调用 requestFocus 方法进行抢占焦点其实都会走到同一个地方 requestFocusNoSearch。
3.2 View.requestFocusNoSearch
[View.java]
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// 检查当前View是否有焦点,灭有焦点不处理了,直接返回false
if (!canTakeFocus()) {
return false;
}
// 如果当前是处在触摸模式下,还要检查focusableInTouchMode ,如果为false。
// 说明 该View不能通过触摸获取焦点
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}
// 检查父View是不是阻拦当前View获取焦点
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
if (!isLayoutValid()) {
mPrivateFlags |= PFLAG_WANTS_FOCUS;
} else {
clearParentsWantFocus();
}
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
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;
}
hasAncestorThatBlocksDescendantFocus方法会循环向上查找父View,检查DescendantFocusability属性,该属性有三个值。
- FOCUS_BLOCK_DESCENDANTS:会阻止其子View获取焦点,哪怕子View是可获取焦点的。
- FOCUS_AFTER_DESCENDANTS:当所有子View都不获取焦点时,该View才获取焦点,也就是最后才获取焦点。
- FOCUS_BEFORE_DESCENDANTS:在所有子View之前获取焦点。
接下来继续看handleFocusGainInternal实现。
3.3 handleFocusGainInternal
ViewGroup重写了handleFocusGainInternal方法。
[ViewGroup.java]
@Override
void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) {
if (mFocused != null) {
mFocused.unFocus(this);
mFocused = null;
mFocusedInCluster = null;
}
super.handleFocusGainInternal(direction, previouslyFocusedRect);
}
mFocused 记录了ViewGroup中当前以获取到焦点的子View,首先清理掉mFocused ,然后再调用super.handleFocusGainInternal,也就是View的handleFocusGainInternal方法。
[View.java]
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " requestFocus()");
}
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;
//找到之前已经获取到焦点的View,用于清理其焦点。找不到为null,无需清理。
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
if (mParent != null) {
mParent.requestChildFocus(this, this);
updateFocusedInCluster(oldFocus, direction);
}
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
//调用焦点变更的监听
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();//更新drawable,突出一下焦点View
}
}
首先会更新当前 View 的标记位 mPrivateFlags 记录自己的 isFocused 状态,PFLAG_FOCUSED表示当前View获取焦点,接着通过 rootView 查找到当前的焦点赋值给 oldFocus,以用于后续逐层清理旧的焦点View的焦点,然后调用 parent 的 requestChildFocus 方法告知 parent 自己当前获取到焦点。
[ViewGroup.java]
@Override
public void requestChildFocus(View child, View focused) {
、、、
//3.2提到过,判断是FOCUS_BLOCK_DESCENDANTS如果是FOCUS_BLOCK_DESCENDANTS,拦截焦点。
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
// Unfocus us, if necessary
super.unFocus(focused);
// mFocused 记录了当前已经获取到焦点的View,清理掉mFocused 的焦点,并将child赋值给mFocused 。
if (mFocused != child) {
if (mFocused != null) {
//清理旧的焦点View的焦点
mFocused.unFocus(focused);
}
mFocused = child;//更新新的焦点View
}
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
[View.java]
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
mPrivateFlags &= ~PFLAG_FOCUSED;//清理焦点标记为
if (propagate && mParent != null) {
// 通知 parent 清除自己(当前的焦点)的 mFocus 值,因为焦点已经不在该 View 树节点下
mParent.clearChildFocus(this);
}
// 回调焦点状态变更的通知
onFocusChanged(false, 0, null);
// 刷新失去焦点后的 drawable 状态
refreshDrawableState();
if (propagate && (!refocus || !rootViewRequestFocus())) {
notifyGlobalFocusCleared(this);
}
}
}
requestChildFocus先进行状态判断,然后通过调用自己以及焦点View的unFocus方法清理焦点,unFocus调用了View的clearFocusInternal方法,同时将当前申请焦点child子View记录到mFocused。在清理完焦点后继续向上层父View请求焦点。因为上层也是ViewGroup,往上递归调用,最终整个View树都将旧的焦点View清理一遍,并将新的焦点View更新到mFocused 记录下来。等到整个View树更新完成,继续回到handleFocusGainInternal内,onFocusChanged回调设置的OnFocusChangeListener监听方法,通知焦点变更。refreshDrawableState刷新背景状态,突出焦点View。
这里有一点要注意一下,当某一个ViewGroup有一个子view获取到了焦点时,该ViewGroup是没有焦点的,有焦点的只有子View。因为焦点的判断是根据标记位mPrivateFlags 判断的,只有子View的标记位被赋值为PFLAG_FOCUSED。isFocused与hasFocus不是一回事。
4.descendantFocusability标记位作用
在前面3.2中提到descendantFocusability有三种行为,决定ViewGroup对焦点的处理方式。这里看一下源码时如何实现的。
[ViewGroup.java]
@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " ViewGroup.requestFocus direction="
+ direction);
}
int descendantFocusability = getDescendantFocusability();
// 主要还是看 ViewGroup 设置的焦点拦截模式
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
// 拦截掉了焦点,前面有提到View中FOCUS_BLOCK_DESCENDANTS直接返回不处理了
//也就是拦截了焦点
return super.requestFocus(direction, previouslyFocusedRect);
case FOCUS_BEFORE_DESCENDANTS: {
// 首先调用 super 的逻辑在自己中 requestFocus,如果自己请求焦点失败再遍历子 View 进行 requestFocus
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
case FOCUS_AFTER_DESCENDANTS: {
// 与 FOCUS_BEFORE_DESCENDANTS 相反,先遍历子 View 进行 requestFocus,如果子 View 都请求焦点失败后再调用 super 的逻辑在自己中 requestFocus
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
return took ? took : super.requestFocus(direction, previouslyFocusedRect);
}
、、、
}
}
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
int index;
int increment;
int end;
int count = mChildrenCount;
if ((direction & FOCUS_FORWARD) != 0) {
// 从前往后遍历
index = 0;
increment = 1;
end = count;
} else {// 从后往前遍历
index = count - 1;
increment = -1;
end = -1;
}
final View[] children = mChildren;
// mChildren 数组中保存了所有的 childView
for (int i = index; i != end; i += increment) {
View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
// 遍历子 View,并且 View 可见
if (child.requestFocus(direction, previouslyFocusedRect)) {
// 该子 View 请求焦点
return true;// 请求焦点成功,直接返回
}
}
}
return false;
}
onRequestFocusInDescendants 主要功能就是遍历该 ViewGroup 下所有子 View,然后对可见的子 View 调用 requestFocus,如果请求焦点成功,则直接返回 true,至此,ViewGroup.requestFocus 也处理完毕了。
5. findFocus
有些时候我们会通过findFocus方法查询当前获取到焦点的View,该方法在View中定义,View的子类ViewGroup重写了该方法。View的实现比较简单,就是查询一下自己的mPrivateFlags标记位,如果获取到了焦点就将自己返回,否则返回null。下面是ViewGroup 的实现。
[ViewGroup.java]
@Override
public View findFocus() {
if (DBG) {
System.out.println("Find focus in " + this + ": flags="
+ isFocused() + ", child=" + mFocused);
}
//如果自己获取到了焦点,返回自己
if (isFocused()) {
return this;
}
//如果是某一层级的子View获取到了焦点,返回子View。
if (mFocused != null) {
return mFocused.findFocus();
}
return null;
}