diff --git a/compose/snippets/src/androidTest/java/com/example/compose/snippets/touchinput/focus/FocusTesting.kt b/compose/snippets/src/androidTest/java/com/example/compose/snippets/touchinput/focus/FocusTesting.kt new file mode 100644 index 000000000..8db61b0ee --- /dev/null +++ b/compose/snippets/src/androidTest/java/com/example/compose/snippets/touchinput/focus/FocusTesting.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2026 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.runtime.Composable +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.assertIsNotFocused +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performKeyInput +import androidx.compose.ui.test.pressKey +import androidx.compose.ui.test.withKeyDown +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +// [START android_compose_touchinput_focus_test_setup] +@RunWith(AndroidJUnit4::class) +class FocusNavigationTest { + @get:Rule + val composeTestRule = createComposeRule() +// [END android_compose_touchinput_focus_test_setup] + + // [START android_compose_touchinput_focus_test_targets] + @Test + fun interactiveElement_isFocusTarget() { + composeTestRule.setContent { + AppTheme { + CardListScreen() + } + } + + val firstCard = composeTestRule.onNodeWithTag("card_1") + val secondCard = composeTestRule.onNodeWithTag("card_2") + + // Focus the first element + firstCard.performClick() + firstCard.assertIsFocused() + secondCard.assertIsNotFocused() + } + // [END android_compose_touchinput_focus_test_targets] + + // [START android_compose_touchinput_focus_test_1d_traversal] + @Test + fun tabKey_navigatesInAppearanceOrder() { + composeTestRule.setContent { + AppTheme { + CardListScreen() + } + } + + val firstCard = composeTestRule.onNodeWithTag("card_1") + val secondCard = composeTestRule.onNodeWithTag("card_2") + val thirdCard = composeTestRule.onNodeWithTag("card_3") + + firstCard.performClick() + firstCard.assertIsFocused() + + // Press Tab -> Moves to second card + firstCard.performKeyInput { + pressKey(Key.Tab) + } + secondCard.assertIsFocused() + firstCard.assertIsNotFocused() + + // Press Tab -> Moves to third card + secondCard.performKeyInput { + pressKey(Key.Tab) + } + thirdCard.assertIsFocused() + + // Press Shift + Tab -> Moves backward to second card + thirdCard.performKeyInput { + withKeyDown(Key.ShiftLeft) { + pressKey(Key.Tab) + } + } + secondCard.assertIsFocused() + } + // [END android_compose_touchinput_focus_test_1d_traversal] + + // [START android_compose_touchinput_focus_test_2d_traversal] + @Test + fun arrowKeys_moveFocusTwoDimensionallyWithoutWrap() { + composeTestRule.setContent { + AppTheme { + GridLayoutScreen() + } + } + + val topLeftButton = composeTestRule.onNodeWithTag("btn_top_left") + val bottomLeftButton = composeTestRule.onNodeWithTag("btn_bottom_left") + + topLeftButton.performClick() + topLeftButton.assertIsFocused() + + // Down arrow moves focus downward to bottom-left button + topLeftButton.performKeyInput { + pressKey(Key.DirectionDown) + } + bottomLeftButton.assertIsFocused() + + // Directional keys do not wrap around: pressing Down on bottom element stays focused + bottomLeftButton.performKeyInput { + pressKey(Key.DirectionDown) + } + bottomLeftButton.assertIsFocused() + } + // [END android_compose_touchinput_focus_test_2d_traversal] + + // [START android_compose_touchinput_focus_test_text_fields] + @Test + fun textField_handlesTabAccordingToLineLimits() { + composeTestRule.setContent { + AppTheme { + FormScreen() + } + } + + val singleLineNode = composeTestRule.onNodeWithTag("single_line_field") + val multiLineNode = composeTestRule.onNodeWithTag("multi_line_field") + val submitButtonNode = composeTestRule.onNodeWithTag("submit_button") + + // Single-line field advances focus on Tab + singleLineNode.performClick() + singleLineNode.assertIsFocused() + singleLineNode.performKeyInput { pressKey(Key.Tab) } + multiLineNode.assertIsFocused() + + // Multi-line field keeps focus and inserts '\t' + multiLineNode.performKeyInput { pressKey(Key.Tab) } + multiLineNode.assertIsFocused() + submitButtonNode.assertIsNotFocused() + + // Shift + Tab escapes multi-line field to previous target + multiLineNode.performKeyInput { + withKeyDown(Key.ShiftLeft) { pressKey(Key.Tab) } + } + singleLineNode.assertIsFocused() + } + // [END android_compose_touchinput_focus_test_text_fields] +// [START_EXCLUDE] +} + +@Composable +private fun AppTheme(content: @Composable () -> Unit) { + content() +} + +@Composable +private fun CardListScreen() {} + +@Composable +private fun GridLayoutScreen() {} + +@Composable +private fun FormScreen() {} +// [END_EXCLUDE] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/CaptureFocus.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/CaptureFocus.kt new file mode 100644 index 000000000..23aafdd9e --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/CaptureFocus.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2026 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.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.focusRequester +import androidx.compose.ui.focus.onFocusChanged + +@Composable +private fun CaptureFocusSample() { + // [START android_compose_touchinput_focus_capture_focus] + val focusRequester = remember { FocusRequester() } + + Box( + modifier = Modifier + .focusRequester(focusRequester) + .focusable() + .onFocusChanged { focusState -> + if (focusState.isFocused) { + focusRequester.captureFocus() + } + } + ) + // [END android_compose_touchinput_focus_capture_focus] +} + +@Composable +private fun FreeFocusSample() { + val focusRequester = remember { FocusRequester() } + + // [START android_compose_touchinput_focus_free_focus] + IconButton( + onClick = { + focusRequester.freeFocus() + } + ) { + Icon(Icons.Default.Close, contentDescription = "Exit focus lock") + } + // [END android_compose_touchinput_focus_free_focus] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusGroup.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusGroup.kt new file mode 100644 index 000000000..73b7c7738 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusGroup.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2026 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.Row +import androidx.compose.material3.Button +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 + +@Composable +private fun FocusGroupOnEnterSample() { + // [START android_compose_touchinput_focus_group_on_enter] + val defaultChildRequester = remember { FocusRequester() } + + Row( + modifier = Modifier + .focusGroup() + .focusProperties { + // Intercept entry into this group and redirect focus to the primary action + onEnter = { + defaultChildRequester.requestFocus() + } + } + ) { + Button(onClick = { /* Cancel action */ }) { + Text("Cancel") + } + Button( + onClick = { /* Confirm action */ }, + modifier = Modifier.focusRequester(defaultChildRequester) + ) { + Text("Confirm (Default)") + } + } + // [END android_compose_touchinput_focus_group_on_enter] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusInTextFields.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusInTextFields.kt new file mode 100644 index 000000000..a68ce2bee --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusInTextFields.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2026 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.fillMaxWidth +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +private fun FocusInTextFieldsLineLimitsSample() { + val singleLineState = rememberTextFieldState() + val multiLineState = rememberTextFieldState() + + // [START android_compose_touchinput_focus_text_fields_line_limits] + // Single-line text field: Tab advances focus to the next focus target + BasicTextField( + state = singleLineState, + lineLimits = TextFieldLineLimits.SingleLine, + modifier = Modifier.fillMaxWidth() + ) + + // Multi-line text field: Tab inserts '\t'; Shift + Tab moves to previous target + BasicTextField( + state = multiLineState, + lineLimits = TextFieldLineLimits.MultiLine(), + modifier = Modifier.fillMaxWidth() + ) + // [END android_compose_touchinput_focus_text_fields_line_limits] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusRestoration.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusRestoration.kt new file mode 100644 index 000000000..a8e0760f5 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusRestoration.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2026 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.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +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.focus.focusRestorer +import androidx.compose.ui.unit.dp + +@Composable +private fun FocusRestorationLazySample(items: List) { + // [START android_compose_touchinput_focus_restore_lazy] + LazyColumn( + modifier = Modifier.focusRestorer() + ) { + items(items) { item -> + Button(onClick = { /* Handle click */ }) { + Text(item) + } + } + } + // [END android_compose_touchinput_focus_restore_lazy] +} + +@Composable +private fun FocusRestorationGroupSample() { + // [START android_compose_touchinput_focus_restore_group] + Row( + modifier = Modifier + .focusGroup() + .focusRestorer() + ) { + Button(onClick = { /* Action 1 */ }) { + Text("First item") + } + Button(onClick = { /* Action 2 */ }) { + Text("Second item") + } + Button(onClick = { /* Action 3 */ }) { + Text("Third item") + } + } + // [END android_compose_touchinput_focus_restore_group] +} + +@Composable +private fun FocusRestorationFallbackSample(items: List) { + // [START android_compose_touchinput_focus_restore_fallback] + val firstItemRequester = remember { FocusRequester() } + + LazyColumn( + modifier = Modifier.focusRestorer(firstItemRequester) + ) { + itemsIndexed(items) { index, item -> + val itemModifier = if (index == 0) { + Modifier.focusRequester(firstItemRequester) + } else { + Modifier + } + Button( + onClick = { /* Handle click */ }, + modifier = itemModifier + ) { + Text(item) + } + } + } + // [END android_compose_touchinput_focus_restore_fallback] +} + +@Composable +private fun FocusRestorationNavigationRailSample( + currentDestination: String, + onNavigate: (String) -> Unit, + modifier: Modifier = Modifier +) { + // [START android_compose_touchinput_focus_restore_navigation_selected] + val (homeRequester, searchRequester, settingsRequester) = remember { FocusRequester.createRefs() } + + val selectedRequester = when (currentDestination) { + "home" -> homeRequester + "search" -> searchRequester + "settings" -> settingsRequester + else -> homeRequester + } + + NavigationRail( + modifier = modifier + .focusProperties { + // Redirect focus to the selected item when focus enters the navigation rail + onEnter = { selectedRequester.requestFocus() } + } + .focusGroup() + ) { + NavigationRailItem( + selected = currentDestination == "home", + onClick = { onNavigate("home") }, + icon = { Icon(Icons.Default.Home, contentDescription = "Home") }, + label = { Text("Home") }, + modifier = Modifier.focusRequester(homeRequester) + ) + NavigationRailItem( + selected = currentDestination == "search", + onClick = { onNavigate("search") }, + icon = { Icon(Icons.Default.Search, contentDescription = "Search") }, + label = { Text("Search") }, + modifier = Modifier.focusRequester(searchRequester) + ) + NavigationRailItem( + selected = currentDestination == "settings", + onClick = { onNavigate("settings") }, + icon = { Icon(Icons.Default.Settings, contentDescription = "Settings") }, + label = { Text("Settings") }, + modifier = Modifier.focusRequester(settingsRequester) + ) + } + // [END android_compose_touchinput_focus_restore_navigation_selected] +} + +@Composable +private fun FocusRestorationDynamicListSample( + items: List, + onItemClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + // [START android_compose_touchinput_focus_restore_dynamic_list] + // Recreate FocusRequester instances when the underlying list changes + val (containerRequester, firstItemRequester) = remember(items) { FocusRequester.createRefs() } + + LazyColumn( + modifier = modifier + .focusRequester(containerRequester) + .focusProperties { + // Save the focused child when focus exits the container + onExit = { + containerRequester.saveFocusedChild() + } + // Restore the previously focused child when focus enters, or fall back to first item + onEnter = { + if (!containerRequester.restoreFocusedChild()) { + firstItemRequester.requestFocus() + } + } + } + ) { + itemsIndexed(items) { index, item -> + val itemModifier = if (index == 0) { + Modifier.focusRequester(firstItemRequester) + } else { + Modifier + } + Card( + onClick = { + // Manually save the focused child before triggering a screen transition + containerRequester.saveFocusedChild() + onItemClick(item) + }, + modifier = itemModifier + ) { + Text(text = item, modifier = Modifier.padding(16.dp)) + } + } + } + // [END android_compose_touchinput_focus_restore_dynamic_list] +} 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..647c344a6 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusTarget.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2026 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.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Card +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +private fun FocusTargetInteractiveDefaultSample() { + // [START android_compose_touchinput_focus_target] + Card(onClick = { /* First card action */ }) { + Text("Card 1 (Interactive)") + } + Card { + Text("Card 2 (Informational - not focusable)") + } + Card(onClick = { /* Third card action */ }) { + Text("Card 3 (Interactive)") + } + // [END android_compose_touchinput_focus_target] +} + +@Composable +private fun FocusTargetMakeInteractiveSample() { + // [START android_compose_touchinput_focus_target_interactive_ui_element] + Card(onClick = { /* Second card action */ }) { + Text("Card 2 (Now interactive and focusable)") + } + // [END android_compose_touchinput_focus_target_interactive_ui_element] +} + +@Composable +private fun FocusTargetClickableModifierSample() { + // [START android_compose_touchinput_focus_target_clickable_modifier] + Box( + modifier = Modifier + .clickable { /* Handle click */ } + ) { + Text("Custom interactive element") + } + // [END android_compose_touchinput_focus_target_clickable_modifier] +} + +@Composable +private fun FocusTargetFocusableModifierSample() { + // [START android_compose_touchinput_focus_focusable] + Box( + modifier = Modifier + .focusable() + ) { + Text("Non-clickable focus target") + } + // [END android_compose_touchinput_focus_focusable] +} 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..f275cb725 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusTraversal.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2026 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.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +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 + +@Composable +private fun FocusTraversalOneDimensionalSample() { + // [START android_compose_touchinput_focus_one_dimensional_traversal] + Column { + Row { + Button(onClick = { /* ... */ }) { Text("1st") } + Button(onClick = { /* ... */ }) { Text("2nd") } + } + Row { + Button(onClick = { /* ... */ }) { Text("3rd") } + Button(onClick = { /* ... */ }) { Text("4th") } + } + } + // [END android_compose_touchinput_focus_one_dimensional_traversal] +} + +@Composable +private fun FocusTraversalCustomizeOneDimensionalSample() { + // [START android_compose_touchinput_focus_customize_one_dimensional_traversal] + val (first, second, third) = remember { FocusRequester.createRefs() } + + Column { + Button( + onClick = { /* ... */ }, + modifier = Modifier + .focusRequester(first) + .focusProperties { next = third } + ) { + Text("First (Tab jumps to Third)") + } + Button( + onClick = { /* ... */ }, + modifier = Modifier.focusRequester(second) + ) { + Text("Second") + } + Button( + onClick = { /* ... */ }, + modifier = Modifier.focusRequester(third) + ) { + Text("Third") + } + } + // [END android_compose_touchinput_focus_customize_one_dimensional_traversal] +} + +@Composable +private fun FocusTraversalCustomizeTwoDimensionalSample() { + // [START android_compose_touchinput_focus_customize_two_dimensional_traversal] + val (topButton, bottomButton) = remember { FocusRequester.createRefs() } + + Button( + onClick = { /* ... */ }, + modifier = Modifier + .focusRequester(topButton) + .focusProperties { + down = bottomButton + right = bottomButton + } + ) { + Text("Top button") + } + Button( + onClick = { /* ... */ }, + modifier = Modifier.focusRequester(bottomButton) + ) { + Text("Bottom button") + } + // [END android_compose_touchinput_focus_customize_two_dimensional_traversal] +} 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..eaadce662 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusedState.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2026 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.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier + +@Composable +private fun FocusedStateRippleSample() { + // [START android_compose_touchinput_focus_ripple] + val interactionSource = remember { MutableInteractionSource() } + + Box( + modifier = Modifier + .clickable( + interactionSource = interactionSource, + indication = null + ) { /* onClick */ } + .indication( + interactionSource = interactionSource, + indication = ripple() + ) + ) { + Text("Custom component with Material ripple") + } + // [END android_compose_touchinput_focus_ripple] +} 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..3321c81a2 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/MoveFocus.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2026 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.Box +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +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 + +@Composable +private fun MoveFocusDirectionSample() { + // [START android_compose_touchinput_focus_move_direction] + val focusManager = LocalFocusManager.current + + Button( + onClick = { + focusManager.moveFocus(FocusDirection.Next) + } + ) { + Text("Next Field") + } + // [END android_compose_touchinput_focus_move_direction] +} + +@Composable +private fun ClearFocusSample() { + // [START android_compose_touchinput_focus_clear_focus] + val focusManager = LocalFocusManager.current + + Box( + modifier = Modifier.onPreviewKeyEvent { keyEvent -> + if (keyEvent.key == Key.Escape && keyEvent.type == KeyEventType.KeyUp) { + focusManager.clearFocus() + true + } else { + false + } + } + ) + // [END android_compose_touchinput_focus_clear_focus] +}