Reputation: 11
I'm experimenting accessibility with TalkBack and Jetpack Compose on a small feature that I designed with a step-by-step process.
I use the composeView that contains my custom Router to drive the navigation on each screen. To simplify this example there is 3 screens with the same content.
To describe the process progression I use a custom BottomBarNavigation that displays a progressBar, a step title and next/previous custom Link to navigate. All is working very well.
But when I use TalkBack to vocalize the feature, i'm facing a basic problem to reset the focus after clicking next or previous. The screen is recompose with the next/previous one, but the focus stay on the selected link.
I would like to reset it to the first element of the screen.
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",
)
}
I've tried to use the FocusRequester, like you can see in my code but has no effect. If someone have an idea, i'm ready to try everything. My design is probably not the more suitable.
Upvotes: 1
Views: 2110
Reputation: 391
If you intended to change talkback request focus on double tap of link the requestFocus should be called from the next button onclick{} not from the LaunchedEffect
Here is a simple example of request focus
val (one, two, three) = remember { FocusRequester.createRefs() }
LazyRow(){
item { Button(onClick = {three.requestFocus()}, modifier = Modifier.focusRequester(one).focusable()) { Text("1") } }
item { Button(onClick = {}, modifier = Modifier.focusRequester(two)) { Text("2") } }
item { Button(onClick = {one.requestFocus()}, modifier = Modifier
.focusRequester(three)
.focusable()) { Text("3") } }
item { Button(onClick = {}) { Text("4") } }
}
And a point to note requestFocus should always call from Modifier clickable event, not as part of composition.
Upvotes: 0