Nico
Nico

Reputation: 11

Manage focusing with Android Jetpack Compose for TalkBack

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

Answers (1)

venkatesh venkey
venkatesh venkey

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

Related Questions