Android Jetpack Compose TV Focus восстановление

У меня есть TvLazyRows внутри TvLazyColumn. Когда я перехожу к концу всех списков (позиция [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)
        }
    }

}

версии

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"
}

Я попытался передать FocusRequestor каждому фокусируемому элементу внутри списка. В этом случае мне удалось восстановить фокус. Но для большого количества элементов внутри списка он начинает падать с OutOfMemmoryError. Поэтому мне нужно другое решение.

2
0
173
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В Jetpack Compose навигация по своей природе не имеет состояния, что означает, что состояние фокуса не сохраняется по умолчанию. Чтобы достичь этого, мы должны сами поддерживать состояние (положение элемента).

Ниже представлено предлагаемое решение, которое вы можете интегрировать в свой код. Обратите внимание, что это решение работает с предположением, что элементы вашего списка не изменяются динамически. Если они это сделают, вам, возможно, придется немного изменить логику:

  1. Вам нужно поддерживать последний сфокусированный элемент в состоянии.
private var lastFocusedItem by rememberSaveable{ mutableStateOf(Pair(0, 0)) }
  1. Когда элемент получает фокус, вам нужно обновить lastFocusedItem.
.onFocusChanged { focusState ->
    if (focusState.hasFocus) {
        lastFocusedItem = Pair(rowPos, itemPos)
    }
    ...
}
  1. Когда вы вернетесь к экрану списка, вам нужно запросить фокус для последнего выделенного элемента.
LaunchedEffect(true) {
    // Request focus to the last focused item
    focusRequesters[lastFocusedItem]?.requestFocus()
}

Чтобы достичь последней точки, нам нужна карта FocusRequesters. Мы должны использовать карту с ключами в качестве позиций элементов (rowPos, itemPos) и значениями в качестве FocusRequesters.

Вот обновленная часть вашего кода, которая поддерживает и восстанавливает фокус последнего перемещенного элемента.

Это двухэтапный процесс:

  1. Создайте изменяемую карту состояний, которая содержит пары (rowPos, itemPos) в качестве ключей и соответствующие им FocusRequester в качестве значений. Используйте RememberSaveable, чтобы сохранить значение во время навигации по экрану.
  2. Запомните FocusRequester для каждого предмета и добавьте его на карту 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()
    }
}

PS. иметь компонуемые методы - плохая идея. Составные должны быть чистыми функциями без побочных эффектов.

Хотя трудно дать правильный ответ на этот вопрос, предложенный здесь подход имеет несколько потенциальных проблем. Во-первых, запрос фокуса при добавлении этого компонуемого в композицию может привести к непредсказуемым изменениям фокуса во многих сценариях. Еще одна вещь, на которую следует обратить внимание, это то, что есть большая вероятность, что последний элемент в фокусе еще не составлен при возврате, что приведет к сбою, если вы попытаетесь вызвать requestFocus для соответствующего FocusRequester.

Remco 24.05.2023 12:46

@ Ремко Шур. с requestFocus нужно быть осторожным. Но в этом случае TvLazyRow восстанавливает положение прокрутки. И этот код (LaunchedEffect(true)) будет запущен только после завершения композиции. Итак, все видимые элементы созданы. Кроме того, нулевая проверка поможет избежать сбоев. Мы добавляем FocusRequesters на карту focusRequesters только после добавления элемента.

corvinav 25.05.2023 16:20

Другие вопросы по теме