第六章:自定义焦点控制技巧

68 阅读4分钟

6.1 重写焦点查找行为

自定义焦点查找逻辑

@Override
public View focusSearch(View focused, int direction) {
    // 特殊处理右方向键
    if (direction == View.FOCUS_RIGHT) {
        // 跳转到紧急目标视图
        View emergencyTarget = findViewById(R.id.emergency_target);
        if (emergencyTarget != null && emergencyTarget.isFocusable()) {
            return emergencyTarget;
        }
    }
    
    // 处理环形导航
    if (mCircularNavigationEnabled) {
        return handleCircularNavigation(focused, direction);
    }
    
    return super.focusSearch(focused, direction);
}

private View handleCircularNavigation(View focused, int direction) {
    int currentIndex = indexOfChild(focused);
    int childCount = getChildCount();
    
    switch (direction) {
        case FOCUS_RIGHT:
            return getChildAt((currentIndex + 1) % childCount);
        case FOCUS_LEFT:
            return getChildAt((currentIndex - 1 + childCount) % childCount);
        case FOCUS_DOWN:
            return getChildAt((currentIndex + COLUMNS) % childCount);
        case FOCUS_UP:
            return getChildAt((currentIndex - COLUMNS + childCount) % childCount);
    }
    return null;
}

6.2 动态焦点链构建

运行时焦点链管理

// 创建动态焦点链
public void buildDynamicFocusChain() {
    View v1 = findViewById(R.id.view1);
    View v2 = findViewById(R.id.view2);
    View v3 = findViewById(R.id.view3);
    
    // 设置焦点顺序
    setNextFocus(v1, FOCUS_RIGHT, v2);
    setNextFocus(v2, FOCUS_RIGHT, v3);
    setNextFocus(v3, FOCUS_RIGHT, v1); // 形成环
    
    // 设置回退链
    setNextFocus(v2, FOCUS_LEFT, v1);
    setNextFocus(v3, FOCUS_LEFT, v2);
}

// 辅助方法设置焦点关系
private void setNextFocus(View view, int direction, View target) {
    switch (direction) {
        case FOCUS_RIGHT:
            view.setNextFocusRightId(target.getId());
            break;
        case FOCUS_LEFT:
            view.setNextFocusLeftId(target.getId());
            break;
        // 其他方向类似
    }
}

6.3 焦点劫持与重定向

焦点事件拦截

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) {
        // 拦截确定键事件
        handleSpecialAction();
        return true; // 事件已消费
    }
    return super.dispatchKeyEvent(event);
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
        // 强制焦点跳转到特定视图
        View target = findViewById(R.id.special_target);
        if (target != null && target.requestFocus()) {
            return true;
        }
    }
    return super.onKeyDown(keyCode, event);
}

6.4 焦点状态同步技术

跨视图焦点状态同步

// 创建焦点组
public class FocusGroup {
    private final List<View> views = new ArrayList<>();
    private View currentFocused;
    
    public void addView(View view) {
        views.add(view);
        view.setOnFocusChangeListener((v, hasFocus) -> {
            if (hasFocus) {
                currentFocused = v;
                onFocusChanged(v);
            }
        });
    }
    
    private void onFocusChanged(View focused) {
        // 更新组内其他视图状态
        for (View view : views) {
            if (view != focused) {
                view.setSelected(view == focused);
            }
        }
    }
    
    public void requestFocus() {
        if (currentFocused != null) {
            currentFocused.requestFocus();
        } else if (!views.isEmpty()) {
            views.get(0).requestFocus();
        }
    }
}

6.5 复杂布局焦点优化

网格布局焦点控制

// 实现网格导航逻辑
@Override
public View focusSearch(View focused, int direction) {
    int position = getPosition(focused);
    if (position == -1) return super.focusSearch(focused, direction);
    
    int row = position / COLUMNS;
    int col = position % COLUMNS;
    
    switch (direction) {
        case FOCUS_RIGHT:
            if (col < COLUMNS - 1) return getViewAt(row, col + 1);
            break;
        case FOCUS_LEFT:
            if (col > 0) return getViewAt(row, col - 1);
            break;
        case FOCUS_DOWN:
            if (row < ROWS - 1) return getViewAt(row + 1, col);
            break;
        case FOCUS_UP:
            if (row > 0) return getViewAt(row - 1, col);
            break;
    }
    
    // 边界处理:跳转到其他区域或返回null
    return handleEdgeCases(row, col, direction);
}

