8.1 焦点调试工具集
8.1.1 ADB调试命令
# 查看当前焦点窗口和视图
adb shell dumpsys window windows | grep -E "mCurrentFocus|mFocusedApp"
# 检查特定Activity的焦点树
adb shell dumpsys activity top | grep -A 50 "Focus"
# 查看视图焦点属性
adb shell dumpsys activity com.example.app | grep -A 10 "View Hierarchy"
# 启用焦点查找器调试
adb shell setprop log.tag.FocusFinder DEBUG
adb logcat -s FocusFinder
# 发送按键事件测试焦点移动
adb shell input keyevent KEYCODE_DPAD_RIGHT
adb shell input keyevent KEYCODE_DPAD_DOWN
8.1.2 Android Studio布局检查器
-
连接设备运行应用
-
选择 View > Tool Windows > Layout Inspector
-
分析布局树:
- 焦点视图标记为 F
focusable和focusableInTouchMode属性- 当前焦点状态
8.1.3 自定义焦点调试视图
public class FocusDebugView extends View {
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制所有可聚焦视图的边界
drawFocusableBounds(canvas);
// 标记当前焦点视图
drawCurrentFocus(canvas);
}
private void drawFocusableBounds(Canvas canvas) {
List<View> focusables = findAllFocusableViews();
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
for (View view : focusables) {
Rect rect = new Rect();
view.getGlobalVisibleRect(rect);
canvas.drawRect(rect, paint);
// 添加视图ID标签
canvas.drawText(getResources().getResourceName(view.getId()),
rect.left, rect.top - 10, paint);
}
}
private void drawCurrentFocus(Canvas canvas) {
View focused = getRootView().findFocus();
if (focused != null) {
Rect rect = new Rect();
focused.getGlobalVisibleRect(rect);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(4);
canvas.drawRect(rect, paint);
canvas.drawText("CURRENT FOCUS", rect.centerX(), rect.centerY(), paint);
}
}
}
8.2 常见焦点问题及解决方案
8.2.1 焦点无法获取
问题表现:视图无法获取焦点
诊断步骤:
- 检查
android:focusable="true" - 确认
android:focusableInTouchMode="true"(触摸模式下) - 检查父容器的
descendantFocusability设置 - 验证视图可见性(
VISIBLE)
解决方案:
// 确保视图可获取焦点
view.setFocusable(true);
view.setFocusableInTouchMode(true);
// 检查父容器设置
if (view.getParent() instanceof ViewGroup) {
((ViewGroup) view.getParent()).setDescendantFocusability(
ViewGroup.FOCUS_AFTER_DESCENDANTS);
}
8.2.2 焦点顺序错乱
问题表现:焦点未按预期顺序移动
诊断步骤:
- 检查
nextFocusDown/Up/Left/Right属性 - 验证布局文件中视图顺序
- 排查动态修改焦点顺序的代码
解决方案:
<!-- 明确指定焦点顺序 -->
<Button
android:id="@+id/btn1"
android:nextFocusRight="@+id/btn2"/>
<Button
android:id="@+id/btn2"
android:nextFocusLeft="@+id/btn1"/>
8.2.3 焦点丢失问题
问题表现:焦点在操作后意外丢失
诊断步骤:
- 检查视图是否在焦点变更时被移除
- 验证触摸模式切换是否导致焦点清除
- 排查
clearFocus()调用位置
解决方案:
// 在视图移除前转移焦点
view.setOnRemoveListener(() -> {
View nextFocus = findNextFocus();
if (nextFocus != null) {
nextFocus.requestFocus();
}
});
// 防止触摸模式切换导致焦点丢失
view.setFocusableInTouchMode(true);
8.2.4 滚动容器中的焦点问题
问题表现:滚动后焦点视图不可见
诊断步骤:
- 验证容器是否实现
isScrollContainer() - 检查
requestChildFocus()实现 - 确认布局是否已完成
解决方案:
// 自定义ScrollView确保滚动到焦点视图
@Override
public void requestChildFocus(View child, View focused) {
if (!mIsLayoutDirty) {
scrollToChild(focused);
} else {
mChildToScrollTo = focused;
}
super.requestChildFocus(child, focused);
}
8.3 高级诊断技术
8.3.1 焦点事件追踪
// 全局焦点监听器
View rootView = getWindow().getDecorView();
rootView.addOnLayoutChangeListener((v, left, top, right, bottom,
oldLeft, oldTop, oldRight, oldBottom) -> {
logFocusTree(rootView, 0);
});
private void logFocusTree(View view, int depth) {
StringBuilder indent = new StringBuilder();
for (int i = 0; i < depth; i++) indent.append(" ");
Log.d("FocusTree", indent + view.toString() +
" focusable=" + view.isFocusable() +
" focused=" + view.isFocused());
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
for (int i = 0; i < group.getChildCount(); i++) {
logFocusTree(group.getChildAt(i), depth + 1);
}
}
}
8.3.2 焦点性能分析
// 测量焦点查找耗时
long startTime = System.nanoTime();
View nextFocus = FocusFinder.getInstance().findNextFocus(
parent, currentFocus, direction);
long duration = System.nanoTime() - startTime;
Log.d("FocusPerf", "Focus search took " + duration + " ns");
// 使用Trace API记录焦点操作
Trace.beginSection("focusSearch");
try {
view.focusSearch(direction);
} finally {
Trace.endSection();
}
8.4 焦点问题诊断流程图
8.5 焦点调试最佳实践
8.5.1 系统化调试流程
-
复现问题:确定稳定复现步骤
-
隔离问题:创建最小复现代码
-
数据收集:
- 焦点树日志
- 布局层次快照
- 系统属性状态
-
分析定位:使用调试工具分析
-
验证修复:单元测试和UI测试
8.5.2 预防性编程
// 焦点安全编程实践
public class FocusSafeView extends View {
@Override
public void clearFocus() {
// 确保焦点转移后才清除
if (isFocused()) {
View next = findNextFocus();
if (next != null && next.requestFocus()) {
super.clearFocus();
}
}
}
@Override
protected void onDetachedFromWindow() {
// 视图移除前转移焦点
if (isFocused()) {
View next = findNextFocus();
if (next != null) {
next.requestFocus();
}
}
super.onDetachedFromWindow();
}
}
8.5.3 焦点单元测试
@RunWith(AndroidJUnit4.class)
public class FocusTest {
@Rule
public ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
@Test
public void testInitialFocus() {
// 验证初始焦点是否正确
onView(withId(R.id.first_button)).check(matches(isFocused()));
}
@Test
public void testFocusNavigation() {
// 测试焦点导航顺序
pressKey(KeyEvent.KEYCODE_DPAD_RIGHT);
onView(withId(R.id.second_button)).check(matches(isFocused()));
pressKey(KeyEvent.KEYCODE_DPAD_DOWN);
onView(withId(R.id.fifth_button)).check(matches(isFocused()));
}
@Test
public void testFocusAfterOrientationChange() {
// 旋转后焦点恢复
onView(withId(R.id.second_button)).perform(click());
// 旋转设备
rotateScreen();
// 验证焦点恢复
onView(withId(R.id.second_button)).check(matches(isFocused()));
}
private void rotateScreen() {
Context context = InstrumentationRegistry.getTargetContext();
int orientation = context.getResources().getConfiguration().orientation;
rule.getActivity().setRequestedOrientation(
orientation == Configuration.ORIENTATION_PORTRAIT ?
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE :
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
);
}
}
8.6 复杂场景调试案例
案例1:RecyclerView焦点跳跃
问题描述:在RecyclerView中,焦点在滚动后跳转到错误位置
诊断步骤:
- 检查LayoutManager的焦点处理逻辑
- 验证ViewHolder的焦点属性
- 分析滚动时的焦点保存/恢复机制
解决方案:
// 自定义LayoutManager
public class StableFocusLayoutManager extends LinearLayoutManager {
@Override
public View onFocusSearchFailed(View focused, int focusDirection,
RecyclerView.Recycler recycler,
RecyclerView.State state) {
// 焦点查找失败时处理
return handleFocusSearchFailure(focused, focusDirection);
}
@Override
public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
// 记录最后聚焦位置
int position = getPosition(child);
saveLastFocusedPosition(position);
return super.onRequestChildFocus(parent, child, focused);
}
}
案例2:对话框焦点泄露
问题描述:对话框关闭后,焦点意外转移到背景视图
诊断步骤:
- 跟踪对话框关闭时的焦点转移
- 检查WindowManager的焦点管理
- 分析焦点恢复逻辑
解决方案:
// 安全的对话框显示/关闭
public void showDialog() {
// 保存当前焦点
mPreviousFocus = getCurrentFocus();
// 显示对话框
dialog.show();
// 设置对话框关闭监听
dialog.setOnDismissListener(dialog -> {
if (mPreviousFocus != null) {
mPreviousFocus.post(() -> mPreviousFocus.requestFocus());
}
});
}
本章小结
-
调试工具:
- ADB命令:
dumpsys window,dumpsys activity - Layout Inspector焦点分析
- 自定义焦点调试视图
- ADB命令:
-
常见问题解决方案:
- 焦点无法获取:检查属性、父容器设置
- 焦点顺序错乱:明确指定
nextFocusXXX - 焦点丢失:生命周期管理,避免意外清除
- 滚动容器问题:正确实现焦点滚动逻辑
-
高级诊断技术:
- 焦点树日志记录
- 焦点操作性能分析
- 系统化调试流程
-
最佳实践:
- 预防性编程处理焦点
- 焦点单元测试覆盖
- 复杂组件自定义焦点逻辑
-
调试案例:
- RecyclerView焦点稳定性
- 对话框焦点管理
- 屏幕旋转焦点恢复
关键诊断原则:
- 从简单到复杂逐步排查
- 区分系统问题和应用问题
- 使用工具验证假设
- 添加日志辅助诊断
通过本章的学习,您应该能够:
- 使用各种工具诊断焦点问题
- 解决常见的焦点相关缺陷
- 设计健壮的焦点处理逻辑
- 编写焦点相关的单元测试
- 分析并修复复杂的焦点场景
在接下来的章节中,我们将探讨Android焦点系统的底层实现原理,包括输入系统与焦点的交互、窗口焦点管理等高级主题