要深入理解为何设置 FLAG_NOT_FOCUSABLE
会导致 onBackPressed()
不被回调,但View点击事件却不受影响,需要从 Android 系统的源码层面分析事件分发的核心逻辑。以下是基于 Android 12 源码的详细分析:
1. 窗口焦点与事件分发的核心逻辑
当调用 getWindow().setFlags(FLAG_NOT_FOCUSABLE, ...)
时,窗口会被标记为不可聚焦。这一标记直接影响了窗口在 WindowManagerService
(WMS) 中的事件分发策略。
源码分析:
-
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
的定义:java// frameworks/base/core/java/android/view/WindowManager.java public static final int FLAG_NOT_FOCUSABLE = 0x00000008;
-
WMS 对焦点窗口的判断 :
在
WindowManagerService
中,处理按键事件时会优先检查目标窗口是否可聚焦:java
ruby// frameworks/base/services/core/java/com/android/server/wm/InputConsumerImpl.java private boolean canReceiveKeys(WindowState win) { return win.canReceiveKeys() && !win.isHiddenOrObscuredBehindDialog(); } // frameworks/base/services/core/java/com/android/server/wm/WindowState.java boolean canReceiveKeys() { return !mAttrs.flags.contains(FLAG_NOT_FOCUSABLE) && !mAttrs.flags.contains(FLAG_NOT_TOUCHABLE); }
关键点 :若窗口被标记为
FLAG_NOT_FOCUSABLE
,canReceiveKeys()
返回false
,按键事件将不会分发给该窗口。
2. 返回键事件的分发路径
onBackPressed()
是在 Activity 接收到返回键事件后被调用的。当窗口不可聚焦时,事件流程如下:
源码分析:
-
InputManagerService (IMS) 接收物理按键:
java
ruby// frameworks/base/services/core/java/com/android/server/input/InputManagerService.java // IMS 接收到返回键事件后,通过 WindowManagerPolicy 决定分发目标
-
WMS 过滤不可聚焦窗口:
java
csharp// frameworks/base/services/core/java/com/android/server/wm/InputConsumerImpl.java @Override public void dispatchKey(InputEvent event, int displayId, ...) { // 查找可接收按键的窗口 WindowState win = mWmService.findKeyEventTargetLocked(...); if (win == null || !canReceiveKeys(win)) { // 若窗口不可聚焦,事件被系统拦截,不会传递给应用 interceptKeyBeforeDispatching(win, event, ...); return; } // 否则分发给目标窗口 dispatchKeyToWindow(win, event, ...); }
-
Activity 接收按键事件的条件:
java
csharp// frameworks/base/core/java/android/app/Activity.java public boolean dispatchKeyEvent(KeyEvent event) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.ECLAIR) { // 只有事件到达 Activity 才会调用 onBackPressed() event.startTracking(); return true; } else { // 旧版本处理逻辑 if (event.getAction() == KeyEvent.ACTION_UP) { onBackPressed(); return true; } return super.dispatchKeyEvent(event); } } return super.dispatchKeyEvent(event); }
结论 :由于
FLAG_NOT_FOCUSABLE
导致窗口无法接收按键事件,dispatchKeyEvent()
不会被调用,因此onBackPressed()
也不会被触发。
3. 触摸事件的分发机制
触摸事件(如按钮点击)的分发路径与按键事件不同,它不依赖窗口焦点。
源码分析:
-
WMS 对触摸事件的处理:
java
csharp// frameworks/base/services/core/java/com/android/server/wm/InputConsumerImpl.java @Override public void dispatchMotion(InputEvent event, int displayId, ...) { // 查找触摸目标窗口(基于坐标和可见性) WindowState win = mWmService.findTouchTargetWindowLocked(...); if (win != null) { // 即使窗口不可聚焦,只要可见且可触摸,仍会分发触摸事件 dispatchMotionToWindow(win, event, ...); } }
-
触摸事件在 View 层级的分发:
java
scss// frameworks/base/core/java/android/view/ViewGroup.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { // ViewGroup 遍历子 View 寻找触摸目标 if (!onFilterTouchEventForSecurity(ev)) { return false; } // 即使窗口不可聚焦,只要 View 可见且可点击,仍会处理触摸事件 final int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN) { // 重置触摸状态 cancelAndClearTouchTargets(ev); resetTouchState(); } // 后续逻辑处理触摸事件分发 }
-
按钮点击事件的触发:
java
java// frameworks/base/core/java/android/widget/Button.java // Button 继承自 TextView,点击事件由 View 类处理 @Override public boolean onTouchEvent(MotionEvent event) { final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE) || ((viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || ((viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { // 处理触摸事件(DOWN、MOVE、UP) // 当检测到点击操作时,触发 onClickListener } return super.onTouchEvent(event); }
结论 :触摸事件的分发只依赖窗口的可见性和
TOUCHABLE
标记,而非焦点状态。因此,即使窗口不可聚焦,按钮仍能正常响应点击事件。
4. 验证实验
通过以下代码可以验证上述分析:
java
typescript
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 禁用窗口焦点
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
);
// 按钮点击事件(正常触发)
findViewById(R.id.myButton).setOnClickListener(v -> {
Log.d("MainActivity", "按钮点击事件触发");
});
// 重写 dispatchKeyEvent(不会被调用)
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
Log.d("MainActivity", "dispatchKeyEvent 被调用");
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
Log.d("MainActivity", "返回键事件到达 Activity");
return true;
}
return super.dispatchKeyEvent(event);
}
}
@Override
public void onBackPressed() {
// 设置 FLAG_NOT_FOCUSABLE 后,此方法不会被调用
Log.d("MainActivity", "onBackPressed 未被调用");
super.onBackPressed();
}
总结
事件类型 | 依赖窗口焦点 | FLAG_NOT_FOCUSABLE 的影响 | 关键源码位置 |
---|---|---|---|
按键事件 | ✅ | 事件被 WMS 拦截,无法到达 Activity | WindowState.canReceiveKeys() |
触摸事件 | ❌ | 事件正常分发到 View 层级 | InputConsumerImpl.dispatchMotion() |
核心区别 :按键事件需要窗口可聚焦才能接收,而触摸事件只需窗口可见且可触摸。因此,设置 FLAG_NOT_FOCUSABLE
后,onBackPressed()
不会被回调,但按钮点击事件仍能正常触发。