Android Jetpack Compose TV 焦点恢复

Plo*_*ial 5 android kotlin android-tv android-jetpack-compose android-jetpack-compose-tv

我在 TvLazyColumn 中有 TvLazyRows。当我导航到所有列表的末尾(位置 [20,20])导航到下一个屏幕并返回时,焦点将恢复到第一个可见位置 [15,1],而不是我之前所在的位置 [20,20] ]。如何将焦点恢复到某个特定位置?

在此输入图像描述

class MainActivity : ComponentActivity() {

    private val rowItems = (0..20).toList()
    private val rows = (0..20).toList()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()
            MyAppNavHost(navController = navController)
        }
    }

    @Composable
    fun List(navController: NavController) {
        val fr = remember {
            FocusRequester()
        }
        TvLazyColumn( modifier = Modifier
            .focusRequester(fr)
            .fillMaxSize()
            ,
            verticalArrangement = Arrangement.spacedBy(16.dp),
            pivotOffsets = PivotOffsets(parentFraction = 0.05f),
        ) {
            items(rows.size) { rowPos ->
                Column() {
                    Text(text = "Row $rowPos")
                    TvLazyRow(
                        modifier = Modifier
                            .height(70.dp),
                        horizontalArrangement = Arrangement.spacedBy(16.dp),
                        pivotOffsets = PivotOffsets(parentFraction = 0.0f),
                    ) {
                        items(rowItems.size) { itemPos ->
                            var color by remember {
                                mutableStateOf(Color.Green)
                            }
                            Box(
                                Modifier
                                    .width(100.dp)
                                    .height(50.dp)
                                    .onFocusChanged {
                                        color = if (it.hasFocus) {
                                            Color.Red
                                        } else {
                                            Color.Green
                                        }
                                    }
                                    .background(color)
                                    .clickable {
                                        navController.navigate("details")
                                    }


                            ) {
                                Text(text = "Item ${itemPos.toString()}", Modifier.align(Alignment.Center))
                            }
                        }
                    }
                }
            }
        }
        LaunchedEffect(true) {
            fr.requestFocus()
        }
    }

    @Composable
    fun MyAppNavHost(
        navController: NavHostController = rememberNavController(),
        startDestination: String = "list"
    ) {
        NavHost(
            navController = navController,
            startDestination = startDestination
        ) {
            composable("details") {
                Details()
            }
            composable("list") { List(navController) }
        }
    }

    @Composable
    fun Details() {
        Box(
            Modifier
                .background(Color.Blue)
                .fillMaxSize()) {
            Text("Second Screen", Modifier.align(Alignment.Center), fontSize = 48.sp)
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

版本

dependencies {

    implementation 'androidx.core:core-ktx:1.10.1'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
    implementation 'androidx.activity:activity-compose:1.7.1'
    implementation platform('androidx.compose:compose-bom:2022.10.00')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
    debugImplementation 'androidx.compose.ui:ui-tooling'
    debugImplementation 'androidx.compose.ui:ui-test-manifest'

    // Compose for TV dependencies
    def tvCompose = '1.0.0-alpha06'
    implementation "androidx.tv:tv-foundation:$tvCompose"
    implementation "androidx.tv:tv-material:$tvCompose"

    def nav_version = "2.5.3"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}
Run Code Online (Sandbox Code Playgroud)

我尝试将 FocusRequestor 传递给列表中的每个可聚焦元素。在这种情况下,我能够恢复注意力。但是对于列表中的大量元素,它开始因 OutOfMemmoryError 而崩溃。所以我需要另一种解决方案。

小智 4

在 Jetpack Compose 中,导航在设计上是无状态的,这意味着默认情况下不保留焦点状态。为了实现这一点,我们必须自己维护状态(项目的位置)。

下面是一个建议的解决方案,您可以将其集成到您的代码中。请注意,此解决方案的工作假设是列表中的项目不会动态更改。如果他们这样做,你可能需要稍微调整一下逻辑:

  1. 您需要将最后一个焦点项目保持在某种状态。
private var lastFocusedItem by rememberSaveable{ mutableStateOf(Pair(0, 0)) }
Run Code Online (Sandbox Code Playgroud)
  1. 当某个项目获得焦点时,您需要更新lastFocusedItem
.onFocusChanged { focusState ->
    if (focusState.hasFocus) {
        lastFocusedItem = Pair(rowPos, itemPos)
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)
  1. 当您导航回列表屏幕时,您需要为最后一个获得焦点的项目请求焦点。
LaunchedEffect(true) {
    // Request focus to the last focused item
    focusRequesters[lastFocusedItem]?.requestFocus()
}
Run Code Online (Sandbox Code Playgroud)

为了实现最后一点,我们需要有 s 的映射FocusRequester。我们应该使用一个映射,其中键作为项目位置(rowPos,itemPos),值作为FocusRequesters。

这是代码的更新部分,用于维护和恢复上次导航项目的焦点。

这是一个两步过程:

  1. 创建一个可变状态映射,其中包含 (rowPos, itemPos) 对作为键及其对应的FocusRequester值。使用 RememberSaveable 在屏幕导航期间保留值。
  2. 记住FocusRequester每个项目的 a 并将其添加到focusRequesters地图中。

您更新后的List可组合项可能如下所示:

@Composable
fun List(navController: NavController) {
    val focusRequesters = remember { mutableMapOf<Pair<Int, Int>, FocusRequester>() }
    var lastFocusedItem by rememberSaveable{ mutableStateOf(Pair(0, 0)) }
    TvLazyColumn(
        modifier = Modifier
            .fillMaxSize(),
        verticalArrangement = Arrangement.spacedBy(16.dp),
        pivotOffsets = PivotOffsets(parentFraction = 0.05f),
    ) {
        items(rows.size) { rowPos ->
            Column() {
                Text(text = "Row $rowPos")
                TvLazyRow(
                    modifier = Modifier
                        .height(70.dp),
                    horizontalArrangement = Arrangement.spacedBy(16.dp),
                    pivotOffsets = PivotOffsets(parentFraction = 0.0f),
                ) {
                    items(rowItems.size) { itemPos ->
                        var color by remember { mutableStateOf(Color.Green) }
                        val fr = remember { FocusRequester() }
                        focusRequesters[Pair(rowPos, itemPos)] = fr
                        Box(
                            Modifier
                                .width(100.dp)
                                .height(50.dp)
                                .focusRequester(fr)
                                .onFocusChanged {
                                    color = if (it.hasFocus) {
                                        lastFocusedItem = Pair(rowPos, itemPos)
                                        Color.Red
                                    } else {
                                        Color.Green
                                    }
                                }
                                .background(color)
                                .clickable {
                                    navController.navigate("details")
                                }
                        ) {
                            Text(text = "Item ${itemPos.toString()}", Modifier.align(Alignment.Center))
                        }
                    }
                }
            }
        }
    }
    LaunchedEffect(true) {
        focusRequesters[lastFocusedItem]?.requestFocus()
    }
}
Run Code Online (Sandbox Code Playgroud)

附言。拥有可组合的方法是一个坏主意。可组合项应该是没有副作用的纯函数。