导航 popBackStack 的焦点恢复和使用状态修饰符的焦点恢复

Fir*_*ate 4 android kotlin android-jetpack-compose android-jetpack-compose-tv

JetStream tv 应用程序示例中,它有一个createInitialFocusRestorerModifiers()函数充当其子级 TvLazyRow/Column 的焦点恢复器。

正如该函数的 KDoc 中所述:

返回一组修饰符 [FocusRequesterModifiers],可用于恢复焦点并指定最初聚焦的项目。

其用法:

  LazyRow(modifier.then(modifiers.parentModifier) {
   item1(modifier.then(modifiers.childModifier) {...}
   item2 {...}
   item3 {...}
   ...
  }
Run Code Online (Sandbox Code Playgroud)

NestedLazyList 可组合项 正确的行为应该如下图所示。向下按 DPad 应聚焦到第一个项目,向上按应将焦点带到最后保存的状态焦点子项。

不过,这里的问题是它不能恢复对导航的关注[ popBackStack()],所以我尝试将上述功能与这个答案集成。

当前代码:

        // var anItemHasBeenFocused: Boolean = false
        // var lastFocusedItem: Pair<Int, Int> = ...

        TvLazyRow(
            modifier = focusRestorerModifiers.parentModifier,
            pivotOffsets = PivotOffsets(parentFraction = 0F)
        ) {
            itemsIndexed(items = items) { columnIndex, item ->
                val focusRequester = remember { FocusRequester() }
                
                RowItem(
                    modifier = Modifier
                        .focusRequester(focusRequester)
                        .ifElse(
                            condition = columnIndex == 0,
                            ifTrueModifier = focusRestorerModifiers.childModifier
                        )
                        .onPlaced {
                            val shouldFocusThisItem = lastFocusedItem.first == rowIndex
                                && lastFocusedItem.second == columnIndex
                                && !anItemHasBeenFocused

                            if (shouldFocusThisItem) {
                                // The stacktrace points here.
                                focusRequester.requestFocus()
                            }
                        }
                        .onFocusChange {
                            if(it.isFocused) {
                                onFocusedChange(item, columnIndex)
                            }
                        }
                        .focusProperties {
                            if (rowIndex == 0) {
                                up = FocusRequester.Cancel
                            } else if (isLastIndex) {
                                down = FocusRequester.Cancel
                            }
                        },
                    item = item,
                    isLastIndex = columnIndex == items.lastIndex,
                    onClick = onNavigateToSecondScreen
                )

        }
Run Code Online (Sandbox Code Playgroud)

该代码仅在第一个项目上运行良好,在浏览第一个项目后,我能够从第二个屏幕返回,没有任何错误。

当我尝试滚动到列表的最后一项(使第一项从当前视图中隐藏)并尝试转到第二个屏幕并立即从中弹出时,应用程序不断抛出:

   FocusRequester is not initialized. Here are some possible fixes:
   1. Remember the FocusRequester: val focusRequester = remember { FocusRequester() }
   2. Did you forget to add a Modifier.focusRequester() ?
   3. Are you attempting to request focus during composition? Focus requests should be made in
   response to some event. Eg Modifier.clickable { focusRequester.requestFocus() }
Run Code Online (Sandbox Code Playgroud)

问题:我如何实现这种专注行为?能够将初始状态焦点恢复器和导航状态焦点恢复器结合起来吗?

Vig*_*aut 7

1. 更新到最新版本,以避免处理可能已修复的问题

由于您的存储库中的构建 ID 严重过时,我首先建议您

  • 要么切换到最新的 alpha 版本(1.0.0-alpha08截至今天)
  • 如果您想继续使用快照版本,可以更新settings.gradle.kts. 查看最新的工作 androidx 快照构建以查找最新的构建 ID。
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { 
            val buildId = "10639124"
            url = URI.create(
                "https://androidx.dev/snapshots/builds/$buildId/artifacts/repository"
            ) 
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

2.回答你的问题(解决方法)

注意:由于焦点恢复 API 还比较新且处于实验阶段,因此其用法在不断发展。当出现更好的用法时,我会尽力使这个答案保持最新状态。焦点恢复中还存在一个错误,它会阻止跨嵌套惰性容器记住焦点。一旦解决了,我们就不必做以下繁琐的事情来将焦点转移到最后一个焦点项目。

我们可以使用 CompositionLocal 来存储内容,而不是将 lastFocusedItem 状态传递给所有子组件。以下是我们将在下面的演示中使用的一些示例实用程序组合局部变量。

private val LocalLastFocusedItemPerDestination = compositionLocalOf<MutableMap<String, String>?> { null }
private val LocalFocusTransferredOnLaunch = compositionLocalOf<MutableState<Boolean>?> { null }
private val LocalNavHostController = compositionLocalOf<NavHostController?> { null }

@Composable
fun LocalLastFocusedItemPerDestinationProvider(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalLastFocusedItemPerDestination provides remember { mutableMapOf() }, content = content)
}

@Composable
fun LocalFocusTransferredOnLaunchProvider(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalFocusTransferredOnLaunch provides remember { mutableStateOf(false) }, content = content)
}

@Composable
fun LocalNavHostControllerProvider(navHostController: NavHostController, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalNavHostController provides navHostController, content = content)
}

@Composable
fun useLocalLastFocusedItemPerDestination(): MutableMap<String, String> {
    return LocalLastFocusedItemPerDestination.current ?: throw RuntimeException("Please wrap your app with LocalLastFocusedItemPerDestinationProvider")
}

@Composable
fun useLocalFocusTransferredOnLaunch(): MutableState<Boolean> {
    return LocalFocusTransferredOnLaunch.current ?: throw RuntimeException("Please wrap your app with LocalLastFocusedItemPerDestinationProvider")
}

@Composable
fun useLocalNavHostController(): NavHostController {
    return LocalNavHostController.current ?: throw RuntimeException("Please wrap your app with LocalNavHostControllerProvider")
}
Run Code Online (Sandbox Code Playgroud)

使用上述实用程序后,我们现在可以创建一个修改器,该修改器将在启动导航目标时聚焦该项目。在此修改器中,我们将检查这是否是导航目的地更改之前最后一个获得焦点的项目,如果是,我们将请求焦点集中到该项目上。

@Composable
fun Modifier.focusOnMount(itemKey: String): Modifier {
    val focusRequester = remember { FocusRequester() }
    val isInitialFocusTransferred = useLocalFocusTransferredOnLaunch()
    val lastFocusedItemPerDestination = useLocalLastFocusedItemPerDestination()
    val navHostController = useLocalNavHostController()
    val currentDestination = remember(navHostController) { navHostController.currentDestination?.route }

    return this
        .focusRequester(focusRequester)
        .onGloballyPositioned {
            val lastFocusedKey = lastFocusedItemPerDestination[currentDestination]
            if (!isInitialFocusTransferred.value && lastFocusedKey == itemKey) {
                focusRequester.requestFocus()
                isInitialFocusTransferred.value = true
            }
        }
        .onFocusChanged {
            if (it.isFocused) {
                lastFocusedItemPerDestination[currentDestination ?: ""] = itemKey
                isInitialFocusTransferred.value = true
            }
        }
}
Run Code Online (Sandbox Code Playgroud)

这个修饰符的使用非常简单。使用我们上面创建的实用程序提供程序包裹屏幕,并将focusOnMount修饰符分配给页面上的所有可聚焦项目(如按钮、卡片、选项卡等)。

请注意,这LocalFocusTransferredOnLaunchProvider是在每个屏幕级别单独提供的。

@Composable
fun App() {
    val navController = rememberNavController()

    LocalNavHostControllerProvider(navController) {
        LocalLastFocusedItemPerDestinationProvider {
            NavHost(navController = navController, startDestination = "home") {
                composable("home") {
                    LocalFocusTransferredOnLaunchProvider {
                        HomePage()
                    }
                }
                composable("movie") {
                    LocalFocusTransferredOnLaunchProvider {
                        Button(
                            modifier = Modifier.focusOnMount("home button"),
                            onClick = { navController.popBackStack() }
                        ) {
                            Text("Home")
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun HomePage() {
    TvLazyColumn {
        items(15) { row ->
            val navController = useLocalNavHostController()
        
            Column {
                Text(text = "Row $row")
        
                val focusRestorerModifiers = createInitialFocusRestorerModifiers()
        
                TvLazyRow(modifier = focusRestorerModifiers.parentModifier) {
                    items(15) { column ->
                        val key = "row=$row, column=$column"
                        key(key) {
                            Card(
                                modifier = Modifier
                                    .ifElse(
                                        condition = column == 0,
                                        ifTrueModifier = focusRestorerModifiers.childModifier
                                    )
                                    .focusOnMount(key),
                                onClick = { navController?.navigate("movie") }
                            ) {}
                        }
                    }
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意 key 和可组合项的用法key。使用焦点恢复时,将可聚焦项目包装在key可组合项中非常重要。

您可能还会发现,当应用程序首次启动时,没有任何焦点。要解决此问题,您可以更新LocalLastFocusedItemPerDestinationProvider以使用最初应获得焦点的项目的键进行初始化。

@Composable
fun LocalLastFocusedItemPerDestinationProvider(content: @Composable () -> Unit) {
    CompositionLocalProvider(
        LocalLastFocusedItemPerDestination provides remember {
            mutableMapOf(
                "home" to "row=0, column=0",
                "movie" to "home button"
            )
        },
        content = content
    )
}
Run Code Online (Sandbox Code Playgroud)