diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt b/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt index 48f2a1109..6cd6d6d09 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt @@ -22,9 +22,11 @@ import android.os.StrictMode import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -60,7 +62,15 @@ import com.example.compose.snippets.images.ImageExamplesScreen import com.example.compose.snippets.landing.LandingScreen import com.example.compose.snippets.layouts.PagerExamples import com.example.compose.snippets.navigation.Destination +import com.example.compose.snippets.navigation.FocusExamplesDestination import com.example.compose.snippets.navigation.TopComponentsDestination +import com.example.compose.snippets.touchinput.focus.FocusExamples +import com.example.compose.snippets.touchinput.focus.FocusTargetExamples +import com.example.compose.snippets.touchinput.focus.FocusedStateExamples +import com.example.compose.snippets.touchinput.focus.MoveFocusExamples +import com.example.compose.snippets.touchinput.focus.OneDimensionalFocusTraversalExamples +import com.example.compose.snippets.touchinput.focus.RequestFocusExamples +import com.example.compose.snippets.touchinput.focus.TwoDimensionalFocusTraversalExamples import com.example.compose.snippets.ui.theme.SnippetsTheme class SnippetsActivity : ComponentActivity() { @@ -94,6 +104,9 @@ class SnippetsActivity : ComponentActivity() { Destination.ShapesExamples -> ApplyPolygonAsClipImage() Destination.SharedElementExamples -> PlaceholderSizeAnimated_Demo() Destination.PagerExamples -> PagerExamples() + Destination.FocusExamples -> FocusExamples() { + navController.navigate(it.route) + } } } } @@ -128,6 +141,34 @@ class SnippetsActivity : ComponentActivity() { } } } + FocusExamplesDestination.entries.forEach { destination -> + composable(destination.route) { + when (destination) { + FocusExamplesDestination.FocusTarget -> { + FocusTargetExamples(modifier = Modifier.padding(32.dp)) + } + FocusExamplesDestination.OneDimensionalFocusTraversal -> { + OneDimensionalFocusTraversalExamples( + modifier = Modifier.padding(32.dp) + ) + } + FocusExamplesDestination.TwoDimensionalFocusTraversal -> { + TwoDimensionalFocusTraversalExamples( + modifier = Modifier.padding(32.dp) + ) + } + FocusExamplesDestination.RequestFocus -> { + RequestFocusExamples(modifier = Modifier.padding(32.dp)) + } + FocusExamplesDestination.FocusedState -> { + FocusedStateExamples(modifier = Modifier.padding(32.dp)) + } + FocusExamplesDestination.MoveFocus -> { + MoveFocusExamples(modifier = Modifier.padding(32.dp)) + } + } + } + } } } } 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 8d86b28b4..8dfb68f07 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"), + FocusExamples("focusExamples", "Focus Examples") } // Enum class for compose components navigation screen. @@ -53,3 +54,13 @@ enum class TopComponentsDestination(val route: String, val title: String) { SwipeToDismissBoxExamples("swipeToDismissBoxExamples", "Swipe to dismiss box examples"), SearchBarExamples("searchBarExamples", "Search bar") } + +// Enum class for focus examples navigation screen. +enum class FocusExamplesDestination(val route: String, val title: String) { + FocusTarget("focusTarget", "Focus target"), + OneDimensionalFocusTraversal("focusTraversal", "Focus traversal: 1D"), + TwoDimensionalFocusTraversal("focusTraversal2D", "Focus traversal 2D"), + RequestFocus("requestFocus", "Request focus"), + FocusedState("focusableState", "Focusable state"), + MoveFocus("moveFocus", "Move focus"), +} 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 2fce7f0aa..71a2ba489 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 @@ -25,15 +25,20 @@ import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn 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.lazy.items import androidx.compose.material3.Button import androidx.compose.material3.Card +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -52,10 +57,13 @@ import androidx.compose.ui.focus.FocusDirection.Companion.Right import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester.Companion.Cancel import androidx.compose.ui.focus.FocusRequester.Companion.Default +import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1 +import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2 +import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component3 +import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component4 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 @@ -70,10 +78,68 @@ import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.invalidateDraw 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 com.example.compose.snippets.navigation.FocusExamplesDestination import kotlinx.coroutines.launch +@Composable +fun FocusExamples( + modifier: Modifier = Modifier, + onNavigation: (FocusExamplesDestination) -> Unit = {} +) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { + items(FocusExamplesDestination.entries.toList()) { + ListItem( + modifier = Modifier.clickable(onClick = { + onNavigation(it) + }), + headlineContent = { + Text(it.title) + } + ) + } + } +} + +@Composable +internal fun Section( + title: String, + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(16.dp), + style: TextStyle = MaterialTheme.typography.headlineMedium, + content: @Composable () -> Unit = {} +) { + Column( + modifier = modifier.focusGroup(), + verticalArrangement = verticalArrangement + ) { + Text(title, style = style) + content() + } +} + +@Composable +internal fun SubSection( + title: String, + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp), + style: TextStyle = MaterialTheme.typography.headlineSmall, + content: @Composable () -> Unit = {} +) { + Section( + title = title, + modifier = modifier, + verticalArrangement = verticalArrangement, + style = style, + content = content, + ) +} + @Preview @Composable private fun BasicSample() { @@ -276,7 +342,7 @@ private fun RequestFocus() { } @Composable -private fun RequestFocus2() { +internal fun RequestFocus2() { // [START android_compose_touchinput_focus_request2] val focusRequester = remember { FocusRequester() } var text by remember { mutableStateOf("") } @@ -382,7 +448,7 @@ private fun ModifierOrder2() { @OptIn(ExperimentalComposeUiApi::class) @Composable -private fun RedirectFocus() { +private fun FocusRedirection() { // [START android_compose_touchinput_focus_redirect] val otherComposable = remember { FocusRequester() } @@ -399,7 +465,7 @@ private fun RedirectFocus() { } @Composable -private fun FocusAdvancing() { +internal fun FocusAdvancing() { // [START android_compose_touchinput_focus_advancing] val focusManager = LocalFocusManager.current var text by remember { mutableStateOf("") } @@ -424,7 +490,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 { @@ -460,7 +526,7 @@ private class MyHighlightIndicationNode(private val interactionSource: Interacti override fun ContentDrawScope.draw() { drawContent() if (isFocused) { - drawRect(size = size, color = Color.White, alpha = 0.2f) + drawRect(size = size, color = White, alpha = 0.2f) } } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusTarget.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusTarget.kt new file mode 100644 index 000000000..55ea1245a --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusTarget.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2025 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.focusGroup +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.unit.dp + +@Composable +internal fun FocusTargetExamples( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Section("Focus target", modifier = modifier) { + Row( + horizontalArrangement = Arrangement.spacedBy(32.dp), + ) { + FocusTarget( + onClick = onClick, + modifier = Modifier.focusGroup() + ) + InteractiveUiElementIsFocusTargets( + onClick = onClick, + modifier = Modifier.focusGroup() + ) + FocusTargetWithClickableModifier( + onClick = onClick, + modifier = Modifier.focusGroup() + ) + } + SubSection("Modifier precedence") { + ModifierPrecedence( + onClick = onClick, + modifier = Modifier.focusGroup() + ) + } + } +} + +@Composable +fun FocusTarget( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + // [START android_compose_touchinput_focus_target] + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier, + ) { + Card(onClick = onClick) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + Card { + Text(text = "2nd card", modifier = Modifier.padding(32.dp)) + } + Card(onClick = onClick) { + Text(text = "3rd card", modifier = Modifier.padding(32.dp)) + } + } + // [END android_compose_touchinput_focus_target] +} + +@Composable +fun InteractiveUiElementIsFocusTargets( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + // [START android_compose_touchinput_focus_target_interactive_ui_element] + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier, + ) { + Card(onClick = onClick) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + Card(onClick = onClick) { + Text(text = "2nd card", modifier = Modifier.padding(32.dp)) + } + Card(onClick = onClick) { + Text(text = "3rd card", modifier = Modifier.padding(32.dp)) + } + } + // [END android_compose_touchinput_focus_target_interactive_ui_element] +} + +@Composable +fun FocusTargetWithClickableModifier( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier, + ) { + Card(onClick = onClick) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + // [START android_compose_touchinput_focus_target_interactive_ui_element] + Box(modifier = Modifier.clickable(onClick = onClick)) { + Text(text = "1st box", modifier = Modifier.padding(32.dp)) + } + // [END android_compose_touchinput_focus_target_interactive_ui_element] + Card(onClick = onClick) { + Text(text = "3rd card", modifier = Modifier.padding(32.dp)) + } + } +} + +@Composable +fun ModifierPrecedence( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Box( + modifier = modifier + ) { + // [START android_compose_touchinput_focus_modifier_precedence] + Card( + onClick = onClick, + modifier = Modifier + .focusProperties { + canFocus = false + } + .focusProperties { + canFocus = true + } + ) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + // [END android_compose_touchinput_focus_modifier_precedence] + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusTraversal.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusTraversal.kt new file mode 100644 index 000000000..0f6266c51 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusTraversal.kt @@ -0,0 +1,399 @@ +/* + * Copyright 2025 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.background +import androidx.compose.foundation.focusGroup +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +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.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.focusProperties +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.unit.dp + +@Composable +internal fun OneDimensionalFocusTraversalExamples( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Section("One-dimensional focus traversal", modifier = modifier) { + SubSection("Focus traversal example") { + OneDimensionalFocusTraversal( + modifier = Modifier.focusGroup(), + onClick = onClick, + ) + } + SubSection("Z-shaped style") { + OneDimensionalFocusTraversalInZShape( + modifier = Modifier.focusGroup(), + onClick = onClick, + ) + } + SubSection("Override") { + OneDimensionalFocusTraversalOverride( + modifier = Modifier.focusGroup(), + onClick = onClick, + ) + } + } +} + +@Composable +internal fun TwoDimensionalFocusTraversalExamples( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Section("Two-dimensional focus traversal", modifier = modifier) { + TwoDimensionalFocusTraversal( + modifier = Modifier.focusGroup(), + onClick = onClick, + ) + SubSection("Override") { + TwoDimensionalFocusTraversalOverride( + modifier = Modifier.focusGroup(), + onClick = onClick, + ) + } + SubSection("Focus group") { + TwoDimensionalFocusTraversalWithFocusGroup( + modifier = Modifier.focusGroup(), + onClick = onClick, + ) + } + SubSection("On enter callback") { + OnEnterCallback( + modifier = Modifier.focusGroup(), + onClick = onClick, + ) + } + } +} + +@Composable +fun OneDimensionalFocusTraversal( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + // [START android_compose_touchinput_focus_one_dimensional_traversal] + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + // Focus target + Card( + onClick = onClick, + modifier = Modifier + .width(160.dp) + .offset(x = 176.dp) + ) { + Text(text = "4th card", modifier = Modifier.padding(32.dp)) + } + // Focus target + Card( + onClick = onClick, + modifier = Modifier + .width(160.dp) + .offset(y = -(104.dp)) + ) { + Text( + text = "3rd card", + modifier = Modifier.padding(32.dp) + ) + } + } + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "2nd card", modifier = Modifier.padding(32.dp)) + } + } + // [END android_compose_touchinput_focus_one_dimensional_traversal] +} + +@Composable +fun OneDimensionalFocusTraversalInZShape( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + // [START android_compose_touchinput_focus_one_dimensional_traversal_in_z_shape] + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "2nd card", modifier = Modifier.padding(32.dp)) + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "3rd card", modifier = Modifier.padding(32.dp)) + } + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "4th card", modifier = Modifier.padding(32.dp)) + } + } + } + // [END android_compose_touchinput_focus_one_dimensional_traversal_in_z_shape] +} + +@Composable +fun OneDimensionalFocusTraversalOverride( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + // [START android_compose_touchinput_focus_one_dimensional_traversal_override] + val (first, second, third) = remember { FocusRequester.createRefs() } + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Focus target + Card( + onClick = onClick, + modifier = Modifier + .focusRequester(first) + .focusProperties { + next = second // Set focus move to 2nd card with the Tab key + } + + ) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + // Focus target + Card( + onClick = onClick, + modifier = Modifier + .focusRequester(third) + .focusProperties { + next = first // Set focus move to 1st card with the Tab key + } + ) { + Text(text = "3rd card", modifier = Modifier.padding(32.dp)) + } + } + // Focus target + Card( + onClick = onClick, + modifier = + Modifier + .focusRequester(second) + .focusProperties { + next = third // Set focus move to third card with the Tab key + } + ) { + Text(text = "2nd card", modifier = Modifier.padding(32.dp)) + } + } + // [END android_compose_touchinput_focus_one_dimensional_traversal_override] +} + +@Composable +fun TwoDimensionalFocusTraversal( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + // [START android_compose_touchinput_focus_two_dimensional_traversal] + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "2nd card", modifier = Modifier.padding(32.dp)) + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "3rd card", modifier = Modifier.padding(32.dp)) + } + Spacer(modifier = Modifier.width(160.dp)) // This is NOT a focus target + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "4th card", modifier = Modifier.padding(32.dp)) + } + } + } + // [END android_compose_touchinput_focus_two_dimensional_traversal] +} + +@Composable +fun TwoDimensionalFocusTraversalOverride( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + // [START android_compose_touchinput_focus_two_dimensional_traversal_override] + val firstCard = remember { FocusRequester() } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + // Focus target + Card( + onClick = onClick, + modifier = Modifier + .width(160.dp) + .focusRequester(firstCard) // Associate with a FocusRequester object + ) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + // Focus target + Card( + onClick = onClick, + modifier = Modifier + .width(160.dp) + .focusProperties { + right = firstCard // Set focus move to 1st card + } + ) { + Text(text = "2nd card", modifier = Modifier.padding(32.dp)) + } + } + // [END android_compose_touchinput_focus_two_dimensional_traversal_override] +} + +@Composable +fun TwoDimensionalFocusTraversalWithFocusGroup( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + // [START android_compose_touchinput_focus_two_dimensional_traversal_with_focus_group] + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.focusGroup() + ) { + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "2nd card", modifier = Modifier.padding(32.dp)) + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.focusGroup() + ) { + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "3rd card", modifier = Modifier.padding(32.dp)) + } + Spacer(modifier = Modifier.width(160.dp)) // This is NOT a focus target + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "4th card", modifier = Modifier.padding(32.dp)) + } + } + } + // [END android_compose_touchinput_focus_two_dimensional_traversal_with_focus_group] +} + +@Composable +fun OnEnterCallback( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + // [START android_compose_touchinput_focus_on_enter_callback] + var isInGroup by remember { mutableStateOf(false) } + val backgroundColor = if (isInGroup) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.background + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .focusProperties { + onEnter = { + isInGroup = true + } + onExit = { + isInGroup = false + } + } + .focusGroup() + .background(backgroundColor) + ) { + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "2nd card", modifier = Modifier.padding(32.dp)) + } + // Focus target + Card(onClick = onClick, modifier = Modifier.width(160.dp)) { + Text(text = "3rd card", modifier = Modifier.padding(32.dp)) + } + } + // [END android_compose_touchinput_focus_on_enter_callback] + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusedState.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusedState.kt new file mode 100644 index 000000000..c4222c828 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusedState.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2025 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.focusGroup +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ripple +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalRippleConfiguration +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +internal fun FocusedStateExamples( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Section("Focus state", modifier = modifier) { + ApplyRipple( + modifier = Modifier.focusGroup(), + onClick = onClick + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ApplyRipple( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier, + ) { + // [START android_compose_touchinput_focus_ripple] + val interactionSource = remember { MutableInteractionSource() } + Box( + modifier = Modifier.clickable( + onClick = onClick, + interactionSource = interactionSource, + indication = ripple() + ) + ) { + Text("With focused state", modifier = Modifier.padding(32.dp)) + } + // [END android_compose_touchinput_focus_ripple] + CompositionLocalProvider( + LocalRippleConfiguration provides null + ) { + Box( + modifier = Modifier.clickable(onClick = onClick) + ) { + Text("Without focused state", modifier = Modifier.padding(32.dp)) + } + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/MoveFocus.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/MoveFocus.kt new file mode 100644 index 000000000..e621a337e --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/MoveFocus.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2025 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.focusGroup +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +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.unit.dp + +@Composable +internal fun MoveFocusExamples( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Section("Move focus", modifier = modifier) { + FocusAdvancing() + SubSection("Clear focus with ESC") { + ClearFocusWithEscKey( + onClick = onClick, + modifier = Modifier.focusGroup(), + ) + } + } +} + +@Composable +fun ClearFocusWithEscKey( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + // [START android_compose_touchinput_clear_focus_esc] + val focusManager = LocalFocusManager.current + Row( + modifier = modifier.onPreviewKeyEvent { + if (it.key == Key.Escape && it.type == KeyEventType.KeyDown) { + focusManager.clearFocus() + true + } else { + false + } + }, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + var inputText by rememberSaveable { mutableStateOf("") } + TextField( + value = inputText, + onValueChange = { inputText = it }, + ) + Button(onClick = onClick) { + Text("Send") + } + } + // [END android_compose_touchinput_clear_focus_esc] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/RequestFocus.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/RequestFocus.kt new file mode 100644 index 000000000..1f81cf026 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/RequestFocus.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2025 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.focusGroup +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +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.unit.dp + +@Composable +fun RequestFocusExamples( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Section("Request focus", modifier = modifier) { + RequestFocus2() + SubSection("Focus redirection") { + FocusRedirection( + onClick = onClick, + modifier = Modifier.focusGroup(), + ) + } + } +} + +@Composable +fun FocusRedirection( + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + val thirdCard = remember { FocusRequester() } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // [START android_compose_touchinput_focus_redirection] + Card(onClick = onClick) { + Text(text = "1st card", modifier = Modifier.padding(32.dp)) + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .focusProperties { + onEnter = { + thirdCard.requestFocus() + } + } + .focusGroup() + ) { + Card(onClick = onClick) { + Text(text = "2nd card", modifier = Modifier.padding(32.dp)) + } + Card(onClick = onClick, modifier = Modifier.focusRequester(thirdCard)) { + Text(text = "3rd card", modifier = Modifier.padding(32.dp)) + } + Card(onClick = onClick) { + Text(text = "4th card", modifier = Modifier.padding(32.dp)) + } + } + // [END android_compose_touchinput_focus_redirection] + } +}