You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Jetpack Compose中TalkBack焦点重置问题求助

问题:Jetpack Compose 导航后 TalkBack 焦点未重置到页面首个元素

我正在为一个分步流程测试TalkBack与Jetpack Compose的无障碍适配,使用带自定义Router的ComposeView驱动页面导航,设置了3个内容相同的页面,通过自定义底部导航栏展示进度条、步骤标题和Next/Previous按钮,功能运行正常。但点击导航按钮后,页面切换完成,焦点仍停留在被点击的按钮上,希望能将焦点重置到页面的首个元素。已尝试使用FocusRequester但无效,求解决方案或设计优化建议。

class MyFragment : Fragment() {
    private lateinit var binding: FragmentTestBinding
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = FragmentTestBinding.inflate(inflater, container, false)

        binding.composeView.setContent {
            AppTheme(darkTheme = true) {
                val navController = rememberNavController()
                Scaffold {
                    Surface(
                        modifier = Modifier
                            .padding(it)
                            .fillMaxSize(),
                        color = MyColors.screenBackground
                    ) {
                        MyRouter(navController = navController)
                    }
                }
            }
        }
        return binding.root
    }
}

@Composable
fun MyRouter(
    navController: NavHostController
) {

    var currentStep: Screens by remember {
        mutableStateOf(Screens.FIRST)
    }

    Column {
        NavHost(
            modifier = Modifier
                .weight(1f),
            navController = navController,
            startDestination = Screens.FIRST.route,
            enterTransition = {
                fadeIn(animationSpec = tween(700))
            },
            exitTransition = { ExitTransition.None },
        ) {
            composable(Screens.FIRST.route) {
                currentStep = Screens.FIRST
                MyScreen()
            }
            composable(Screens.SECOND.route) {
                currentStep = Screens.SECOND
                MyScreen()
            }
            composable(Screens.THIRD.route) {
                currentStep = Screens.THIRD
                MyScreen()
            }
        }
        if (currentStep.progress > 0f) {
            Box(modifier = Modifier.wrapContentHeight()) {
                MyBottomNavigationBar(
                    stepTitle = currentStep.stepTitle,
                    progress = currentStep.progress,
                    nextAction = currentStep.nextRoute?.let {
                        {
                            navController.navigate(it)
                        }
                    },
                    previousAction = currentStep.previousRoute?.let {
                        {
                            navController.navigate(it)
                        }
                    }
                )
            }
        }
    }
}

@Composable
fun MyScreen() {
    val scrollState = rememberScrollState()
    val focusRequester = remember { FocusRequester() }

    LaunchedEffect(key1 = true) {
        focusRequester.requestFocus()
    }

    Column(
        modifier = Modifier
            .padding(MyDimens.padding15)
            .verticalScroll(scrollState)
            .semantics { contentDescription = "Screen description" }
            .focusRequester(focusRequester)
            .focusable(true),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            modifier = Modifier.fillMaxWidth(0.5f),
            painter = painterResource(id = AppDrawable.imgRepeteur6Couv),
            contentDescription = "Image description",
            contentScale = ContentScale.FillWidth
        )
        Spacer(modifier = Modifier.height(MyDimens.padding30))
        MyMessageView(
            modifier = Modifier
                .semantics { contentDescription = "More information about procedure" },
            state = State.INFO,
            title = "More information",
            customIcon = AppDrawable.iconRealTime
        )
    }
}

@Composable
fun MyBottomNavigationBar(
    modifier: Modifier = Modifier,
    stepTitle: String? = null,
    progress: Float = 0f,
    nextAction: (() -> Unit)? = null,
    previousAction: (() -> Unit)? = null
) {
    Column(modifier = modifier) {
        LinearProgressIndicator(
            progress = { progress
            },
            modifier = Modifier
                .fillMaxWidth()
                .semantics {
                    contentDescription = "Progression at $progress percent"
                },
            color = MyColors.primaryColor,
            trackColor = MyColors.gray4,
        )
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = MyDimens.padding10, horizontal = MyDimens.padding10)
        ) {
            previousAction?.let {
                Row(modifier = Modifier.align(Alignment.CenterStart)) {
                    MyTextLink(
                        modifier = Modifier
                            .widthIn(max = 120.dp)
                            .semantics { contentDescription = "Back to previous step" },
                        text = "Previous",
                        chevronClose = true,
                        chevronDirection = ChevronDirection.LEFT,
                        onClick = it,
                    )
                }
            }
            stepTitle?.let {
                MyBigLabel(
                    modifier = Modifier
                        .align(Alignment.Center)
                        .widthIn(max = 120.dp)
                        .semantics { contentDescription = "Step title, $it" },
                    text = it
                )
            }
            nextAction?.let {
                Row(modifier = Modifier.align(Alignment.CenterEnd)) {
                    MyTextLink(
                        modifier = Modifier
                            .widthIn(max = 120.dp)
                            .semantics { contentDescription = "Follow next step" }
                            .clickable { it.invoke() },
                        text = "Next",
                        chevronClose = true,
                        chevronDirection = ChevronDirection.RIGHT,
                        onClick = it
                    )
                }
            }
        }
    }
}

