Android Focus的管理
本篇题目是"Android Focus管理",内容包括Input focus和Accessibility focus。
文章大纲:
-
focus的分类
-
Input focus
window focus的控制逻辑
window内view focus的控制逻辑
touch mode如何发生变化,发生变化后对view的影响,比如已经highlight的UI是否会清除?
-
Accessibility focus
哪些内容?
Touch mode: Touch mode 下,大部分view是不需要有focus 效果的;很多view也不愿意在touch mode下设置为focusable。
1. Focus
| 类型 | Input Focus | Accessibility Focus |
| 定义 | 用于标识当前接收用户输入(如键盘、方向键)的视图。 | 无障碍服务(如屏幕阅读器)用于标识当前正在操作的界面元素,帮助残障用户感知界面内容 |
| 用户群体 | 所有用户。 | 主要服务于残障用户(如视力障碍)。 |
| 触发方式 | 用户点击、键盘导航、代码设置。 | 无障碍服务手势(如 TalkBack 的滑动导航)。 |
| 可见性 | 可能伴随光标或高亮效果。 | 通常通过无障碍服务的高亮框或语音反馈体现。 |
2. Input Focus
- 由系统或开发者直接控制,通过 View.requestFocus() 设置。
setFocusable: view是否可以收到焦点。
setFocusableInTouchMode(...) 这个view在touch mode是否可以收到焦点。 - 通过 View.isFocused() 判断是否获得焦点。
- 直接影响用户输入事件(如键盘输入、方向键导航)。
Input focus会影响Key event的派发,可以分为两个层级来理解:
- Window 层级,Input 模块在派发key event时会找到focused window
- View 层级,ViewRootImpl接收到Input 模块派发给的key event后,在View hierarchy中找到focused view,将event派发给它
View对象作为view hierarchy中的一员,持有焦点时:- event派发时可以找到它
- 为了用户容易注意到它,highlight的UI效果是有必要的
Q:Focus是怎么在不同window之间转移的?
A: DisplayContent中的findFocusedWindowIfNeeded(int topFocusedDisplayId)方法用于查找focused window,查找的具体逻辑在lamda function mFindFocusedWindow中,updateFocusedWindowLocked(...)方法会调用findFocusedWindowIfNeeded(int topFocusedDisplayId)方法,找到focused window后会更新mCurrentFocus,mCurrentFocus是一个WindowState类型的包内可见全局变量。InputMonitor的updateInputFocusRequest(...)方法会用到该变量,将window相关信息设置到input module。Input module会上报FOCUS event给ViewRootImpl通知Window focus发生了变化,继而处理view hierarchy内focus的逻辑,比如 focus清理逻辑,旧UI hightlight 效果清除和新UI highlight 绘制等。DisplayContent方法的调用触发场景很多,调用代码也有多处;ActiviytRecord,ActivityTaskManagerService,RootWindowContainer,WindowManagerService,WindowState和WindowToken都有调用。
关注InputDispatcher中的如下两个Event:
- EventEntry::Type::FOCUS
通知ViewRootImpl, window Focus发生了变化 - EventEntry::Type::TOUCH_MODE_CHANGED
通知ViewRootImpl,touch mode发生了变化
View.java中的requestFocus()方法
View.java中requestFocus()方法会触发其parent的requestChildFocus(...)方法,最终会调用到ViewRootImpl的requestChildFocus(...)方法,继而会调用performTraversals()方法,该方法会把measuge, relayout和draw都执行一遍,在这个过程中focused window会通知到input module。
view调用requestFocus申请焦点,如果成功拿到焦点会发送sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
InputDispatcher中的几个方法的调用:
- setFocusedWindow(...)
该方法调用的路径有很多,把能看懂或者比较明显可能触发的操作总结如下:
- WindowManagerService中的
relayoutWindow(...)方法可能会触发 - WindowManagerService中的
addWindow()方法可能会触发
SurfaceComposerClient android_view_surfaceControl SurfaceControl#Transaction InputMonitor#UpdateInputForAllWindowsConsumer InputMonitor DisplayContent RootWindowContainer WindowManagerService X SurfaceComposerClient android_view_surfaceControl SurfaceControl#Transaction InputMonitor#UpdateInputForAllWindowsConsumer InputMonitor DisplayContent RootWindowContainer WindowManagerService X ActivityRecord,ActivityTaskManagerService, WindowState和 RootWindowContainer等都可能会调用该方法 mCurrentFocus = newFocus 该方法调用的地方很多 :Transaction: mInputWindowCommands.focusRequests.push_back(request) opt [updateInputWindows] updateFocusedWindowLocked(int mode, boolean updateInputWindows)) updateFocusedWindowLocked(int mode, boolean updateInputWindows)) updateFocusedWindowLocked(int mode, boolean updateInputWindows)) setInputFocusLw(int mode, boolean updateInputWindows)) updateInputWindowsLw(false) scheduleUpdateInputWindows() updateInputWindows(...) updateInputFocusRequest(...) requestFocus(...) setFocusedWindow(...) nativeSetFocusedWindow(...) :Transaction:setFocusedWindow(...)
ViewRootImpl WindowInputEventReceiver InputChannel InputPublisher InputDispatcher InputManager SurfaceFlinger Scheduler HandlerMessageQueue ViewRootImpl WindowInputEventReceiver InputChannel InputPublisher InputDispatcher InputManager SurfaceFlinger Scheduler HandlerMessageQueue EventEntry::Type::FOCUS 中间省略了几个方法 InputTransport.cpp InputMessage::Type::FOCUS ViewRootImpl的内部类 onFrameSignal(...) commit(...) updateInputFlinger(...) setFocusedWindow(...) setFocusedWindow(...) enqueueFocusEventLocked(...) dispatchFocusLocked(...) dispatchEventLocked(...) startDispatchCycleLocked(...) publishFocusEvent(...) sendMessage(...) onFocusEvent(...) windowFocusChanged(bool hasFocus)
-
setInputWindowsLocked(...)
根据该方法的注释,该方法会从InputManagerService中被调用,但是从source code 中并未从InputManagerService中发现相关code。在InputDispatcher内部有调用。
-
onWindowInfosChanged(...)
InputDispatcher DispatcherWindowListenerInputDispatcher WindowInfosListenerInvoker SurfaceFlinger Scheduler HandlerMessageQueue InputDispatcher DispatcherWindowListenerInputDispatcher WindowInfosListenerInvoker SurfaceFlinger Scheduler HandlerMessageQueue 继承了android::impl::MessageQueue SurfaceFlinger的initScheduler方法构造了Scheduler 对象 onFrameSignal(...) commit(...) updateInputFlinger(...) windowInfosChanged(gui::WindowInfosUpdate update, reportedListeners, forceImmediateCall) onWindowInfosChanged(const gui::WindowInfosUpdate& update) onWindowInfosChanged(update) setInputWindowsLocked(windowInfoHandles, displayId)
- setFocusedApplication
Java层调用DisplayContent.java中的setFocusedApp(...)方法可以更新Input 模块中的mFocucusedApplicationHandlesByDisplay中的值,在派发Key event时该变量会被用到,处理ANR相关逻辑。
InputDispatcher InputManagerService InputMonitor DisplayContent X InputDispatcher InputManagerService InputMonitor DisplayContent X ActivityRecord.java, ActivityTaskManagerService.java, Task.java, TaskFragment.java和WindowOrganizerController.java都会调用该方法 update mFocusedApplicationHandlesByDisplay setFocusedApp(ActivityRecord newFocus) setFocusedAppLw(newFocus) setFocusedApplication(displayId, applicationHandle) setFocusedApplication(displayId, applicationHandle) setFocusedApplicationLocked(displayId, applicationHandle)
打开input cycle的log
在'frameworks/native/services/inputfliger/dispatcher/DebugConfig.h'中介绍了打开不同input log的方式,下面是一个例子:
adb shell setprop persist.log.tag.InputDispatcherDispatchCycle D
不过有次尝试不成功,没去查证为什么,deepseek 给了下面的命令,倒是起作用了:
adb shell setprop persist.log.tag.InputDispatcher DEBUG
adb shell setprop persist.log.tag.InputDispatcher VERBOSE
打开input trace
# 启用 INPUT 标签(0x80)
adb shell setprop debug.atrace.tags.enableflags 0x80
# 启动跟踪(需要 root 或 userdebug 设备)
adb shell cmd stats print-logs
# 开始记录跟踪数据(保存到 /data/local/tmp/input_trace)
adb shell atrace --async_start -b 4096 input
# 停止跟踪
adb shell atrace --async_stop > /data/local/tmp/input_trace
# 导出到本地
adb pull /data/local/tmp/input_trace .
# 使用 Perfetto 打开跟踪文件
https://ui.perfetto.dev/
InputDispatcher在派发motion 时,依照前后顺序-from front to back 确认window是否可以接受该event:
- window没有FLAG_NOT_TOUCHABLE flag
FLAG_NOT_TOUCH_MODAL :允许将其window范围外的pointer event(比如,touch screen)派发给其后面的window。
FLAG_NOT_FOCUSABLE :表明window不take input focus,用户发的key或者button event会给后面可以take input focus的window。这个flag会enable FLAG_NOT_TOUCH_MODAL
FLAG_NOT_TOUCHABLE: 表明window不能接收touch events,以便将touch events留给它后面(z order)的window。 - window的touchable region包含当前motion event的坐标
3.Accessibility Focus
-
由无障碍服务(如 TalkBack)独立管理。
-
通过 AccessibilityNodeInfo 的isAccessibilityFocused() 判断。
-
开发者可通过AccessibilityNodeInfo来让相应的View申请Accessibility focus
accessibilityNodeInfo.performAction(ACTION_ACCESSIBILITY_FOCUS);
View.java内在处理该action时会调用requestAccessibilityFocus()方法来请求Accessibility focus。
frameworks/native/libs/gui/include/gui/WindowInfo.h