diff --git a/compose/snippets/src/main/AndroidManifest.xml b/compose/snippets/src/main/AndroidManifest.xml
index c9b15e6c..30e91caf 100644
--- a/compose/snippets/src/main/AndroidManifest.xml
+++ b/compose/snippets/src/main/AndroidManifest.xml
@@ -55,6 +55,9 @@
android:exported="false"/>
+
+
+
ApplyPolygonAsClipImage()
Destination.SharedElementExamples -> PlaceholderSizeAnimated_Demo()
Destination.PagerExamples -> PagerExamples()
+ Destination.FocusExample -> FocusExample()
}
}
}
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt
index 78396390..3c163947 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt
@@ -24,7 +24,8 @@ enum class Destination(val route: String, val title: String) {
ScreenshotExample("screenshotExample", "Screenshot Examples"),
ShapesExamples("shapesExamples", "Shapes Examples"),
SharedElementExamples("sharedElement", "Shared elements"),
- PagerExamples("pagerExamples", "Pager examples")
+ PagerExamples("pagerExamples", "Pager examples"),
+ FocusExample("focusExample", "Keyboard Focus"),
}
// Enum class for compose components navigation screen.
@@ -49,5 +50,5 @@ enum class TopComponentsDestination(val route: String, val title: String) {
MenusExample("menusExamples", "Menus"),
TooltipExamples("tooltipExamples", "Tooltips"),
NavigationDrawerExamples("navigationDrawerExamples", "Navigation drawer"),
- SegmentedButtonExamples("segmentedButtonExamples", "Segmented button")
+ SegmentedButtonExamples("segmentedButtonExamples", "Segmented button"),
}
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusExample.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusExample.kt
new file mode 100644
index 00000000..b9a5e4db
--- /dev/null
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusExample.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.snippets.touchinput.focus
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusRestorer
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+
+enum class FocusExample(
+ val route: String,
+ val title: String
+) {
+ Home("home", "Home"),
+ FocusTraversalOrder("focusTraversal", "Focus Traversal Order"),
+ InitialFocus("initialFocus", "Initial Focus"),
+ InitialFocusWithScrollableContainer(
+ "initialFocusWithScrollableContainer",
+ "Initial Focus with Scrollable Container"
+ ),
+ InitialFocusEnablingContentReload(
+ "initialFocusEnablingContentReload",
+ "Initial Focus Enabling Content Reload"
+ ),
+ FocusRestoration("focusRestoration", "Focus Restoration"),
+ FocusInListDetailLayout("focusInListDetailLayout", "Focus In List-Detail Layout"),
+}
+
+@Composable
+fun FocusExample(
+ navController: NavHostController = rememberNavController()
+) {
+ NavHost(navController, startDestination = FocusExample.Home.route) {
+ composable(FocusExample.Home.route) {
+ val entries = remember {
+ listOf(
+ FocusExample.FocusTraversalOrder,
+ FocusExample.InitialFocus,
+ FocusExample.InitialFocusWithScrollableContainer,
+ FocusExample.InitialFocusEnablingContentReload,
+ FocusExample.FocusRestoration,
+ FocusExample.FocusInListDetailLayout,
+ )
+ }
+ FocusExampleScreen(entries) {
+ navController.navigate(it.route)
+ }
+ }
+ composable(FocusExample.FocusTraversalOrder.route) {
+ FocusTraversalScreen(modifier = Modifier.padding(16.dp))
+ }
+ composable(FocusExample.InitialFocus.route) {
+ InitialFocusScreen()
+ }
+ composable(FocusExample.InitialFocusWithScrollableContainer.route) {
+ InitialFocusWithScrollableContainerScreen()
+ }
+ composable(FocusExample.InitialFocusEnablingContentReload.route) {
+ InitialFocusWithContentReloadScreen()
+ }
+ composable(FocusExample.FocusRestoration.route) {
+ FocusRestorationScreen()
+ }
+ composable(FocusExample.FocusInListDetailLayout.route) {
+ FocusRestorationInListDetailScreen()
+ }
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun FocusExampleScreen(
+ examples: List,
+ navigateToDetails: (FocusExample) -> Unit,
+) {
+ val focusRequester = remember { FocusRequester() }
+
+ Box(contentAlignment = Alignment.TopCenter) {
+ ExampleList(
+ examples = examples,
+ showDetails = {
+ // Save the focused child before navigating to the detail pane
+ focusRequester.saveFocusedChild()
+ navigateToDetails(it)
+ },
+ modifier = Modifier
+ .widthIn(max = 600.dp)
+ .focusRequester(focusRequester)
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun ExampleList(
+ examples: List,
+ showDetails: (FocusExample) -> Unit = {},
+ modifier: Modifier = Modifier,
+
+ ) {
+ val context = LocalContext.current
+ // [START android_compose_touchinput_focus_restoration]
+ LazyColumn(
+ modifier = modifier.focusRestorer()
+ ) {
+ items(examples) {
+ ListItem(
+ headlineContent = { Text(it.title) },
+ modifier = Modifier.clickable {
+ showDetails(it)
+ }
+ )
+ }
+ }
+ // [END android_compose_touchinput_focus_restoration]
+}
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusRestorationInListDetailLayoutSnippet.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusRestorationInListDetailLayoutSnippet.kt
new file mode 100644
index 00000000..3e6d8839
--- /dev/null
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusRestorationInListDetailLayoutSnippet.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.snippets.touchinput.focus
+
+import android.os.Parcelable
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Button
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.layout.AnimatedPane
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
+import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
+import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusRestorer
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.unit.dp
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+class CatalogItem(val value: Int) : Parcelable {
+ companion object {
+ fun createCatalog(items: Int, startValue: Int = 1): List {
+ val lastValue = startValue + items
+ return (startValue..lastValue).map { CatalogItem(it) }
+ }
+ }
+}
+
+@Composable
+fun FocusRestorationInListDetailScreen(
+ modifier: Modifier = Modifier
+) {
+
+ val catalogData = remember {
+ CatalogItem.createCatalog(32)
+ }
+
+ FocusRestorationInListDetail(catalogData, modifier)
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalComposeUiApi::class)
+// [START android_compose_touchinput_focus_restoration_listdetail]
+@Composable
+fun FocusRestorationInListDetail(catalogData: List, modifier: Modifier = Modifier) {
+ val threePaneScaffoldNavigator = rememberListDetailPaneScaffoldNavigator()
+ val listState = rememberLazyListState()
+ val focusRequester = remember { FocusRequester() }
+
+ // Remember the last selected item in the list pane
+ // to specify the list item to be focused when users back from the details pane.
+ var lastSelectedCatalogItem by remember { mutableStateOf(catalogData.first()) }
+
+ // Flag indicating that it is safe to request focus on the list pane.
+ var isListPaneVisible by remember { mutableStateOf(false) }
+
+ BackHandler(threePaneScaffoldNavigator.canNavigateBack(BackNavigationBehavior.PopLatest)) {
+ threePaneScaffoldNavigator.navigateBack(BackNavigationBehavior.PopLatest)
+ }
+
+ ListDetailPaneScaffold(
+ value = threePaneScaffoldNavigator.scaffoldValue,
+ directive = threePaneScaffoldNavigator.scaffoldDirective,
+ listPane = {
+ AnimatedPane {
+ // ListPane implements the list pane.
+ // showDetails function is called when the user select a list item.
+ ListPane(
+ catalogData = catalogData,
+ state = listState,
+ initialFocusItem = lastSelectedCatalogItem,
+ showDetails = { catalogItem ->
+ // Update lastSelectedCatalogItem with the catalogItem object
+ // associated with the clicked list item.
+ lastSelectedCatalogItem = catalogItem
+
+ // Save the focused child in the ListPane
+ // so that the component can restore focus
+ // when users moving focus to ListPane in two-pane layout.
+ focusRequester.saveFocusedChild()
+
+ // Show the details of the catalogItem value in the detail pane.
+ threePaneScaffoldNavigator.navigateTo(
+ ListDetailPaneScaffoldRole.Detail,
+ catalogItem
+ )
+ },
+ modifier = Modifier
+ // Associate focusRequester value with the ListPane composable.
+ .focusRequester(focusRequester)
+ // Set true to isListPaneVisible variable as ListPane composable is visible.
+ .onPlaced { isListPaneVisible = true }
+ )
+ DisposableEffect(Unit) {
+ // ListPane is removed from the composition when the app is in single pane layout.
+ // Set isSafeToRequestFocus to false so that ListPane gets focused
+ // when it is displayed by users' back action.
+ onDispose { isListPaneVisible = false }
+ }
+ }
+ },
+ detailPane = {
+ AnimatedPane {
+ val catalogItem = threePaneScaffoldNavigator.currentDestination?.content
+ if (catalogItem != null) {
+ DetailsPane(catalogItem)
+ }
+ }
+ },
+ modifier = modifier
+ )
+
+ LaunchedEffect(isListPaneVisible) {
+ if (isListPaneVisible) {
+ val catalogItemIndex = catalogData.indexOf(lastSelectedCatalogItem)
+ if (catalogItemIndex >= 0) {
+ // Ensure the ListItem for the last selected item is visible
+ listState.animateScrollToItem(catalogItemIndex)
+ }
+ focusRequester.requestFocus()
+ }
+ }
+}
+// [END android_compose_touchinput_focus_restoration_listdetail]
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun ListPane(
+ catalogData: List,
+ showDetails: (CatalogItem) -> Unit,
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ initialFocusItem: CatalogItem? = null,
+) {
+ val initialFocus = remember { FocusRequester() }
+ val columnModifier = if (catalogData.isEmpty()) {
+ modifier.focusRestorer {
+ initialFocus
+ }
+ } else {
+ modifier.focusRestorer()
+ }
+
+ LazyColumn(
+ modifier = columnModifier,
+ state = state,
+ ) {
+ items(catalogData) {
+ val itemModifier = if (it == initialFocusItem) {
+ Modifier.focusRequester(initialFocus)
+ } else {
+ Modifier
+ }
+ ListItem(
+ headlineContent = {
+ Text("Item ${it.value}")
+ },
+ modifier = itemModifier.clickable { showDetails(it) },
+ )
+ }
+ }
+}
+
+@Composable
+private fun DetailsPane(
+ catalogItem: CatalogItem,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text("Item ${catalogItem.value}", style = MaterialTheme.typography.displayMedium)
+ Button(
+ onClick = {},
+ modifier = Modifier.initialFocus()
+ ) {
+ Text("Click me")
+ }
+ }
+}
+
+fun FocusRequester.tryRequestFocus(): Result {
+ try {
+ requestFocus()
+ } catch (e: IllegalStateException) {
+ return Result.failure(e)
+ }
+ return Result.success(Unit)
+}
+
+@Composable
+fun Modifier.initialFocus(focusRequester: FocusRequester = remember { FocusRequester() }): Modifier {
+ var isSafe by remember { mutableStateOf(false) }
+
+ LaunchedEffect(isSafe) {
+ if (isSafe) {
+ focusRequester.tryRequestFocus()
+ }
+ }
+ return this.focusRequester(focusRequester).onPlaced { isSafe = true }
+}
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusRestorationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusRestorationSnippets.kt
new file mode 100644
index 00000000..0548de8c
--- /dev/null
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusRestorationSnippets.kt
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.snippets.touchinput.focus
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusGroup
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusRestorer
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.compose.viewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+data class Section(val name: String, val catalog: List)
+
+class FocusRestorationScreenViewModel : ViewModel() {
+
+ companion object {
+ private val sectionListA = listOf(
+ Section("Section A", CatalogItem.createCatalog(16)),
+ Section("Section B", CatalogItem.createCatalog(8, startValue = 18)),
+ Section("Section C", CatalogItem.createCatalog(16, startValue = 27)),
+ )
+
+ private val sectionListB = listOf(
+ Section("Section D", CatalogItem.createCatalog(8, startValue = 100)),
+ Section("Section F", CatalogItem.createCatalog(16, startValue = 109)),
+ Section("Section E", CatalogItem.createCatalog(8, startValue = 126)),
+ )
+
+ private val sectionSet = listOf(sectionListA, sectionListB)
+ }
+
+ private val currentSet = MutableStateFlow(0)
+
+ val sections = currentSet.map {
+ sectionSet[it]
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
+
+ fun nextPage() {
+ currentSet.value = (currentSet.value + 1) % sectionSet.size
+ }
+}
+
+@Composable
+fun FocusRestorationScreen(
+ modifier: Modifier = Modifier,
+ coroutineScope: CoroutineScope = rememberCoroutineScope(),
+ focusRestorationScreenViewModel: FocusRestorationScreenViewModel = viewModel()
+) {
+ val sections by focusRestorationScreenViewModel.sections.collectAsStateWithLifecycle()
+ val state = rememberLazyListState()
+ val focusRequester = remember { FocusRequester() }
+ val scrollToTop = remember {
+ {
+ coroutineScope.launch {
+ state.scrollToItem(0)
+ focusRequester.requestFocus()
+ }
+ }
+ }
+
+ CatalogWithSection(
+ sections = sections,
+ state = state,
+ reload = {
+ focusRestorationScreenViewModel.nextPage()
+ scrollToTop()
+ },
+ scrollToTop = { scrollToTop() },
+ modifier = modifier.focusRequester(focusRequester)
+ )
+}
+
+@Composable
+private fun CatalogWithSection(
+ sections: List,
+ reload: () -> Unit,
+ scrollToTop: () -> Unit,
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+) {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(32.dp),
+ state = state,
+ modifier = modifier
+ ) {
+ items(sections) {
+ CatalogSection(it)
+ }
+ item {
+ Controls(
+ reload = reload,
+ scrollToTop = scrollToTop,
+ modifier = Modifier.padding(horizontal = 16.dp)
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun CatalogSection(
+ section: Section,
+ modifier: Modifier = Modifier,
+ horizontalOffset: Dp = 16.dp,
+ coroutineScope: CoroutineScope = rememberCoroutineScope()
+) {
+ val bringIntoViewRequester = remember { BringIntoViewRequester() }
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = modifier
+ .bringIntoViewRequester(bringIntoViewRequester)
+ .onFocusChanged { focusState ->
+ // Bring the Column into view port when any CatalogItemCard get focused,
+ // so that users can see the section title and cards at the same time.
+ if (focusState.hasFocus) {
+ coroutineScope.launch {
+ bringIntoViewRequester.bringIntoView()
+ }
+ }
+ },
+ ) {
+ Text(
+ section.name,
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(horizontal = horizontalOffset)
+ )
+ SectionCatalog(section.catalog)
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun SectionCatalog(
+ catalog: List,
+ modifier: Modifier = Modifier,
+ horizontalOffset: Dp = 16.dp,
+) {
+// [START android_compose_touchinput_focus_restoration_manually]
+ val focusRequester = remember(catalog) { FocusRequester() }
+
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ contentPadding = PaddingValues(horizontal = horizontalOffset),
+ modifier = modifier
+ .focusRequester(focusRequester)
+ .focusProperties {
+ exit = {
+ focusRequester.saveFocusedChild()
+ FocusRequester.Default
+ }
+ enter = {
+ if (focusRequester.restoreFocusedChild()) {
+ FocusRequester.Cancel
+ } else {
+ FocusRequester.Default
+ }
+ }
+ }
+ ) {
+ items(catalog) {
+ CatalogItemCard(it, modifier = Modifier.width(128.dp))
+ }
+ }
+// [END android_compose_touchinput_focus_restoration_manually]
+}
+
+@Composable
+private fun CatalogItemCard(
+ catalogItem: CatalogItem,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit = {}
+) {
+ Card(onClick = onClick, modifier = modifier.aspectRatio(9f / 16f)) {
+ Text("${catalogItem.value}", modifier = Modifier.padding(16.dp))
+ }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun Controls(
+ reload: () -> Unit,
+ scrollToTop: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ // [START android_compose_touchinput_focus_restoration_with_row]
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = modifier
+ .focusRestorer()
+ .focusGroup()
+ ) {
+ BackToTopCard(onClick = scrollToTop, modifier = Modifier.width(128.dp))
+ ReloadCard(onClick = reload, modifier = Modifier.width(128.dp))
+ }
+ // [END android_compose_touchinput_focus_restoration_with_row]
+}
+
+@Composable
+private fun ReloadCard(
+ onClick: () -> Unit = {},
+ modifier: Modifier = Modifier
+) {
+ SquareCard(modifier = modifier, onClick = onClick) {
+ Text("Reload")
+ }
+}
+
+@Composable
+private fun BackToTopCard(
+ onClick: () -> Unit = {},
+ modifier: Modifier = Modifier
+) {
+ SquareCard(modifier = modifier, onClick = onClick) {
+ Text("To top")
+ }
+}
+
+@Composable
+private fun SquareCard(
+ onClick: () -> Unit = {},
+ modifier: Modifier = Modifier,
+ content: @Composable BoxScope.() -> Unit
+) {
+ Card(onClick = onClick, modifier = modifier.aspectRatio(1f)) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ content = content
+ )
+ }
+}
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt
index 82b5b2d1..341ffa9e 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt
@@ -18,8 +18,10 @@
package com.example.compose.snippets.touchinput.focus
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Indication
import androidx.compose.foundation.IndicationInstance
+import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -28,24 +30,37 @@ import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
@@ -59,7 +74,6 @@ import androidx.compose.ui.focus.FocusRequester.Companion.Default
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Blue
import androidx.compose.ui.graphics.Color.Companion.Green
import androidx.compose.ui.graphics.Color.Companion.Red
@@ -71,13 +85,211 @@ import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+internal fun FocusTraversalScreen(
+ modifier: Modifier = Modifier
+) {
+ val tabs = listOf("Layout and traversal", "Focus properties", "Focus group")
+ val focusRequesters = remember { tabs.map { FocusRequester() } }
+ var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) }
+ var moveFocusToContent by remember { mutableStateOf(false) }
+
+
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(32.dp)
+ ) {
+ TabRow(
+ selectedTabIndex = selectedTabIndex,
+ modifier = Modifier
+ .focusProperties {
+ enter = { focusRequesters[selectedTabIndex] }
+ }
+ .focusGroup()
+ ) {
+ tabs.zip(focusRequesters).forEachIndexed { index, (label, focusRequester) ->
+ Tab(
+ selected = index == selectedTabIndex,
+ onClick = {
+ moveFocusToContent = true
+ selectedTabIndex = index
+ },
+ text = { Text(label) },
+ modifier = Modifier.focusRequester(focusRequester)
+ )
+ }
+ }
+ when (selectedTabIndex) {
+ 0 -> FocusTraversal(requestFocusOnLoad = moveFocusToContent)
+ 1 -> FocusProperties(requestFocusOnLoad = moveFocusToContent)
+ 2 -> FocusGroup(requestFocusOnLoad = moveFocusToContent)
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun FocusTraversal(
+ modifier: Modifier = Modifier,
+ verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(48.dp),
+ scrollState: ScrollState = rememberScrollState(),
+ focusRequester: FocusRequester = remember { FocusRequester() },
+ requestFocusOnLoad: Boolean = false
+) {
+ Container(
+ modifier = modifier,
+ verticalArrangement = verticalArrangement,
+ scrollState = scrollState,
+ focusRequester = focusRequester,
+ requestFocusOnLoad = requestFocusOnLoad
+ ) {
+ Title("Focus traversal order is irrelevant to layout order")
+
+ Section(title = "Two rows in a column") {
+ BasicSample(contentAlignment = Alignment.TopStart)
+ }
+
+ Section(title = "Two columns in a row") {
+ BasicSample2(contentAlignment = Alignment.TopStart)
+ }
+
+ Section(title = "Element with offset") {
+ BasicSample3(contentAlignment = Alignment.TopStart)
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun FocusProperties(
+ modifier: Modifier = Modifier,
+ verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(48.dp),
+ scrollState: ScrollState = rememberScrollState(),
+ focusRequester: FocusRequester = remember { FocusRequester() },
+ requestFocusOnLoad: Boolean = false
+) {
+ Container(
+ modifier = modifier,
+ verticalArrangement = verticalArrangement,
+ scrollState = scrollState,
+ focusRequester = focusRequester,
+ requestFocusOnLoad = requestFocusOnLoad
+ ) {
+ Title("FocusProperties override default traversal order")
+
+ Section(title = "Default traversal order") {
+ DefaultTraversalOrder()
+ }
+
+ Section(title = "Override default traversal order") {
+ OverrideDefaultTraversalOrder()
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun FocusGroup(
+ modifier: Modifier = Modifier,
+ verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(48.dp),
+ scrollState: ScrollState = rememberScrollState(),
+ focusRequester: FocusRequester = remember { FocusRequester() },
+ requestFocusOnLoad: Boolean = false
+) {
+ Container(
+ modifier = modifier,
+ verticalArrangement = verticalArrangement,
+ scrollState = scrollState,
+ focusRequester = focusRequester,
+ requestFocusOnLoad = requestFocusOnLoad
+ ) {
+ Title("FocusGroup enables to handle enter and exit")
+
+ Section(title = "Without focus group") {
+ WithoutFocusGroup()
+ }
+
+ Section(title = "Focus moves to the second item") {
+ WithFocusGroup()
+ }
+ }
+}
+
+@Composable
+private fun Container(
+ modifier: Modifier = Modifier,
+ verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(48.dp),
+ scrollState: ScrollState = rememberScrollState(),
+ focusRequester: FocusRequester = remember { FocusRequester() },
+ requestFocusOnLoad: Boolean = false,
+ content: @Composable ColumnScope.() -> Unit,
+) {
+ Column(
+ modifier = modifier
+ .verticalScroll(scrollState)
+ .focusRequester(focusRequester)
+ .focusGroup(),
+ verticalArrangement = verticalArrangement,
+ content = content
+ )
+ LaunchedEffect(Unit) {
+ if (requestFocusOnLoad) {
+ focusRequester.requestFocus()
+ }
+ }
+}
+
+@Composable
+private fun Title(
+ text: String,
+ modifier: Modifier = Modifier,
+ style: TextStyle = MaterialTheme.typography.displaySmall
+) {
+ Text(text = text, modifier = modifier, style = style)
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun Section(
+ title: String,
+ modifier: Modifier = Modifier,
+ verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp),
+ style: TextStyle = MaterialTheme.typography.titleLarge,
+ bringIntoViewRequester: BringIntoViewRequester = remember { BringIntoViewRequester() },
+ coroutineScope: CoroutineScope = rememberCoroutineScope(),
+ content: @Composable () -> Unit,
+) {
+ Column(
+ modifier = modifier
+ .bringIntoViewRequester(bringIntoViewRequester)
+ .onFocusChanged { focusState ->
+ if (focusState.hasFocus) {
+ coroutineScope.launch {
+ bringIntoViewRequester.bringIntoView()
+ }
+ }
+ },
+ verticalArrangement = verticalArrangement
+ ) {
+ Text(title, style = style)
+ content()
+ }
+}
@Preview
@Composable
-private fun BasicSample() {
- Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+private fun BasicSample(
+ modifier: Modifier = Modifier,
+ contentAlignment: Alignment = Alignment.Center
+) {
+ Box(modifier = modifier, contentAlignment = contentAlignment) {
// [START android_compose_touchinput_focus_horizontal]
Column {
Row {
@@ -95,8 +307,11 @@ private fun BasicSample() {
@Preview
@Composable
-private fun BasicSample2() {
- Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+private fun BasicSample2(
+ modifier: Modifier = Modifier,
+ contentAlignment: Alignment = Alignment.Center
+) {
+ Box(modifier = modifier, contentAlignment = contentAlignment) {
// [START android_compose_touchinput_focus_vertical]
Row {
Column {
@@ -112,9 +327,31 @@ private fun BasicSample2() {
}
}
-@Preview
@Composable
-fun OverrideDefaultOrder() {
+private fun BasicSample3(
+ modifier: Modifier = Modifier,
+ contentAlignment: Alignment = Alignment.Center
+) {
+ Box(modifier = modifier, contentAlignment = contentAlignment) {
+ // [START android_compose_touchinput_focus_vertical]
+ Row {
+ Column {
+ TextButton(
+ onClick = { },
+ modifier = Modifier.offset(x = 300.dp)
+ ) { Text("First field") }
+ TextButton({ }) { Text("Second field") }
+ }
+ Column {
+ TextButton({ }) { Text("Third field") }
+ TextButton({ }) { Text("Fourth field") }
+ }
+ }
+ }
+}
+
+@Composable
+private fun DefaultTraversalOrder() {
// [START android_compose_touchinput_focus_override_refs]
val (first, second, third, fourth) = remember { FocusRequester.createRefs() }
// [END android_compose_touchinput_focus_override_refs]
@@ -131,6 +368,12 @@ fun OverrideDefaultOrder() {
}
}
// [END android_compose_touchinput_focus_override]
+}
+
+@Preview
+@Composable
+private fun OverrideDefaultTraversalOrder() {
+ val (first, second, third, fourth) = remember { FocusRequester.createRefs() }
// [START android_compose_touchinput_focus_override_use]
Column {
@@ -194,8 +437,51 @@ fun OverrideTwoDimensionalOrder() {
// [END android_compose_touchinput_focus_override_2d]
}
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun WithoutFocusGroup() {
+ // [START android_compose_touchinput_focus_without_focus_group]
+ val secondItem = remember { FocusRequester() }
+ Row(
+ modifier = Modifier.focusProperties {
+ enter = { secondItem }
+ }
+ ) {
+
+ TextButton(onClick = {}) { Text("First") }
+ TextButton(onClick = {}, modifier = Modifier.focusRequester(secondItem)) { Text("Second") }
+ TextButton(onClick = {}) { Text("Third") }
+ TextButton(onClick = {}) { Text("Fourth") }
+
+ }
+ // [END android_compose_touchinput_focus_without_focus_group]
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+private fun WithFocusGroup() {
+ // [START android_compose_touchinput_focus_without_focus_group]
+ val secondItem = remember { FocusRequester() }
+ Row(
+ modifier = Modifier
+ .focusProperties {
+ enter = { secondItem }
+ }
+ .focusGroup()
+ ) {
+
+ TextButton(onClick = {}) { Text("First") }
+ TextButton(onClick = {}, modifier = Modifier.focusRequester(secondItem)) { Text("Second") }
+ TextButton(onClick = {}) { Text("Third") }
+ TextButton(onClick = {}) { Text("Fourth") }
+
+ }
+ // [END android_compose_touchinput_focus_without_focus_group]
+}
+
+
@Composable
-private fun FocusGroup() {
+private fun FocusGroupInLazyVerticalGrid() {
@Composable
fun FilterChipA() {
@@ -424,7 +710,7 @@ private fun FocusAdvancing() {
@Composable
private fun ReactToFocus() {
// [START android_compose_touchinput_focus_react]
- var color by remember { mutableStateOf(Color.White) }
+ var color by remember { mutableStateOf(White) }
Card(
modifier = Modifier
.onFocusChanged {
@@ -442,7 +728,7 @@ private class MyHighlightIndicationInstance(isEnabledState: State) :
override fun ContentDrawScope.drawIndication() {
drawContent()
if (isEnabled) {
- drawRect(size = size, color = Color.White, alpha = 0.2f)
+ drawRect(size = size, color = White, alpha = 0.2f)
}
}
}
@@ -452,7 +738,7 @@ private class MyHighlightIndicationInstance(isEnabledState: State) :
class MyHighlightIndication : Indication {
@Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource):
- IndicationInstance {
+ IndicationInstance {
val isFocusedState = interactionSource.collectIsFocusedAsState()
return remember(interactionSource) {
MyHighlightIndicationInstance(isEnabledState = isFocusedState)
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/InitialFocusSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/InitialFocusSnippets.kt
new file mode 100644
index 00000000..6934d942
--- /dev/null
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/InitialFocusSnippets.kt
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.compose.snippets.touchinput.focus
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Card
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.compose.viewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+// [START android_compose_touchinput_initialfocus_basics]
+@Composable
+fun InitialFocusScreen(
+ onCardClick: (String) -> Unit = {}
+) {
+ // Remember the FocusRequester object
+ val initialFocus = remember { FocusRequester() }
+
+ Row {
+ Card(
+ onClick = { onCardClick("Card 1") },
+ // Associate the card with the FocusRequester object.
+ modifier = Modifier.focusRequester(initialFocus)
+ ) {
+ Text("Card 1", modifier = Modifier.padding(16.dp))
+ }
+ Card(onClick = { onCardClick("Card 2") }) {
+ Text("Card 2", modifier = Modifier.padding(16.dp))
+ }
+ Card(onClick = { onCardClick("Card 3") }) {
+ Text("Card 3", modifier = Modifier.padding(16.dp))
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ // Request focus on the first card.
+ initialFocus.requestFocus()
+ }
+}
+// [END android_compose_touchinput_initialfocus_basics]
+
+class InitialFocusEnablingContentReloadViewModel(
+ private val pageSize: Int = 16
+) : ViewModel() {
+
+ private val itemIndex = MutableStateFlow(0)
+
+ val cardData = itemIndex.map {
+ (it..it + pageSize).map { index ->
+ "Card $index"
+ }
+ }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
+
+ fun reload() {
+ nextPage()
+ }
+
+ private fun nextPage() {
+ itemIndex.value += pageSize
+ }
+}
+
+@Composable
+fun InitialFocusWithScrollableContainerScreen(
+ viewModel: InitialFocusEnablingContentReloadViewModel = viewModel(),
+ onCardClick: (String) -> Unit = {}
+) {
+ val cardData by viewModel.cardData.collectAsStateWithLifecycle()
+ InitialFocusWithScrollableContainer(cardData, onCardClick)
+}
+
+// [START android_compose_touchinput_initialfocus_with_scrollable_container]
+@Composable
+fun InitialFocusWithScrollableContainer(
+ cardData: List,
+ onCardClick: (String) -> Unit = {}
+) {
+ val initialFocus = remember { FocusRequester() }
+
+ // Flag to determine if it is safe to set initial focus or not.
+ var isSafeToSetInitialFocus by remember {
+ mutableStateOf(false)
+ }
+
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ contentPadding = PaddingValues(horizontal = 32.dp),
+ ) {
+ itemsIndexed(cardData) { index, item ->
+ val cardModifier = when (index) {
+ // Associate the FocusRequester object with the first card
+ 0 ->
+ Modifier
+ .focusRequester(initialFocus)
+ .onGloballyPositioned {
+ // Set the flag true if the element in the viewport
+ isSafeToSetInitialFocus = true
+ }
+
+ else -> Modifier
+ }
+ Card(
+ onClick = { onCardClick(item) },
+ modifier = cardModifier
+ ) {
+ Text(item, modifier = Modifier.padding(16.dp))
+ }
+ }
+ }
+
+ // The flag is the key to trigger the coroutine to set initial focus.
+ LaunchedEffect(isSafeToSetInitialFocus) {
+ // Your app should set initial focus only if the UI element is in viewport
+ if (isSafeToSetInitialFocus) {
+ initialFocus.requestFocus()
+ }
+ }
+}
+// [END android_compose_touchinput_initialfocus_with_scrollable_container]
+
+// [START android_compose_touchinput_initialfocus_with_content_reload]
+@Composable
+fun InitialFocusWithContentReloadScreen(
+ viewModel: InitialFocusEnablingContentReloadViewModel = viewModel(),
+ onCardClick: (String) -> Unit = {}
+) {
+ val cardData by viewModel.cardData.collectAsStateWithLifecycle()
+ val initialFocus = remember { FocusRequester() }
+ val state = rememberLazyListState()
+
+ // Recreate the flag when the cardData value changes by reloads
+ var isSafeToSetInitialFocus by remember(cardData) {
+ mutableStateOf(false)
+ }
+
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ contentPadding = PaddingValues(horizontal = 32.dp),
+ state = state,
+ ) {
+ itemsIndexed(cardData) { index, item ->
+ val cardModifier = when (index) {
+ // Associate the FocusRequester object with the first card
+ 0 ->
+ Modifier
+ .focusRequester(initialFocus)
+ .onGloballyPositioned {
+ // Set the flag true if the element in the viewport
+ isSafeToSetInitialFocus = true
+ }
+
+ else -> Modifier
+ }
+ Card(
+ onClick = { onCardClick(item) },
+ modifier = cardModifier
+ ) {
+ Text(item, modifier = Modifier.padding(16.dp))
+ }
+ }
+ item {
+ // Click to reload the content
+ Card(onClick = viewModel::reload) {
+ Text("Reload", modifier = Modifier.padding(16.dp))
+ }
+ }
+ }
+
+ // The flag is the key to trigger the coroutine to set initial focus.
+ LaunchedEffect(isSafeToSetInitialFocus) {
+ // Scroll to the first item
+ state.animateScrollToItem(0)
+ // Your app should set initial focus only if the UI element is in viewport
+ if (isSafeToSetInitialFocus) {
+ initialFocus.requestFocus()
+ }
+ }
+}
+// [END android_compose_touchinput_initialfocus_with_content_reload]
diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt
index a7f21362..2b9c4781 100644
--- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt
+++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt
@@ -27,16 +27,27 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.coroutines.flow.MutableStateFlow
-class MainActivity : ComponentActivity() {
- // Activity codes such as overridden onStart method.
+// [START android_compose_keyboard_shortcuts_helper]
+class KeyboardShortcutsHelperActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.N)
- // [START android_compose_keyboard_shortcuts_helper]
override fun onProvideKeyboardShortcuts(
data: MutableList?,
menu: Menu?,
@@ -54,10 +65,11 @@ class MainActivity : ComponentActivity() {
)
data?.add(shortcutGroup)
}
- // [END android_compose_keyboard_shortcuts_helper]
}
+// [END android_compose_keyboard_shortcuts_helper]
-class AnotherActivity : ComponentActivity() {
+
+class KeyboardShortcutsHelperRequestActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.N)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -114,3 +126,144 @@ class AnotherActivity : ComponentActivity() {
}
// [END android_compose_keyboard_shortcuts_helper_with_groups]
}
+
+private sealed class CursorMovement(val label: String) {
+
+ data object Up : CursorMovement("Up")
+ data object Down : CursorMovement("Down")
+ data object Forward : CursorMovement("Forward")
+ data object Backward : CursorMovement("Backward")
+}
+
+private class CursorMovementKeyboardShortcut(
+ val cursorMovement: CursorMovement,
+ val key: Int,
+ val modifiers: Int = 0,
+) {
+ @RequiresApi(Build.VERSION_CODES.N)
+ fun intoKeyboardShortcutInfo(): KeyboardShortcutInfo {
+ return KeyboardShortcutInfo(
+ cursorMovement.label,
+ key,
+ modifiers
+ )
+ }
+}
+
+private enum class CursorMovementStyle(
+ val label: String,
+ val shortcuts: List
+) {
+
+ Emacs(
+ "Emacs", listOf(
+ CursorMovementKeyboardShortcut(
+ CursorMovement.Up,
+ KeyEvent.KEYCODE_P,
+ KeyEvent.META_CTRL_ON
+ ),
+ CursorMovementKeyboardShortcut(
+ CursorMovement.Down,
+ KeyEvent.KEYCODE_N,
+ KeyEvent.META_CTRL_ON
+ ),
+ CursorMovementKeyboardShortcut(
+ CursorMovement.Forward,
+ KeyEvent.KEYCODE_F,
+ KeyEvent.META_CTRL_ON
+ ),
+ CursorMovementKeyboardShortcut(
+ CursorMovement.Backward,
+ KeyEvent.KEYCODE_B,
+ KeyEvent.META_CTRL_ON
+ ),
+
+ CursorMovementKeyboardShortcut(CursorMovement.Up, KeyEvent.KEYCODE_DPAD_UP),
+ CursorMovementKeyboardShortcut(CursorMovement.Down, KeyEvent.KEYCODE_DPAD_DOWN),
+ CursorMovementKeyboardShortcut(CursorMovement.Forward, KeyEvent.KEYCODE_DPAD_RIGHT),
+ CursorMovementKeyboardShortcut(CursorMovement.Backward, KeyEvent.KEYCODE_DPAD_LEFT),
+ )
+ ),
+ Vim(
+ "Vim", listOf(
+ CursorMovementKeyboardShortcut(CursorMovement.Up, KeyEvent.KEYCODE_K),
+ CursorMovementKeyboardShortcut(CursorMovement.Down, KeyEvent.KEYCODE_J),
+ CursorMovementKeyboardShortcut(CursorMovement.Forward, KeyEvent.KEYCODE_L),
+ CursorMovementKeyboardShortcut(CursorMovement.Backward, KeyEvent.KEYCODE_H),
+
+ CursorMovementKeyboardShortcut(CursorMovement.Up, KeyEvent.KEYCODE_DPAD_UP),
+ CursorMovementKeyboardShortcut(CursorMovement.Down, KeyEvent.KEYCODE_DPAD_DOWN),
+ CursorMovementKeyboardShortcut(CursorMovement.Forward, KeyEvent.KEYCODE_DPAD_RIGHT),
+ CursorMovementKeyboardShortcut(CursorMovement.Backward, KeyEvent.KEYCODE_DPAD_LEFT),
+ )
+ );
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ fun intoKeyboardShortcutGroup(): KeyboardShortcutGroup {
+ return KeyboardShortcutGroup(
+ "Cursor movement($label)",
+ shortcuts.map { it.intoKeyboardShortcutInfo() }
+ )
+ }
+}
+
+class ShortcutCustomizableKeyboardShortcutsHelperActivity : ComponentActivity() {
+ private var cursorMovement = MutableStateFlow(CursorMovementStyle.Emacs)
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun onProvideKeyboardShortcuts(
+ data: MutableList?,
+ menu: Menu?,
+ deviceId: Int
+ ) {
+ val cursorMovementSection = cursorMovement.value.intoKeyboardShortcutGroup()
+ data?.add(cursorMovementSection)
+ }
+
+ private fun updateCursorMovementStyle(style: CursorMovementStyle) {
+ cursorMovement.value = style
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ MaterialTheme {
+ val cursorMovementKeyboardShortcuts by cursorMovement.collectAsStateWithLifecycle()
+ val activity = LocalContext.current as? Activity
+
+ Box(
+ modifier = Modifier
+ .safeDrawingPadding()
+ .fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Column {
+ SingleChoiceSegmentedButtonRow {
+ CursorMovementStyle.entries.forEachIndexed { index, cursorMovementStyle ->
+ SegmentedButton(
+ selected = cursorMovementStyle == cursorMovementKeyboardShortcuts,
+ onClick = { updateCursorMovementStyle(cursorMovementStyle) },
+ label = { Text(cursorMovementStyle.label) },
+ shape = SegmentedButtonDefaults.itemShape(
+ index = index,
+ count = CursorMovementStyle.entries.size
+ )
+ )
+ }
+ }
+ Button(
+ onClick = {
+ activity?.requestShowKeyboardShortcuts()
+ }
+ ) {
+ Text(text = "Show keyboard shortcuts")
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file