enum class Screens(
    val route: String,
    val stepTitle: String? = null,
    val progress: Float = 0f,
    val nextRoute: String? = null,
    val previousRoute: String? = null,
) {
    FIRST(
        route = "first",
        stepTitle = "1/3",
        progress = 0.3f,
        nextRoute = "second",
    ),
    SECOND(
        route = "second",
        stepTitle = "2/3",
        progress = 0.6f,
        nextRoute = "third",
        previousRoute = "first",
    ),
    THIRD(
        route = "third",
        stepTitle = "3/3",
        progress = 1.0f,
        previousRoute = "second",
    )
}
解决方案

1. 修正FocusRequester的触发时机

原代码中LaunchedEffect(key1 = true)仅在首次组合时执行,页面切换时不会重新触发。需要将当前页面路由作为key,确保每次导航切换都重新请求焦点:

@Composable
fun MyScreen(currentRoute: String) {
    val scrollState = rememberScrollState()
    val focusRequester = remember { FocusRequester() }

    // 用当前路由作为触发key,页面切换时重新执行
    LaunchedEffect(key1 = currentRoute) {
        focusRequester.requestFocus()
    }

    Column(
        modifier = Modifier
            .padding(MyDimens.padding15)
            .verticalScroll(scrollState)
            .semantics { contentDescription = "Screen description" },
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            modifier = Modifier
                .fillMaxWidth(0.5f)
                .focusRequester(focusRequester)
                .focusable(true), // 让Image成为可聚焦元素
            painter = painterResource(id = AppDrawable.imgRepeteur6Couv),
            contentDescription = "Image description",
            contentScale = ContentScale.FillWidth
        )
        // 其余代码不变
    }
}

然后在MyRouter的NavHost中传递路由参数:

composable(Screens.FIRST.route) {
    currentStep = Screens.FIRST
    MyScreen(currentRoute = Screens.FIRST.route)
}
composable(Screens.SECOND.route) {
    currentStep = Screens.SECOND
    MyScreen(currentRoute = Screens.SECOND.route)
}
composable(Screens.THIRD.route) {
    currentStep = Screens.THIRD
    MyScreen(currentRoute = Screens.THIRD.route)
}

2. 确保焦点指向页面首个可聚焦元素

默认情况下Image不可聚焦,需要添加.focusable(true)并将FocusRequester绑定到该元素,而不是外层的Column,这样焦点会直接落在页面的首个视觉元素上,符合TalkBack用户的预期。

3. 优化状态同步逻辑

原代码中在composable内部直接修改currentStep的方式容易导致状态不一致,建议通过监听NavController的栈顶条目来自动同步当前步骤:

@Composable
fun MyRouter(
    navController: NavHostController
) {
    // 通过NavController自动同步当前步骤
    val backStackEntry by navController.currentBackStackEntryAsState()
    val currentStep = remember(backStackEntry) {
        backStackEntry?.destination?.route?.let { route ->
            Screens.values().find { it.route == route } ?: Screens.FIRST
        } ?: Screens.FIRST
    }

    Column {
        NavHost(
            modifier = Modifier.weight(1f),
            navController = navController,
            startDestination = Screens.FIRST.route,
            enterTransition = { fadeIn(animationSpec = tween(700)) },
            exitTransition = { ExitTransition.None },
        ) {
            composable(Screens.FIRST.route) {
                MyScreen(currentRoute = Screens.FIRST.route)
            }
            composable(Screens.SECOND.route) {
                MyScreen(currentRoute = Screens.SECOND.route)
            }
            composable(Screens.THIRD.route) {
                MyScreen(currentRoute = Screens.THIRD.route)
            }
        }
        // 底部导航栏代码不变
    }
}

4. 可选:延迟焦点请求

如果页面切换动画导致焦点请求失效,可以在LaunchedEffect中添加短暂延迟:

LaunchedEffect(key1 = currentRoute) {
    delay(100) // 等待页面切换动画完成
    focusRequester.requestFocus()
}

内容的提问来源于stack exchange,提问作者Nico

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 04:45:53