aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-04-24 17:50:04 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2023-04-24 17:50:04 +0000
commit1e9993d8adf910412256d7a25c8b5f7fc6010085 (patch)
tree0e66e67f60b2b6c7b9582fd1f29a1d72b8cc8d3c
parentd6e61cfa5ca9eeff9e09d56106c7e6d50f1021dd (diff)
parentecddfecd4194a37512a5081466b888af7b3c1a71 (diff)
downloadsupport-sparse-9949164-L22700000960137459.tar.gz
Merge "Merge cherrypicks of ['android-review.googlesource.com/2520700', 'android-review.googlesource.com/2557491', 'android-review.googlesource.com/2558170', 'android-review.googlesource.com/2544354', 'android-review.googlesource.com/2555513'] into sparse-9949164-L22700000960137459. SPARSE_CHANGE: Ia77fccb794b016d699a4b6ffef8e4fe690fc4766 SPARSE_CHANGE: I7715190081686b2b01010b35375633cfc1c6ca3b SPARSE_CHANGE: I2436c2a6b585e0063ad87d4852404f7fcc0cb8f0 SPARSE_CHANGE: I4dc771beb990020ea91d6fbf83de71145a4b6382 SPARSE_CHANGE: I28392927e6d2263ca0019b40b898d06797096041" into sparse-9949164-L22700000960137459sparse-9949164-L22700000960137459
-rw-r--r--compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt1
-rw-r--r--compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt2
-rw-r--r--compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt66
-rw-r--r--compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterTest.kt485
-rw-r--r--compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusExitTest.kt92
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt57
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt5
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt2
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt4
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt16
-rw-r--r--libraryversions.toml2
-rw-r--r--tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt2
12 files changed, 716 insertions, 18 deletions
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 202e7f463d4..12537a7ee43 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -111,6 +111,7 @@ class VersionChecker(val context: IrPluginContext) {
9702 to "1.4.0",
9703 to "1.4.1",
9704 to "1.4.2",
+ 9705 to "1.4.3",
)
/**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index fa396e40b90..536c711e3a7 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@ internal object ComposeVersion {
* IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
* `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
*/
- const val version: Int = 9704
+ const val version: Int = 9705
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
index 74a02fcf12e..6ceb34fb0ee 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchPreviousTest.kt
@@ -275,6 +275,72 @@ class OneDimensionalFocusSearchPreviousTest {
}
@Test
+ fun focusMovesToParent() {
+ // Arrange.
+ val (parent, child1, child2, child3) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ FocusableBox(parent, 0, 0, 10, 10) {
+ FocusableBox(child1, 10, 0, 10, 10, initialFocus)
+ FocusableBox(child2, 20, 0, 10, 10)
+ FocusableBox(child3, 20, 0, 10, 10)
+ }
+ }
+
+ // Act.
+ val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(movedFocusSuccessfully).isTrue()
+ assertThat(parent.value).isTrue()
+ }
+ }
+
+ @Test
+ fun focusMovesToParent_ignoresDeactivated() {
+ // Arrange.
+ val (item, parent, child1, child2) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ FocusableBox(item, 0, 0, 10, 10)
+ FocusableBox(parent, 0, 0, 10, 10, deactivated = true) {
+ FocusableBox(child1, 10, 0, 10, 10, initialFocus)
+ FocusableBox(child2, 20, 0, 10, 10)
+ }
+ }
+
+ // Act.
+ val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(movedFocusSuccessfully).isTrue()
+ assertThat(item.value).isTrue()
+ }
+ }
+
+ @Test
+ fun focusMovesToParent_ignoresDeactivated_andWrapsAround() {
+ // Arrange.
+ val (parent, child1, child2, child3) = List(4) { mutableStateOf(false) }
+ rule.setContentForTest {
+ FocusableBox(parent, 0, 0, 10, 10, deactivated = true) {
+ FocusableBox(child1, 10, 0, 10, 10, initialFocus)
+ FocusableBox(child2, 20, 0, 10, 10)
+ FocusableBox(child3, 0, 0, 10, 10)
+ }
+ }
+
+ // Act.
+ val movedFocusSuccessfully = rule.runOnIdle { focusManager.moveFocus(Previous) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(movedFocusSuccessfully).isTrue()
+ assertThat(child3.value).isTrue()
+ }
+ }
+
+ @Test
fun focusWrapsAroundToLastItem() {
// Arrange.
val (item1, item2, item3) = List(3) { mutableStateOf(false) }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterTest.kt
new file mode 100644
index 00000000000..206c37eb4cf
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusEnterTest.kt
@@ -0,0 +1,485 @@
+<<<<<<< HEAD (182910 Merge "Fix 2D focus search issue" into snap-temp-L6200000096)
+=======
+/*
+ * Copyright 2023 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
+ *
+ * http://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 androidx.compose.ui.focus
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
+import androidx.compose.ui.focus.FocusRequester.Companion.Default
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalComposeUiApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RequestFocusEnterTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val focusRequester = FocusRequester()
+ private var enterTriggered = false
+ private lateinit var focusState: FocusState
+
+ @Test
+ fun gainingFocus_doesNotTriggersEnter() {
+ // Arrange.
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .focusProperties {
+ enter = { enterTriggered = true; Default }
+ }
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(enterTriggered).isFalse()
+ assertThat(focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun gainingFocus_doesNotTriggersEnterForChild() {
+ // Arrange.
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusProperties {
+ enter = { enterTriggered = true; Default }
+ }
+ .focusTarget()
+ )
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(enterTriggered).isFalse()
+ assertThat(focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun gainingFocus_triggersEnterForParent() {
+ // Arrange.
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusProperties {
+ enter = { enterTriggered = true; Default }
+ }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(enterTriggered).isTrue()
+ assertThat(focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun gainingFocus_triggersEnterForGrandparent() {
+ // Arrange.
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusProperties {
+ enter = { enterTriggered = true; Default }
+ }
+ .focusTarget()
+ ) {
+ Box {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(enterTriggered).isTrue()
+ assertThat(focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun cancellingFocusGain_usingEnterProperty() {
+ // Arrange.
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .focusProperties { enter = { Cancel } }
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun cancellingFocusGain_usingEnterPropertyOnChild() {
+ // Arrange.
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ ) {
+ Box(Modifier.focusProperties { enter = { Cancel } })
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun cancellingFocusGain_usingEnterPropertyOnParent() {
+ // Arrange.
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusProperties { enter = { Cancel } }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ @Test
+ fun cancellingFocusGain_usingEnterPropertyOnGrandparent() {
+ // Arrange.
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusProperties { enter = { Cancel } }
+ .focusTarget()
+ ) {
+ Box {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ @Test
+ fun cancellingFocusGain_usingACustomDestination() {
+ // Arrange.
+ val customDestination = FocusRequester()
+ rule.setFocusableContent {
+ Box(Modifier.focusTarget()) {
+ Box(
+ Modifier
+ .focusProperties { enter = { customDestination } }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .focusTarget()
+ )
+ Box(Modifier.focusTarget())
+ }
+ Box(Modifier.focusTarget()) {
+ Box(
+ Modifier
+ .focusRequester(customDestination)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun cancellingFocusGain_usingAChildAsACustomDestination() {
+ // Arrange.
+ val customDestination = FocusRequester()
+ lateinit var destinationFocusState: FocusState
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusProperties { enter = { customDestination } }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .focusRequester(customDestination)
+ .onFocusChanged { destinationFocusState = it }
+ .focusTarget()
+ )
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(destinationFocusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun cancellingFocusGain_usingASiblingAsACustomDestination() {
+ // Arrange.
+ val customDestination = FocusRequester()
+ lateinit var destinationFocusState: FocusState
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusProperties { enter = { customDestination } }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ Box(
+ Modifier
+ .focusRequester(customDestination)
+ .onFocusChanged { destinationFocusState = it }
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(destinationFocusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun cancellingFocusGain_usingParentAsACustomDestination() {
+ // Arrange.
+ val customDestination = FocusRequester()
+ lateinit var destinationFocusState: FocusState
+ rule.setFocusableContent {
+ Box(
+ Modifier
+ .focusRequester(customDestination)
+ .focusProperties { enter = { customDestination } }
+ .onFocusChanged { destinationFocusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(destinationFocusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun redirectingFocusRequestOnChild1ToChild2_focusEnterIsCalled() {
+ // Arrange.
+ val (initialFocus, child1, child2) = FocusRequester.createRefs()
+ var enterCount = 0
+ rule.setFocusableContent {
+ Box(Modifier.focusTarget()) {
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .focusProperties {
+ enter = {
+ enterCount++
+ child2
+ }
+ }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(child1)
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .focusRequester(child2)
+ .focusTarget()
+ )
+ }
+ }
+ }
+ rule.runOnIdle { initialFocus.requestFocus() }
+
+ // Act.
+ rule.runOnIdle { child1.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(enterCount).isEqualTo(1) }
+
+ // Reset - To ensure that focus enter is called every time we enter.
+ rule.runOnIdle {
+ initialFocus.requestFocus()
+ enterCount = 0
+ }
+
+ // Act.
+ rule.runOnIdle { child1.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(enterCount).isEqualTo(1) }
+ }
+}
+>>>>>>> CHANGE (7a047f Fix Regression in custom enter/exit focus properties)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusExitTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusExitTest.kt
new file mode 100644
index 00000000000..834b2a05168
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusExitTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 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
+ *
+ * http://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 androidx.compose.ui.focus
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalComposeUiApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RequestFocusExitTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun redirectingFocusExitFromChild1ToChild2_focusExitIsCalled() {
+ // Arrange.
+ val (destination, child1, child2) = FocusRequester.createRefs()
+ var exitCount = 0
+ rule.setFocusableContent {
+ Box(Modifier.focusTarget()) {
+ Box(
+ Modifier
+ .focusRequester(destination)
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .focusProperties {
+ exit = {
+ exitCount++
+ child2
+ }
+ }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(child1)
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .focusRequester(child2)
+ .focusTarget()
+ )
+ }
+ }
+ }
+ rule.runOnIdle { child1.requestFocus() }
+
+ // Act.
+ rule.runOnIdle { destination.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(exitCount).isEqualTo(1) }
+
+ // Reset - To ensure that focus exit is called every time we exit.
+ rule.runOnIdle {
+ child1.requestFocus()
+ exitCount = 0
+ }
+
+ // Act.
+ rule.runOnIdle { destination.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(exitCount).isEqualTo(1) }
+ }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
index 57ec4b00b61..27dc28bc545 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
@@ -93,6 +93,63 @@ class FocusTargetModifierNode : ObserverNode, ModifierLocalNode, Modifier.Node()
return properties
}
+<<<<<<< HEAD (182910 Merge "Fix 2D focus search issue" into snap-temp-L6200000096)
+=======
+ /**
+ * Fetch custom enter destination associated with this [focusTarget].
+ *
+ * Custom focus enter properties are specified as a lambda. If the user runs code in this
+ * lambda that triggers a focus search, or some other focus change that causes focus to leave
+ * the sub-hierarchy associated with this node, we could end up in a loop as that operation
+ * will trigger another invocation of the lambda associated with the focus exit property.
+ * This function prevents that re-entrant scenario by ensuring there is only one concurrent
+ * invocation of this lambda.
+ */
+ @OptIn(ExperimentalComposeUiApi::class)
+ internal inline fun fetchCustomEnter(
+ focusDirection: FocusDirection,
+ block: (FocusRequester) -> Unit
+ ) {
+ if (!isProcessingCustomEnter) {
+ isProcessingCustomEnter = true
+ try {
+ fetchFocusProperties().enter(focusDirection).also {
+ if (it !== Default) block(it)
+ }
+ } finally {
+ isProcessingCustomEnter = false
+ }
+ }
+ }
+
+ /**
+ * Fetch custom exit destination associated with this [focusTarget].
+ *
+ * Custom focus exit properties are specified as a lambda. If the user runs code in this
+ * lambda that triggers a focus search, or some other focus change that causes focus to leave
+ * the sub-hierarchy associated with this node, we could end up in a loop as that operation
+ * will trigger another invocation of the lambda associated with the focus exit property.
+ * This function prevents that re-entrant scenario by ensuring there is only one concurrent
+ * invocation of this lambda.
+ */
+ @OptIn(ExperimentalComposeUiApi::class)
+ internal inline fun fetchCustomExit(
+ focusDirection: FocusDirection,
+ block: (FocusRequester) -> Unit
+ ) {
+ if (!isProcessingCustomExit) {
+ isProcessingCustomExit = true
+ try {
+ fetchFocusProperties().exit(focusDirection).also {
+ if (it !== Default) block(it)
+ }
+ } finally {
+ isProcessingCustomExit = false
+ }
+ }
+ }
+
+>>>>>>> CHANGE (7a047f Fix Regression in custom enter/exit focus properties)
internal fun invalidateFocus() {
when (focusState) {
// Clear focus from the current FocusTarget.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
index 70b237cffa6..d19c5431826 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
@@ -71,10 +71,9 @@ private fun FocusTargetModifierNode.backwardFocusSearch(
// Unlike forwardFocusSearch, backwardFocusSearch visits the children before the parent.
when (focusedChild.focusStateImpl) {
- ActiveParent ->
- focusedChild.backwardFocusSearch(onFound) ||
+ ActiveParent -> focusedChild.backwardFocusSearch(onFound) ||
generateAndSearchChildren(focusedChild, Previous, onFound) ||
- (fetchFocusProperties().canFocus && onFound.invoke(focusedChild))
+ (focusedChild.fetchFocusProperties().canFocus && onFound.invoke(focusedChild))
// Since this item "is focused", it means we already visited all its children.
// So just search among its siblings.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
index ad9109fac66..dc0befb5284 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
@@ -212,6 +212,8 @@ private fun DelegatableNode.collectAccessibleChildren(
accessibleChildren: MutableVector<FocusTargetModifierNode>
) {
visitSubtreeIf(Nodes.FocusTarget) {
+ // TODO(b/278765590): Find the root issue why visitChildren returns unattached nodes.
+ if (!it.isAttached) return@visitSubtreeIf false
if (it.fetchFocusProperties().canFocus) {
accessibleChildren.add(it)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 46ae3ed096c..0c7faa9b8aa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -411,7 +411,7 @@ internal class LayoutNode(
mLookaheadScope =
parent?.mLookaheadScope ?: if (isLookaheadRoot) LookaheadScope(this) else null
- nodes.attach(performInvalidations = false)
+ nodes.attach()
_foldedChildren.forEach { child ->
child.attach(owner)
}
@@ -1379,7 +1379,7 @@ internal class LayoutNode(
resetModifierState()
}
// resetModifierState detaches all nodes, so we need to re-attach them upon reuse.
- nodes.attach(true)
+ nodes.attach()
}
override fun onDeactivate() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index e6eeda92dc1..5684882ca77 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -213,7 +213,7 @@ internal class NodeChain(val layoutNode: LayoutNode) {
syncCoordinators()
}
if (attachNeeded && layoutNode.isAttached) {
- attach(performInvalidations = true)
+ attach()
}
}
@@ -264,17 +264,15 @@ internal class NodeChain(val layoutNode: LayoutNode) {
outerCoordinator = coordinator
}
- fun attach(performInvalidations: Boolean) {
+ fun attach() {
headToTail {
if (!it.isAttached) {
it.attach()
- if (performInvalidations) {
- if (it.insertedNodeAwaitingAttachForInvalidation) {
- autoInvalidateInsertedNode(it)
- }
- if (it.updatedNodeAwaitingAttachForInvalidation) {
- autoInvalidateUpdatedNode(it)
- }
+ if (it.insertedNodeAwaitingAttachForInvalidation) {
+ autoInvalidateInsertedNode(it)
+ }
+ if (it.updatedNodeAwaitingAttachForInvalidation) {
+ autoInvalidateUpdatedNode(it)
}
// when we attach with performInvalidations == false no separate
// invalidations needed as the whole LayoutNode is attached to the tree.
diff --git a/libraryversions.toml b/libraryversions.toml
index 0b6c8036395..cc31d626832 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -22,7 +22,7 @@ CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.4.0-alpha01"
COLLECTION = "1.3.0-alpha03"
COLLECTION_KMP = "1.3.0-dev01"
-COMPOSE = "1.4.2"
+COMPOSE = "1.4.3"
COMPOSE_COMPILER = "1.4.3"
COMPOSE_MATERIAL3 = "1.1.0-alpha07"
COMPOSE_RUNTIME_TRACING = "1.0.0-alpha03"
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
index cec085f3880..51f87fd09fb 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
@@ -84,7 +84,6 @@ import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.CountDownLatch
import kotlin.math.roundToInt
import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -813,7 +812,6 @@ class LazyListTest(orientation: Orientation) : BaseLazyListTestWithOrientation(o
}
}
- @Ignore // b/268211857
@Test
fun scroll_makeListSmaller_scroll() {
var items by mutableStateOf((1..100).toList())