Merge "Improve focus handling using the HierarchicalFocusCoordinator" into androidx-main
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt
index dfcca4b..956835f 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/SwipeToDismissBoxTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -36,6 +37,8 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
@@ -53,6 +56,8 @@
import androidx.compose.ui.test.swipeRight
import java.lang.Math.sin
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
@@ -419,6 +424,58 @@
}
}
+ @OptIn(ExperimentalWearFoundationApi::class)
+ @Test
+ fun partial_swipe_maintains_focus() {
+ var focusedBackground by mutableStateOf(false)
+ var focusedContent by mutableStateOf(false)
+
+ rule.setContent {
+ val state = rememberSwipeToDismissBoxState()
+ SwipeToDismissBox(
+ state = state,
+ modifier = Modifier.testTag(TEST_TAG),
+ ) { isBackground ->
+ if (isBackground) {
+ val focusRequester = rememberActiveFocusRequester()
+ BasicText(
+ BACKGROUND_MESSAGE,
+ Modifier
+ .onFocusChanged { focusedBackground = it.isFocused }
+ .focusRequester(focusRequester)
+ .focusable())
+ } else {
+ val focusRequester = rememberActiveFocusRequester()
+ BasicText(
+ CONTENT_MESSAGE,
+ Modifier
+ .onFocusChanged { focusedContent = it.isFocused }
+ .focusRequester(focusRequester)
+ .focusable())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertTrue(focusedContent)
+ assertFalse(focusedBackground)
+ }
+
+ // Click down and drag across 1/4 of the screen to start a swipe,
+ // but don't release the finger, so that the screen can be inspected
+ // (note that swipeRight would release the finger and does not pause time midway).
+ rule.onNodeWithTag(TEST_TAG).performTouchInput {
+ down(Offset(x = 0f, y = height / 2f))
+ moveTo(Offset(x = width / 4f, y = height / 2f))
+ }
+
+ // We started showing the background, but focus hasn't changed.
+ rule.runOnIdle {
+ assertTrue(focusedContent)
+ assertFalse(focusedBackground)
+ }
+ }
+
private fun testBothDirectionScroll(
initialTouch: Long,
duration: Long,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt
index b776940..f4e9c56 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeToDismissBox.kt
@@ -203,13 +203,15 @@
if (!isBackground ||
(userSwipeEnabled && (state.swipeableState.offset?.roundToInt() ?: 0) > 0)
) {
- Box(contentModifier) {
- // We use the repeat loop above and call content at this location
- // for both background and foreground so that any persistence
- // within the content composable has the same call stack which is used
- // as part of the hash identity for saveable state.
- content(isBackground)
- Box(modifier = scrimModifier)
+ HierarchicalFocusCoordinator(requiresFocus = { !isBackground }) {
+ Box(contentModifier) {
+ // We use the repeat loop above and call content at this location
+ // for both background and foreground so that any persistence
+ // within the content composable has the same call stack which is used
+ // as part of the hash identity for saveable state.
+ content(isBackground)
+ Box(modifier = scrimModifier)
+ }
}
}
}
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index 02fcfac..d06de43 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -43,6 +43,7 @@
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
import androidx.wear.compose.foundation.SwipeToDismissBox
import androidx.wear.compose.foundation.SwipeToDismissBoxState
import androidx.wear.compose.foundation.SwipeToDismissKeys
@@ -54,6 +55,7 @@
import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import androidx.wear.compose.foundation.lazy.ScalingParams
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.foundation.rememberActiveFocusRequester
import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
import androidx.wear.compose.integration.demos.common.ActivityDemo
import androidx.wear.compose.integration.demos.common.ComposableDemo
@@ -133,7 +135,9 @@
) {
ScalingLazyColumnWithRSB(
horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier.fillMaxWidth().testTag(DemoListTag),
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag(DemoListTag),
) {
item {
ListHeader {
@@ -169,7 +173,9 @@
) {
Text(
text = description,
- modifier = Modifier.fillMaxWidth().align(Alignment.Center),
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.Center),
textAlign = TextAlign.Center
)
}
@@ -259,13 +265,15 @@
true
}.let {
if (focusRequester != null) {
- it.focusRequester(focusRequester)
+ it
+ .focusRequester(focusRequester)
.focusable()
} else it
}
}
}
+@OptIn(ExperimentalWearFoundationApi::class)
@Composable
fun ScalingLazyColumnWithRSB(
modifier: Modifier = Modifier,
@@ -284,7 +292,7 @@
val flingBehavior = if (snap) ScalingLazyColumnDefaults.snapFlingBehavior(
state = state
) else ScrollableDefaults.flingBehavior()
- val focusRequester = remember { FocusRequester() }
+ val focusRequester = rememberActiveFocusRequester()
ScalingLazyColumn(
modifier = modifier.rsbScroll(
scrollableState = state,
@@ -300,7 +308,4 @@
autoCentering = autoCentering,
content = content
)
- LaunchedEffect(Unit) {
- focusRequester.requestFocus()
- }
}