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
相关产品推荐
相关产品推荐