6.6 焦点冲突解决方案

常见冲突场景及解决方案

场景1:多个视图同时请求焦点

// 使用优先级系统
public class FocusManager {
    private static final Map<View, Integer> priorityMap = new HashMap<>();
    
    public static void requestFocusWithPriority(View view, int priority) {
        View currentFocus = view.findFocus();
        if (currentFocus == null || 
            priority > priorityMap.getOrDefault(currentFocus, 0)) {
            view.requestFocus();
        }
    }
}

// 使用示例
FocusManager.requestFocusWithPriority(importantView, 10);

场景2:对话框与背景视图焦点冲突

@Override
public void showDialog() {
    // 显示对话框前保存当前焦点
    mLastFocusedView = getCurrentFocus();
    
    // 显示对话框
    dialog.show();
    
    // 对话框显示后清除背景焦点
    if (mLastFocusedView != null) {
        mLastFocusedView.clearFocus();
    }
}

@Override
public void dismissDialog() {
    dialog.dismiss();
    
    // 恢复焦点
    if (mLastFocusedView != null) {
        mLastFocusedView.requestFocus();
    }
}

场景3:动态加载视图的焦点控制

// 动态添加视图后设置焦点链
public void addDynamicView(View newView) {
    container.addView(newView);
    
    // 设置新视图的焦点关系
    View lastView = findLastFocusable();
    if (lastView != null) {
        lastView.setNextFocusRightId(newView.getId());
        newView.setNextFocusLeftId(lastView.getId());
    }
    
    // 设置新视图为首个焦点目标
    if (container.getChildCount() == 1) {
        newView.setNextFocusLeftId(newView.getId());
        newView.setNextFocusRightId(newView.getId());
    }
}

6.7 调试与测试工具

焦点可视化工具

// 在View上绘制焦点状态
@Override
protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        if (child.isFocused()) {
            // 绘制焦点框
            Paint paint = new Paint();
            paint.setColor(Color.RED);
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeWidth(4);
            
            Rect rect = new Rect();
            child.getDrawingRect(rect);
            offsetDescendantRectToMyCoords(child, rect);
            
            canvas.drawRect(rect, paint);
        }
    }
}

焦点单元测试

// 使用Espresso测试焦点
@RunWith(AndroidJUnit4.class)
public class FocusTest {
    @Rule
    public ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);

    @Test
    public void testFocusNavigation() {
        // 初始焦点检查
        onView(withId(R.id.first_button)).check(matches(isFocused()));
        
        // 模拟右方向键
        pressKey(KeyEvent.KEYCODE_DPAD_RIGHT);
        
        // 检查焦点是否移动
        onView(withId(R.id.second_button)).check(matches(isFocused()));
        
        // 模拟确定键
        pressKey(KeyEvent.KEYCODE_DPAD_CENTER);
        
        // 检查点击事件
        onView(withId(R.id.result_text)).check(matches(withText("Button clicked")));
    }
}

本章小结

  1. 自定义焦点查找

    • 重写focusSearch方法实现特殊导航逻辑
    • 实现环形导航等高级模式
  2. 动态焦点链

    • 运行时构建焦点关系
    • 使用setNextFocusXxxId动态配置
  3. 焦点劫持技术

    • 拦截按键事件实现特殊行为
    • 强制重定向焦点到特定视图
  4. 焦点状态同步

    • 创建焦点组实现状态同步
    • 跨视图焦点状态管理
  5. 复杂布局优化

    • 网格布局导航算法
    • 位置计算与边界处理
  6. 焦点冲突解决

    • 优先级系统管理焦点请求
    • 对话框与背景视图焦点隔离
    • 动态添加视图的焦点链维护
  7. 调试与测试

    • 可视化焦点状态
    • Espresso焦点导航测试

关键技巧总结

  • 优先使用系统焦点机制,必要时才自定义
  • 复杂布局中明确焦点路径
  • 动态内容需同步更新焦点关系
  • 始终考虑无障碍需求

在下一章中,我们将深入探讨TV开发中的焦点特殊处理,包括焦点放大效果、D-Pad导航优化和无障碍焦点支持