Migrate focus modifiers to Modifier.Node
This CL is a result of squashing all the commits
from the topic:
https://android-review.googlesource.com/q/topic:%22Migrate+focus+to+Modifer.Node%22
Here are some highlights of this CL
- FocusModifier is replaced by FocusTargetNode
- FocusProperties are implemented using Modifier.Node
- FocusEvents are powered by a backing FocusEventNode
- FocusRequesters find the focusTarget using the new
Modifier.Node APIs
- KeyInputEvents and RotaryScrollEvents are routed using
a common FocusAwareInputNode
- We use the Modifier.Node APIs to find the current value of
The BeyondBoundsLayout modifier local that is needed when
focus search is traversing through lazy lists.
Bug: 247708726
Bug: 255352203
Bug: 253043481
Bug: 247716483
Bug: 247708726
Bug: 254529934
Bug: 251840112
Bug: 251859987
Bug: 257141589
Test: ./gradlew compose:ui:ui:cC -P android.testInstrumentationRunnerArguments.package=androidx.compose.ui.focus
Relnote: FocusRequesterModifier is deprecated in favor of FocusRequesterNode
Change-Id: I7f4d7a99aa42f7f3e4f49d034f8358a41ed42d0f
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index c3eb0aa..6db15c3 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -20,7 +20,11 @@
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.relocation.BringIntoViewResponder
import androidx.compose.foundation.relocation.bringIntoViewResponder
import androidx.compose.foundation.text.BasicText
@@ -33,6 +37,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
@@ -42,6 +47,7 @@
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsFocused
@@ -50,6 +56,7 @@
import androidx.compose.ui.test.isNotFocusable
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -363,6 +370,7 @@
@Test
fun focusable_requestsBringIntoView_whenFocused() {
+ // Arrange.
val requestedRects = mutableListOf<Rect?>()
val bringIntoViewResponder = object : BringIntoViewResponder {
override fun calculateRectForParent(localRect: Rect): Rect = localRect
@@ -387,13 +395,52 @@
rule.runOnIdle {
assertThat(requestedRects).isEmpty()
+ }
+
+ // Act.
+ rule.runOnIdle {
focusRequester.requestFocus()
}
+ // Assert.
rule.runOnIdle {
assertThat(requestedRects).containsExactly(Rect(Offset.Zero, Size(1f, 1f)))
}
}
+ // This test also verifies that the internal API autoInvalidateRemovedNode()
+ // is called when a modifier node is disposed.
+ @Test
+ fun removingFocusableFromLazyList_clearsFocus() {
+ // Arrange.
+ var lazyRowHasFocus = false
+ lateinit var state: LazyListState
+ lateinit var coroutineScope: CoroutineScope
+ var items by mutableStateOf((1..20).toList())
+ rule.setContent {
+ state = rememberLazyListState()
+ coroutineScope = rememberCoroutineScope()
+ LazyRow(
+ modifier = Modifier
+ .requiredSize(100.dp)
+ .onFocusChanged { lazyRowHasFocus = it.hasFocus },
+ state = state
+ ) {
+ items(items.size) {
+ Box(Modifier.requiredSize(10.dp).testTag("$it").focusable())
+ }
+ }
+ }
+ rule.runOnIdle { coroutineScope.launch { state.scrollToItem(19) } }
+ rule.onNodeWithTag("19").performSemanticsAction(SemanticsActions.RequestFocus)
+
+ // Act.
+ rule.runOnIdle { items = (1..11).toList() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(lazyRowHasFocus).isFalse()
+ }
+ }
@OptIn(ExperimentalFoundationApi::class)
@Test
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
index 856f6b7..d1b621f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/BasicMarquee.kt
@@ -39,7 +39,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.DrawModifier
-import androidx.compose.ui.focus.FocusEventModifier
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.clipRect
@@ -181,7 +180,10 @@
private val initialDelayMillis: Int,
private val velocity: Dp,
private val density: Density,
-) : Modifier.Element, LayoutModifier, DrawModifier, FocusEventModifier {
+) : Modifier.Element,
+ LayoutModifier,
+ DrawModifier,
+ @Suppress("DEPRECATION") androidx.compose.ui.focus.FocusEventModifier {
private var contentWidth by mutableStateOf(0)
private var containerWidth by mutableStateOf(0)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
index d292d7e..11bd819 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonScreenshotTest.kt
@@ -165,6 +165,9 @@
rule.runOnIdle {
@OptIn(ExperimentalComposeUiApi::class)
localInputModeManager!!.requestInputMode(InputMode.Keyboard)
+ }
+
+ rule.runOnIdle {
focusRequester.requestFocus()
}
diff --git a/compose/ui/ui-util/api/current.txt b/compose/ui/ui-util/api/current.txt
index b71072e..e80820a 100644
--- a/compose/ui/ui-util/api/current.txt
+++ b/compose/ui/ui-util/api/current.txt
@@ -20,6 +20,7 @@
method public static inline <T> T? fastFirstOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method public static inline <T> void fastForEach(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
method public static inline <T> void fastForEachIndexed(java.util.List<? extends T>, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,kotlin.Unit> action);
+ method public static inline <T> void fastForEachReversed(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
method public static inline <T, R> java.util.List<R> fastMap(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R, C extends java.util.Collection<? super R>> C fastMapTo(java.util.List<? extends T>, C destination, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R extends java.lang.Comparable<? super R>> T? fastMaxBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
diff --git a/compose/ui/ui-util/api/public_plus_experimental_current.txt b/compose/ui/ui-util/api/public_plus_experimental_current.txt
index b71072e..e80820a 100644
--- a/compose/ui/ui-util/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-util/api/public_plus_experimental_current.txt
@@ -20,6 +20,7 @@
method public static inline <T> T? fastFirstOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method public static inline <T> void fastForEach(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
method public static inline <T> void fastForEachIndexed(java.util.List<? extends T>, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,kotlin.Unit> action);
+ method public static inline <T> void fastForEachReversed(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
method public static inline <T, R> java.util.List<R> fastMap(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R, C extends java.util.Collection<? super R>> C fastMapTo(java.util.List<? extends T>, C destination, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R extends java.lang.Comparable<? super R>> T? fastMaxBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
diff --git a/compose/ui/ui-util/api/restricted_current.txt b/compose/ui/ui-util/api/restricted_current.txt
index b71072e..e80820a 100644
--- a/compose/ui/ui-util/api/restricted_current.txt
+++ b/compose/ui/ui-util/api/restricted_current.txt
@@ -20,6 +20,7 @@
method public static inline <T> T? fastFirstOrNull(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
method public static inline <T> void fastForEach(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
method public static inline <T> void fastForEachIndexed(java.util.List<? extends T>, kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,kotlin.Unit> action);
+ method public static inline <T> void fastForEachReversed(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action);
method public static inline <T, R> java.util.List<R> fastMap(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R, C extends java.util.Collection<? super R>> C fastMapTo(java.util.List<? extends T>, C destination, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
method public static inline <T, R extends java.lang.Comparable<? super R>> T? fastMaxBy(java.util.List<? extends T>, kotlin.jvm.functions.Function1<? super T,? extends R> selector);
diff --git a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
index 001b673..25b8c3c 100644
--- a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
+++ b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/ListUtils.kt
@@ -38,6 +38,24 @@
}
/**
+ * Iterates through a [List] in reverse order using the index and calls [action] for each item.
+ * This does not allocate an iterator like [Iterable.forEach].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+inline fun <T> List<T>.fastForEachReversed(action: (T) -> Unit) {
+ contract { callsInPlace(action) }
+ for (index in indices.reversed()) {
+ val item = get(index)
+ action(item)
+ }
+}
+
+/**
* Iterates through a [List] using the index and calls [action] for each item.
* This does not allocate an iterator like [Iterable.forEachIndexed].
*
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 42a92cf..b9629b2 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -1,3 +1,7 @@
// Baseline format: 1.0
InvalidNullConversion: androidx.compose.ui.graphics.GraphicsLayerModifierKt#graphicsLayer(androidx.compose.ui.Modifier, float, float, float, float, float, float, float, float, float, float, long, androidx.compose.ui.graphics.Shape, boolean, androidx.compose.ui.graphics.RenderEffect, long, long):
Attempted to remove @NonNull annotation from method androidx.compose.ui.graphics.GraphicsLayerModifierKt.graphicsLayer(androidx.compose.ui.Modifier,float,float,float,float,float,float,float,float,float,float,long,androidx.compose.ui.graphics.Shape,boolean,androidx.compose.ui.graphics.RenderEffect,long,long)
+
+
+RemovedClass: androidx.compose.ui.focus.FocusManagerKt:
+ Removed class androidx.compose.ui.focus.FocusManagerKt
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 257bea7c..aa8aa59 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -292,8 +292,8 @@
property public final int Up;
}
- @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusEventModifier extends androidx.compose.ui.Modifier.Element {
- method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
+ @Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusEventModifier extends androidx.compose.ui.Modifier.Element {
+ method @Deprecated public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
}
public final class FocusEventModifierKt {
@@ -305,9 +305,6 @@
method public boolean moveFocus(int focusDirection);
}
- public final class FocusManagerKt {
- }
-
public final class FocusModifierKt {
method @Deprecated public static androidx.compose.ui.Modifier focusModifier(androidx.compose.ui.Modifier);
method public static androidx.compose.ui.Modifier focusTarget(androidx.compose.ui.Modifier);
@@ -401,8 +398,8 @@
public final class FocusRequesterKt {
}
- @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
- method public androidx.compose.ui.focus.FocusRequester getFocusRequester();
+ @Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
+ method @Deprecated public androidx.compose.ui.focus.FocusRequester getFocusRequester();
property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
}
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 8334cde..5199a29 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -388,22 +388,23 @@
property public final int Up;
}
- @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusEventModifier extends androidx.compose.ui.Modifier.Element {
- method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
+ @Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusEventModifier extends androidx.compose.ui.Modifier.Element {
+ method @Deprecated public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
}
public final class FocusEventModifierKt {
method public static androidx.compose.ui.Modifier onFocusEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusEvent);
}
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface FocusEventModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
+ }
+
@kotlin.jvm.JvmDefaultWithCompatibility public interface FocusManager {
method public void clearFocus(optional boolean force);
method public boolean moveFocus(int focusDirection);
}
- public final class FocusManagerKt {
- }
-
public final class FocusModifierKt {
method @Deprecated public static androidx.compose.ui.Modifier focusModifier(androidx.compose.ui.Modifier);
method public static androidx.compose.ui.Modifier focusTarget(androidx.compose.ui.Modifier);
@@ -487,6 +488,10 @@
method public static androidx.compose.ui.Modifier focusProperties(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusProperties,kotlin.Unit> scope);
}
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface FocusPropertiesModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public void modifyFocusProperties(androidx.compose.ui.focus.FocusProperties focusProperties);
+ }
+
public final class FocusRequester {
ctor public FocusRequester();
method public boolean captureFocus();
@@ -526,13 +531,19 @@
public final class FocusRequesterKt {
}
- @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
- method public androidx.compose.ui.focus.FocusRequester getFocusRequester();
+ @Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
+ method @Deprecated public androidx.compose.ui.focus.FocusRequester getFocusRequester();
property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
}
public final class FocusRequesterModifierKt {
+ method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean captureFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
+ method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean freeFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean requestFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+ }
+
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface FocusRequesterModifierNode extends androidx.compose.ui.node.DelegatableNode {
}
public interface FocusState {
@@ -544,6 +555,13 @@
property public abstract boolean isFocused;
}
+ @androidx.compose.ui.ExperimentalComposeUiApi public final class FocusTargetModifierNode extends androidx.compose.ui.Modifier.Node implements androidx.compose.ui.modifier.ModifierLocalNode androidx.compose.ui.node.ObserverNode {
+ ctor public FocusTargetModifierNode();
+ method public androidx.compose.ui.focus.FocusState getFocusState();
+ method public void onObservedReadsChanged();
+ property public final androidx.compose.ui.focus.FocusState focusState;
+ }
+
public final class FocusTransactionsKt {
}
@@ -1587,6 +1605,11 @@
method public static androidx.compose.ui.Modifier onPreviewKeyEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreviewKeyEvent);
}
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface KeyInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public boolean onKeyEvent(android.view.KeyEvent event);
+ method public boolean onPreKeyEvent(android.view.KeyEvent event);
+ }
+
public final class Key_androidKt {
method public static long Key(int nativeKeyCode);
method public static int getNativeKeyCode(long);
@@ -1932,6 +1955,11 @@
method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier onRotaryScrollEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.rotary.RotaryScrollEvent,java.lang.Boolean> onRotaryScrollEvent);
}
+ @androidx.compose.ui.ExperimentalComposeUiApi public interface RotaryInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+ method public boolean onPreRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
+ method public boolean onRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
+ }
+
@androidx.compose.ui.ExperimentalComposeUiApi public final class RotaryScrollEvent {
method public float getHorizontalScrollPixels();
method public long getUptimeMillis();
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 42a92cf..b9629b2 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -1,3 +1,7 @@
// Baseline format: 1.0
InvalidNullConversion: androidx.compose.ui.graphics.GraphicsLayerModifierKt#graphicsLayer(androidx.compose.ui.Modifier, float, float, float, float, float, float, float, float, float, float, long, androidx.compose.ui.graphics.Shape, boolean, androidx.compose.ui.graphics.RenderEffect, long, long):
Attempted to remove @NonNull annotation from method androidx.compose.ui.graphics.GraphicsLayerModifierKt.graphicsLayer(androidx.compose.ui.Modifier,float,float,float,float,float,float,float,float,float,float,long,androidx.compose.ui.graphics.Shape,boolean,androidx.compose.ui.graphics.RenderEffect,long,long)
+
+
+RemovedClass: androidx.compose.ui.focus.FocusManagerKt:
+ Removed class androidx.compose.ui.focus.FocusManagerKt
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 4db7404..7d8ccd9 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -292,8 +292,8 @@
property public final int Up;
}
- @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusEventModifier extends androidx.compose.ui.Modifier.Element {
- method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
+ @Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusEventModifier extends androidx.compose.ui.Modifier.Element {
+ method @Deprecated public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
}
public final class FocusEventModifierKt {
@@ -305,9 +305,6 @@
method public boolean moveFocus(int focusDirection);
}
- public final class FocusManagerKt {
- }
-
public final class FocusModifierKt {
method @Deprecated public static androidx.compose.ui.Modifier focusModifier(androidx.compose.ui.Modifier);
method public static androidx.compose.ui.Modifier focusTarget(androidx.compose.ui.Modifier);
@@ -401,8 +398,8 @@
public final class FocusRequesterKt {
}
- @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
- method public androidx.compose.ui.focus.FocusRequester getFocusRequester();
+ @Deprecated @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusRequesterModifier extends androidx.compose.ui.Modifier.Element {
+ method @Deprecated public androidx.compose.ui.focus.FocusRequester getFocusRequester();
property public abstract androidx.compose.ui.focus.FocusRequester focusRequester;
}
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ConditionalFocusabilityDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ConditionalFocusabilityDemo.kt
index d4c43cf..7c416f6 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ConditionalFocusabilityDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/ConditionalFocusabilityDemo.kt
@@ -81,8 +81,7 @@
Row {
var item2active by remember { mutableStateOf(false) }
Text(
- text = "focusable item that is " +
- "${if (item2active) "activated" else "deactivated"}",
+ text = "focusable item that is " + if (item2active) "activated" else "deactivated",
modifier = Modifier
.focusAwareBackground()
.focusRequester(item2)
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/LazyListChildFocusDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/LazyListChildFocusDemos.kt
index 6760f3b..80c066f6 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/LazyListChildFocusDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/LazyListChildFocusDemos.kt
@@ -28,16 +28,22 @@
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.FocusRequester.Companion.Default
+import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LocalPinnableContainer
+import androidx.compose.ui.layout.PinnableContainer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@@ -84,15 +90,30 @@
var previouslyFocusedItem: FocusRequester? by remember { mutableStateOf(null) }
LazyRow(
Modifier
- .onFocusChanged { if (it.isFocused) previouslyFocusedItem?.requestFocus() }
- .then(previouslyFocusedItem?.let { Modifier.focusable() } ?: Modifier)
+ .focusProperties {
+ @OptIn(ExperimentalComposeUiApi::class)
+ enter = { previouslyFocusedItem ?: Default }
+ }
) {
- items(10) {
- val focusRequester = remember { FocusRequester() }
+ items(10) { index ->
+ val focusRequester = remember(index) { FocusRequester() }
+ val pinnableContainer = LocalPinnableContainer.current
+ var pinnedHandle: PinnableContainer.PinnedHandle? = null
FocusableBox(Modifier
- .onFocusChanged { if (it.isFocused) previouslyFocusedItem = focusRequester }
+ .onFocusChanged {
+ if (it.isFocused) {
+ previouslyFocusedItem = focusRequester
+ pinnedHandle = pinnableContainer?.pin()
+ }
+ }
.focusRequester(focusRequester)
)
+ DisposableEffect(pinnableContainer) {
+ onDispose {
+ pinnedHandle?.unpin()
+ pinnedHandle = null
+ }
+ }
}
}
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
index 5e49f81..a70eb38 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CaptureFocusTest.kt
@@ -19,10 +19,6 @@
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -47,9 +43,10 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Active))
+ .focusTarget()
)
}
+ rule.runOnIdle { focusRequester.requestFocus() }
// Act.
val success = rule.runOnIdle {
@@ -68,14 +65,26 @@
fun activeParent_captureFocus_retainsStateAsActiveParent() {
// Arrange.
lateinit var focusState: FocusState
+ val initialFocus = FocusRequester()
val focusRequester = FocusRequester()
rule.setFocusableContent {
Box(
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(ActiveParent))
- )
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .focusTarget()
+ )
+ }
+ }
+ rule.runOnIdle {
+ initialFocus.requestFocus()
+ assertThat(focusState.isCaptured).isFalse()
+ assertThat(focusState.hasFocus).isTrue()
}
// Act.
@@ -101,9 +110,14 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Captured))
+ .focusTarget()
)
}
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ focusRequester.captureFocus()
+ assertThat(focusState.isCaptured).isTrue()
+ }
// Act.
val success = rule.runOnIdle {
@@ -118,7 +132,7 @@
}
@Test
- fun deactivated_captureFocus_retainsStateAsDeactivated() {
+ fun deactivated_captureFocus_retainsFocusState() {
// Arrange.
lateinit var focusState: FocusState
val focusRequester = FocusRequester()
@@ -142,12 +156,11 @@
assertThat(success).isFalse()
assertThat(focusState.isCaptured).isFalse()
assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.isDeactivated).isTrue()
}
}
@Test
- fun deactivatedParent_captureFocus_retainsStateAsDeactivatedParent() {
+ fun deactivatedParent_captureFocus_retainsFocusState() {
// Arrange.
lateinit var focusState: FocusState
val initialFocus = FocusRequester()
@@ -179,7 +192,6 @@
assertThat(success).isFalse()
assertThat(focusState.isCaptured).isFalse()
assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isDeactivated).isTrue()
}
}
@@ -193,7 +205,7 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
}
@@ -210,6 +222,3 @@
}
}
}
-
-private val FocusState.isDeactivated: Boolean
- get() = (this as FocusStateImpl).isDeactivated
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
deleted file mode 100644
index 52142f1..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ClearFocusTest.kt
+++ /dev/null
@@ -1,421 +0,0 @@
-/*
- * Copyright 2020 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.runtime.SideEffect
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
-import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
-import androidx.compose.ui.test.junit4.createComposeRule
-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
-import org.junit.runners.Parameterized
-
-@SmallTest
-@RunWith(Parameterized::class)
-class ClearFocusTest(private val forced: Boolean) {
- @get:Rule
- val rule = createComposeRule()
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "forcedClear = {0}")
- fun initParameters() = listOf(true, false)
- }
-
- @Test
- fun active_isCleared() {
- // Arrange.
- val modifier = FocusModifier(Active)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(modifier))
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusState).isEqualTo(Inactive)
- }
- }
-
- @Test
- fun active_isClearedAndRemovedFromParentsFocusedChild() {
- // Arrange.
- val parent = FocusModifier(ActiveParent)
- val modifier = FocusModifier(Active)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(parent)) {
- Box(Modifier.focusTarget(modifier))
- }
- SideEffect {
- parent.focusedChild = modifier
- }
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusState).isEqualTo(Inactive)
- }
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun activeParent_noFocusedChild_throwsException() {
- // Arrange.
- val modifier = FocusModifier(ActiveParent)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(modifier))
- }
-
- // Act.
- rule.runOnIdle {
- modifier.clearFocus(forced)
- }
- }
-
- @Test
- fun activeParent_isClearedAndRemovedFromParentsFocusedChild() {
- // Arrange.
- val parent = FocusModifier(ActiveParent)
- val modifier = FocusModifier(ActiveParent)
- val child = FocusModifier(Active)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(parent)) {
- Box(Modifier.focusTarget(modifier)) {
- Box(Modifier.focusTarget(child))
- }
- }
- SideEffect {
- parent.focusedChild = modifier
- modifier.focusedChild = child
- }
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusedChild).isNull()
- assertThat(modifier.focusState).isEqualTo(Inactive)
- }
- }
-
- @Test
- fun activeParent_clearsEntireHierarchy() {
- // Arrange.
- val modifier = FocusModifier(ActiveParent)
- val child = FocusModifier(ActiveParent)
- val grandchild = FocusModifier(ActiveParent)
- val greatGrandchild = FocusModifier(Active)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(modifier)) {
- Box(Modifier.focusTarget(child)) {
- Box(Modifier.focusTarget(grandchild)) {
- Box(Modifier.focusTarget(greatGrandchild))
- }
- }
- }
- SideEffect {
- modifier.focusedChild = child
- child.focusedChild = grandchild
- grandchild.focusedChild = greatGrandchild
- }
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusedChild).isNull()
- assertThat(child.focusedChild).isNull()
- assertThat(grandchild.focusedChild).isNull()
- assertThat(modifier.focusState).isEqualTo(Inactive)
- assertThat(child.focusState).isEqualTo(Inactive)
- assertThat(grandchild.focusState).isEqualTo(Inactive)
- assertThat(greatGrandchild.focusState).isEqualTo(Inactive)
- }
- }
-
- @Test
- fun captured_isCleared_whenForced() {
- // Arrange.
- val modifier = FocusModifier(Captured)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(modifier))
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- when (forced) {
- true -> {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusState).isEqualTo(Inactive)
- }
- false -> {
- assertThat(cleared).isFalse()
- assertThat(modifier.focusState).isEqualTo(Captured)
- }
- }
- }
- }
-
- @Test
- fun active_isClearedAndRemovedFromParentsFocusedChild_whenForced() {
- // Arrange.
- val parent = FocusModifier(ActiveParent)
- val modifier = FocusModifier(Captured)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(parent)) {
- Box(Modifier.focusTarget(modifier))
- }
- SideEffect {
- parent.focusedChild = modifier
- }
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- when (forced) {
- true -> {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusState).isEqualTo(Inactive)
- }
- false -> {
- assertThat(cleared).isFalse()
- assertThat(modifier.focusState).isEqualTo(Captured)
- }
- }
- }
- }
-
- @Test
- fun Inactive_isUnchanged() {
- // Arrange.
- val modifier = FocusModifier(Inactive)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(modifier))
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusState).isEqualTo(Inactive)
- }
- }
-
- @Test
- fun Deactivated_isUnchanged() {
- // Arrange.
- val modifier = FocusModifier(Inactive)
- rule.setFocusableContent {
- Box(
- Modifier
- .focusProperties { canFocus = false }
- .focusTarget(modifier)
- )
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusState.isDeactivated).isTrue()
- }
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun deactivatedParent_noFocusedChild_throwsException() {
- // Arrange.
- val modifier = FocusModifier(DeactivatedParent)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(modifier))
- }
-
- // Act.
- rule.runOnIdle {
- modifier.clearFocus(forced)
- }
- }
-
- @Test
- fun deactivatedParent_isClearedAndRemovedFromParentsFocusedChild() {
- // Arrange.
- val parent = FocusModifier(ActiveParent)
- val modifier = FocusModifier(ActiveParent)
- val child = FocusModifier(Active)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(parent)) {
- Box(
- Modifier
- .focusProperties { canFocus = false }
- .focusTarget(modifier)
- ) {
- Box(Modifier.focusTarget(child))
- }
- }
- SideEffect {
- parent.focusedChild = modifier
- modifier.focusedChild = child
- }
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusedChild).isNull()
- assertThat(modifier.focusState.isDeactivated).isTrue()
- }
- }
-
- @Test
- fun deactivatedParent_withDeactivatedGrandParent_isClearedAndRemovedFromParentsFocusedChild() {
- // Arrange.
- val parent = FocusModifier(ActiveParent)
- val modifier = FocusModifier(ActiveParent)
- val child = FocusModifier(Active)
- rule.setFocusableContent {
- Box(Modifier
- .focusProperties { canFocus = false }
- .focusTarget(parent)
- ) {
- Box(Modifier
- .focusProperties { canFocus = false }
- .focusTarget(modifier)
- ) {
- Box(Modifier.focusTarget(child))
- }
- }
- SideEffect {
- parent.focusedChild = modifier
- modifier.focusedChild = child
- }
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusedChild).isNull()
- assertThat(modifier.focusState.isDeactivated).isTrue()
- }
- }
-
- @Test
- fun deactivatedParent_clearsEntireHierarchy() {
- // Arrange.
- val modifier = FocusModifier(ActiveParent)
- val child = FocusModifier(ActiveParent)
- val grandchild = FocusModifier(ActiveParent)
- val greatGrandchild = FocusModifier(ActiveParent)
- val greatGreatGrandchild = FocusModifier(Active)
- rule.setFocusableContent {
- Box(Modifier
- .focusProperties { canFocus = false }
- .focusTarget(modifier)
- ) {
- Box(modifier = Modifier.focusTarget(child)) {
- Box(Modifier
- .focusProperties { canFocus = false }
- .focusTarget(grandchild)
- ) {
- Box(Modifier.focusTarget(greatGrandchild)) {
- Box(Modifier.focusTarget(greatGreatGrandchild))
- }
- }
- }
- }
- SideEffect {
- modifier.focusedChild = child
- child.focusedChild = grandchild
- grandchild.focusedChild = greatGrandchild
- greatGrandchild.focusedChild = greatGreatGrandchild
- }
- }
-
- // Act.
- val cleared = rule.runOnIdle {
- modifier.clearFocus(forced)
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(cleared).isTrue()
- assertThat(modifier.focusedChild).isNull()
- assertThat(child.focusedChild).isNull()
- assertThat(grandchild.focusedChild).isNull()
- assertThat(modifier.focusState).isEqualTo(Deactivated)
- assertThat(child.focusState).isEqualTo(Inactive)
- assertThat(grandchild.focusState).isEqualTo(Deactivated)
- assertThat(greatGrandchild.focusState).isEqualTo(Inactive)
- assertThat(greatGreatGrandchild.focusState).isEqualTo(Inactive)
- }
- }
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CombinedFocusModifierNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CombinedFocusModifierNodeTest.kt
new file mode 100644
index 0000000..721a3089
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CombinedFocusModifierNodeTest.kt
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2022 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.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.modifierElementOf
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
+@RunWith(Parameterized::class)
+class CombinedFocusModifierNodeTest(private val delegatedFocusTarget: Boolean) {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun requestFocus() {
+ // Arrange.
+ val combinedFocusNode = CombinedFocusNode(delegatedFocusTarget)
+ rule.setFocusableContent {
+ Box(Modifier.combinedFocusNode(combinedFocusNode))
+ }
+
+ // Act.
+ rule.runOnIdle {
+ combinedFocusNode.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(combinedFocusNode.focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun captureFocus() {
+ // Arrange.
+ val combinedFocusNode = CombinedFocusNode(delegatedFocusTarget)
+ rule.setFocusableContent {
+ Box(Modifier.combinedFocusNode(combinedFocusNode))
+ }
+ rule.runOnIdle {
+ combinedFocusNode.requestFocus()
+ }
+
+ // Act.
+ rule.runOnIdle {
+ combinedFocusNode.captureFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(combinedFocusNode.focusState.isFocused).isTrue()
+ assertThat(combinedFocusNode.focusState.isCaptured).isTrue()
+ }
+ }
+
+ @Test
+ fun freeFocus() {
+ // Arrange.
+ val combinedFocusNode = CombinedFocusNode(delegatedFocusTarget)
+ rule.setFocusableContent {
+ Box(Modifier.combinedFocusNode(combinedFocusNode))
+ }
+ rule.runOnIdle {
+ combinedFocusNode.requestFocus()
+ combinedFocusNode.captureFocus()
+ }
+
+ // Act.
+ rule.runOnIdle {
+ combinedFocusNode.freeFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(combinedFocusNode.focusState.isFocused).isTrue()
+ assertThat(combinedFocusNode.focusState.isCaptured).isFalse()
+ }
+ }
+
+ @Test
+ fun requestFocusWhenCanFocusIsTrue() {
+ // Arrange.
+ val combinedFocusNode = CombinedFocusNode(delegatedFocusTarget).apply { canFocus = true }
+ rule.setFocusableContent {
+ Box(Modifier.combinedFocusNode(combinedFocusNode))
+ }
+
+ // Act.
+ rule.runOnIdle {
+ combinedFocusNode.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(combinedFocusNode.focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun requestFocusWhenCanFocusIsFalse() {
+ // Arrange.
+ val combinedFocusNode = CombinedFocusNode(delegatedFocusTarget).apply { canFocus = false }
+ rule.setFocusableContent {
+ Box(Modifier.combinedFocusNode(combinedFocusNode))
+ }
+
+ // Act.
+ rule.runOnIdle {
+ combinedFocusNode.requestFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(combinedFocusNode.focusState.isFocused).isFalse()
+ }
+ }
+
+ /**
+ * This test checks that [FocusPropertiesModifierNode.modifyFocusProperties] is called when a
+ * property changes.
+ */
+ @Test
+ fun losesFocusWhenCanFocusChangesToFalse() {
+ // Arrange.
+ val combinedFocusNode = CombinedFocusNode(delegatedFocusTarget)
+ rule.setFocusableContent {
+ Box(Modifier.combinedFocusNode(combinedFocusNode))
+ }
+ rule.runOnIdle {
+ combinedFocusNode.requestFocus()
+ }
+
+ // Act.
+ rule.runOnIdle {
+ combinedFocusNode.canFocus = false
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(combinedFocusNode.focusState.isFocused).isFalse()
+ }
+ }
+
+ @Test
+ fun doesNotGainFocusWhenCanFocusChangesToTrue() {
+ // Arrange.
+ val combinedFocusNode = CombinedFocusNode(delegatedFocusTarget)
+ rule.setFocusableContent {
+ Box(Modifier.combinedFocusNode(combinedFocusNode))
+ }
+ rule.runOnIdle {
+ combinedFocusNode.requestFocus()
+ combinedFocusNode.canFocus = false
+ }
+
+ // Act.
+ rule.runOnIdle {
+ combinedFocusNode.canFocus = true
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(combinedFocusNode.focusState.isFocused).isFalse()
+ }
+ }
+
+ private fun Modifier.combinedFocusNode(combinedFocusNode: CombinedFocusNode): Modifier {
+ return this
+ .then(
+ modifierElementOf(
+ key = combinedFocusNode,
+ create = { combinedFocusNode },
+ update = { it.focusState = combinedFocusNode.focusState },
+ definitions = { name = "CombinedFocusNode" }
+ )
+ )
+ .then(if (delegatedFocusTarget) Modifier else Modifier.focusTarget())
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "delegatedFocusTarget = {0}")
+ fun initParameters() =
+ listOf(
+ false,
+ // TODO: Delegation does not work right now because a delegated node can
+ // reference the node delegating to it, but it can't reference a delegated node in
+ // its parent. For some use-cases, a parent needs to invalidate a child. We cannot
+ // do this when the child is a delegated node.
+ // true
+ )
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ private class CombinedFocusNode(delegatedFocusTarget: Boolean) :
+ FocusRequesterModifierNode,
+ FocusEventModifierNode,
+ FocusPropertiesModifierNode,
+ DelegatingNode() {
+
+ init {
+ if (delegatedFocusTarget) delegated { FocusTargetModifierNode() }
+ }
+
+ lateinit var focusState: FocusState
+
+ var canFocus by mutableStateOf(true)
+
+ override fun onFocusEvent(focusState: FocusState) {
+ this.focusState = focusState
+ }
+
+ override fun modifyFocusProperties(focusProperties: FocusProperties) {
+ focusProperties.canFocus = canFocus
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
index c3f7bcc..e3749d3 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
@@ -22,6 +22,9 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key.Companion.Tab
@@ -42,6 +45,7 @@
import org.junit.runner.RunWith
import com.google.common.truth.Truth.assertThat
import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
@ExperimentalComposeUiApi
@MediumTest
@@ -55,7 +59,7 @@
companion object {
@JvmStatic
- @Parameterized.Parameters(name = "moveFocusProgrammatically = {0}, useFocusModifier = {1}")
+ @Parameters(name = "moveFocusProgrammatically = {0}, useFocusOrderModifier = {1}")
fun initParameters() = listOf(
arrayOf(true, true),
arrayOf(true, false),
@@ -713,7 +717,7 @@
}
@Test
- fun focusProperties_emptyfocusPropertiesInParent_doesNotResetCustomNextSetByChild() {
+ fun focusProperties_emptyFocusPropertiesInParent_doesNotResetCustomNextSetByChild() {
// Arrange.
var item1Focused = false
var item2Focused = false
@@ -764,8 +768,128 @@
}
}
+ @Test
+ fun changedFocusProperties() {
+ // Arrange.
+ var item1Focused = false
+ var item2Focused = false
+ var item3Focused = false
+ var item4Focused = false
+ val (item1, item3, item4) = FocusRequester.createRefs()
+ var nextItem = item3
+ lateinit var focusManager: FocusManager
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ Row {
+ Box(
+ Modifier
+ .focusRequester(item1)
+ .dynamicFocusProperties { next = nextItem }
+ .onFocusChanged { item1Focused = it.isFocused }
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .onFocusChanged { item2Focused = it.isFocused }
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .focusRequester(item3)
+ .onFocusChanged { item3Focused = it.isFocused }
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .focusRequester(item4)
+ .onFocusChanged { item4Focused = it.isFocused }
+ .focusTarget()
+ )
+ }
+ }
+ rule.runOnIdle { item1.requestFocus() }
+
+ // Act.
+ rule.runOnIdle {
+ nextItem = item4
+ }
+ if (moveFocusProgrammatically) {
+ rule.runOnIdle {
+ focusManager.moveFocus(FocusDirection.Next)
+ }
+ } else {
+ rule.onRoot().performKeyPress(KeyEvent(AndroidKeyEvent(KeyDown, Tab.nativeKeyCode)))
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(item1Focused).isFalse()
+ assertThat(item2Focused).isFalse()
+ assertThat(item3Focused).isFalse()
+ assertThat(item4Focused).isTrue()
+ }
+ }
+
+ @Test
+ fun changedFocusProperties_mutableState() {
+ // Arrange.
+ var item1Focused = false
+ var item2Focused = false
+ var item3Focused = false
+ var item4Focused = false
+ val (item1, item3, item4) = FocusRequester.createRefs()
+ var nextItem by mutableStateOf(item3)
+ lateinit var focusManager: FocusManager
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ Row {
+ Box(
+ Modifier
+ .focusRequester(item1)
+ .dynamicFocusProperties { next = nextItem }
+ .onFocusChanged { item1Focused = it.isFocused }
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .onFocusChanged { item2Focused = it.isFocused }
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .focusRequester(item3)
+ .onFocusChanged { item3Focused = it.isFocused }
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .focusRequester(item4)
+ .onFocusChanged { item4Focused = it.isFocused }
+ .focusTarget()
+ )
+ }
+ }
+ rule.runOnIdle { item1.requestFocus() }
+
+ // Act.
+ rule.runOnIdle { nextItem = item4 }
+ if (moveFocusProgrammatically) {
+ rule.runOnIdle { focusManager.moveFocus(FocusDirection.Next) }
+ } else {
+ rule.onRoot().performKeyPress(KeyEvent(AndroidKeyEvent(KeyDown, Tab.nativeKeyCode)))
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(item1Focused).isFalse()
+ assertThat(item2Focused).isFalse()
+ assertThat(item3Focused).isFalse()
+ assertThat(item4Focused).isTrue()
+ }
+ }
+
@Suppress("DEPRECATION")
- fun Modifier.dynamicFocusProperties(block: FocusOrder.() -> Unit): Modifier =
+ private fun Modifier.dynamicFocusProperties(block: FocusOrder.() -> Unit): Modifier =
if (useFocusOrderModifier) {
this.then(ReceiverFocusOrderModifier(block))
} else {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/DeactivatedFocusPropertiesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/DeactivatedFocusPropertiesTest.kt
deleted file mode 100644
index b5a057b..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/DeactivatedFocusPropertiesTest.kt
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright 2021 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.foundation.layout.Column
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class DeactivatedFocusPropertiesTest {
- @get:Rule
- val rule = createComposeRule()
-
- @Test
- fun notDeactivatedByDefault() {
- // Arrange.
- var isDeactivated: Boolean? = null
- rule.setFocusableContent {
- Box(modifier = Modifier
- .onFocusChanged { isDeactivated = it.isDeactivated }
- .focusTarget()
- )
- }
-
- // Assert.
- rule.runOnIdle { assertThat(isDeactivated).isFalse() }
- }
-
- @Test
- fun initializedAsNotDeactivated() {
- // Arrange.
- var deactivated: Boolean? = null
- rule.setFocusableContent {
- Box(modifier = Modifier
- .focusProperties { canFocus = true }
- .onFocusChanged { deactivated = it.isDeactivated }
- .focusTarget()
- )
- }
-
- // Assert.
- rule.runOnIdle { assertThat(deactivated).isFalse() }
- }
-
- @Test
- fun initializedAsDeactivated() {
- // Arrange.
- var isDeactivated: Boolean? = null
- rule.setFocusableContent {
- Box(modifier = Modifier
- .focusProperties { canFocus = false }
- .onFocusChanged { isDeactivated = it.isDeactivated }
- .focusTarget()
- )
- }
-
- // Assert.
- rule.runOnIdle { assertThat(isDeactivated).isTrue() }
- }
-
- @Test
- fun leftMostDeactivatedPropertyTakesPrecedence() {
- // Arrange.
- var deactivated: Boolean? = null
- rule.setFocusableContent {
- Box(modifier = Modifier
- .focusProperties { canFocus = false }
- .focusProperties { canFocus = true }
- .onFocusChanged { deactivated = it.isDeactivated }
- .focusTarget()
- )
- }
-
- // Assert.
- rule.runOnIdle { assertThat(deactivated).isTrue() }
- }
-
- @Test
- fun leftMostNonDeactivatedPropertyTakesPrecedence() {
- // Arrange.
- var deactivated: Boolean? = null
- rule.setFocusableContent {
- Box(modifier = Modifier
- .focusProperties { canFocus = true }
- .focusProperties { canFocus = false }
- .onFocusChanged { deactivated = it.isDeactivated }
- .focusTarget()
- )
- }
-
- // Assert.
- rule.runOnIdle { assertThat(deactivated).isFalse() }
- }
-
- @Test
- fun ParentsDeactivatedPropertyTakesPrecedence() {
- // Arrange.
- var deactivated: Boolean? = null
- rule.setFocusableContent {
- Box(modifier = Modifier.focusProperties { canFocus = false }) {
- Box(modifier = Modifier
- .focusProperties { canFocus = true }
- .onFocusChanged { deactivated = it.isDeactivated }
- .focusTarget()
- )
- }
- }
-
- // Assert.
- rule.runOnIdle { assertThat(deactivated).isTrue() }
- }
-
- @Test
- fun ParentsNotDeactivatedPropertyTakesPrecedence() {
- // Arrange.
- var deactivated: Boolean? = null
- rule.setFocusableContent {
- Box(modifier = Modifier.focusProperties { canFocus = true }) {
- Box(modifier = Modifier
- .focusProperties { canFocus = false }
- .onFocusChanged { deactivated = it.isDeactivated }
- .focusTarget()
- )
- }
- }
-
- // Assert.
- rule.runOnIdle { assertThat(deactivated).isFalse() }
- }
-
- @Test
- fun deactivatedItemDoesNotGainFocus() {
- // Arrange.
- var isFocused: Boolean? = null
- val focusRequester = FocusRequester()
- rule.setFocusableContent {
- Box(modifier = Modifier
- .focusProperties { canFocus = false }
- .focusRequester(focusRequester)
- .onFocusChanged { isFocused = it.isFocused }
- .focusTarget()
- )
- }
-
- // Act.
- rule.runOnIdle { focusRequester.requestFocus() }
-
- // Assert.
- rule.runOnIdle { assertThat(isFocused).isFalse() }
- }
-
- @Test
- fun deactivatedFocusPropertiesOnNonFocusableParentAppliesToAllChildren() {
- // Arrange.
- var isParentDeactivated: Boolean? = null
- var isChild1Deactivated: Boolean? = null
- var isChild2Deactivated: Boolean? = null
- var isGrandChildDeactivated: Boolean? = null
- rule.setFocusableContent {
- Column(modifier = Modifier
- .focusProperties { canFocus = false }
- .onFocusChanged { isParentDeactivated = it.isDeactivated }
- ) {
- Box(modifier = Modifier
- .onFocusChanged { isChild1Deactivated = it.isDeactivated }
- .focusTarget()
- )
- Box(modifier = Modifier
- .onFocusChanged { isChild2Deactivated = it.isDeactivated }
- .focusTarget()
- ) {
- Box(modifier = Modifier
- .onFocusChanged { isGrandChildDeactivated = it.isDeactivated }
- .focusTarget()
- )
- }
- }
- }
-
- // Assert.
- rule.runOnIdle { assertThat(isParentDeactivated).isTrue() }
- rule.runOnIdle { assertThat(isChild1Deactivated).isTrue() }
- rule.runOnIdle { assertThat(isChild2Deactivated).isTrue() }
- rule.runOnIdle { assertThat(isGrandChildDeactivated).isFalse() }
- }
-
- @Test
- fun focusedItemLosesFocusWhenDeactivated() {
- // Arrange.
- var isFocused: Boolean? = null
- val focusRequester = FocusRequester()
- var deactivated by mutableStateOf(false)
- rule.setFocusableContent {
- Box(modifier = Modifier
- .focusProperties { canFocus = !deactivated }
- .focusRequester(focusRequester)
- .onFocusChanged { isFocused = it.isFocused }
- .focusTarget()
- )
- }
- rule.runOnIdle {
- focusRequester.requestFocus()
- assertThat(isFocused).isTrue()
- }
-
- // Act.
- rule.runOnIdle { deactivated = true }
-
- // Assert.
- rule.runOnIdle { assertThat(isFocused).isFalse() }
- }
-}
-
-private val FocusState.isDeactivated: Boolean
- get() = (this as FocusStateImpl).isDeactivated
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
deleted file mode 100644
index 1fc85e9..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindFocusableChildrenTest.kt
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright 2020 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.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
-import androidx.compose.ui.graphics.Color.Companion.Red
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class FindFocusableChildrenTest(private val excludeDeactivated: Boolean) {
- @get:Rule
- val rule = createComposeRule()
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "excludeDeactivated = {0}")
- fun initParameters() = listOf(true, false)
- }
-
- @Test
- fun returnsFirstFocusNodeInModifierChain() {
- val focusModifier1 = FocusModifier(Inactive)
- val focusModifier2 = FocusModifier(Inactive)
- val focusModifier3 = FocusModifier(Inactive)
- val focusModifier4 = FocusModifier(Inactive)
- // Arrange.
- // layoutNode--focusNode1--focusNode2--focusNode3--focusNode4
- rule.setContent {
- Box(
- Modifier
- .focusTarget(focusModifier1)
- .focusProperties { canFocus = false }
- .focusTarget(focusModifier2)
- .focusTarget(focusModifier3)
- .focusTarget(focusModifier4)
- )
- }
-
- // Act.
- val focusableChildren = rule.runOnIdle {
- focusModifier1.focusableChildren(excludeDeactivated)
- }
-
- // Assert.
- rule.runOnIdle {
- if (excludeDeactivated) {
- assertThat(focusableChildren).isExactly(focusModifier3)
- } else {
- assertThat(focusableChildren).isExactly(focusModifier2)
- }
- }
- }
-
- @Test
- fun skipsNonFocusNodesAndReturnsFirstFocusNodeInModifierChain() {
- val focusModifier1 = FocusModifier(Inactive)
- val focusModifier2 = FocusModifier(Inactive)
- val focusModifier3 = FocusModifier(Inactive)
- // Arrange.
- // layoutNode--focusNode1--nonFocusNode--focusNode2--focusNode3
- rule.setContent {
- Box(
- Modifier
- .focusTarget(focusModifier1)
- .background(color = Red)
- .focusProperties { canFocus = false }
- .focusTarget(focusModifier2)
- .focusTarget(focusModifier3)
- )
- }
-
- // Act.
- val focusableChildren = rule.runOnIdle {
- focusModifier1.focusableChildren(excludeDeactivated)
- }
-
- // Assert.
- rule.runOnIdle {
- if (excludeDeactivated) {
- assertThat(focusableChildren).isExactly(focusModifier3)
- } else {
- assertThat(focusableChildren).isExactly(focusModifier2)
- }
- }
- }
-
- @Test
- fun returnsFirstFocusChildOfEachChildLayoutNode() {
- // Arrange.
- // parentLayoutNode--parentFocusNode
- // |___________________________________________
- // | |
- // childLayoutNode1--focusNode1--focusNode2 childLayoutNode2--focusNode3--focusNode4
- val parentFocusModifier = FocusModifier(Inactive)
- val focusModifier1 = FocusModifier(Inactive)
- val focusModifier2 = FocusModifier(Inactive)
- val focusModifier3 = FocusModifier(Inactive)
- val focusModifier4 = FocusModifier(Inactive)
- rule.setContent {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(
- Modifier
- .focusProperties { canFocus = false }
- .focusTarget(focusModifier1)
- .focusTarget(focusModifier2)
- )
- Box(
- Modifier
- .focusTarget(focusModifier3)
- .focusProperties { canFocus = false }
- .focusTarget(focusModifier4)
- )
- }
- }
-
- // Act.
- val focusableChildren = rule.runOnIdle {
- parentFocusModifier.focusableChildren(excludeDeactivated)
- }
-
- // Assert.
- rule.runOnIdle {
- if (excludeDeactivated) {
- assertThat(focusableChildren).isExactly(
- focusModifier2, focusModifier3
- )
- } else {
- assertThat(focusableChildren).isExactly(
- focusModifier1, focusModifier3
- )
- }
- }
- }
-
- @OptIn(ExperimentalComposeUiApi::class)
- @Test
- fun focusedChildIsAvailableFromOnFocusEvent() {
- // Arrange.
- val parentFocusModifier = FocusModifier(Inactive)
- val childFocusModifier = FocusModifier(Inactive)
- val focusRequester = FocusRequester()
- var focusedChildAtTimeOfEvent: FocusModifier? = null
- rule.setFocusableContent {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(
- Modifier
- .onFocusEvent {
- if (it.isFocused) {
- focusedChildAtTimeOfEvent = parentFocusModifier.focusedChild
- }
- }
- .focusRequester(focusRequester)
- .focusTarget(childFocusModifier)
- )
- }
- }
-
- // Act.
- rule.runOnIdle { focusRequester.requestFocus() }
-
- // Assert.
- assertThat(focusedChildAtTimeOfEvent)
- .isEqualTo(childFocusModifier)
- }
-
- private fun FocusModifier.focusableChildren(excludeDeactivated: Boolean): List<FocusModifier> =
- (if (excludeDeactivated) activatedChildren() else children).asMutableList()
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
deleted file mode 100644
index abb72aa..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FindParentFocusNodeTest.kt
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * Copyright 2020 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.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
-import androidx.compose.ui.graphics.Color.Companion.Red
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class FindParentFocusNodeTest(private val deactivated: Boolean) {
- @get:Rule
- val rule = createComposeRule()
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "isDeactivated = {0}")
- fun initParameters() = listOf(true, false)
- }
-
- @Test
- fun noParentReturnsNull() {
- // Arrange.
- val focusModifier = FocusModifier(Inactive)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(focusModifier))
- }
-
- // Act.
- val rootFocusNode = rule.runOnIdle {
- focusModifier.parent!!.parent
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(rootFocusNode).isNull()
- }
- }
-
- @Test
- fun returnsImmediateParentFromModifierChain() {
- // Arrange.
- // focusNode1--focusNode2--focusNode3--focusNode4--focusNode5
- val modifier1 = FocusModifier(Inactive)
- val modifier2 = FocusModifier(Inactive)
- val modifier3 = FocusModifier(Inactive)
- val modifier4 = FocusModifier(Inactive)
- val modifier5 = FocusModifier(Inactive)
- rule.setFocusableContent {
- Box(
- Modifier
- .focusTarget(modifier1)
- .focusProperties { canFocus = !deactivated }
- .focusTarget(modifier2)
- .focusTarget(modifier3)
- .focusTarget(modifier4)
- .focusTarget(modifier5)
- )
- }
-
- // Act.
- val parent = rule.runOnIdle {
- modifier3.parent
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(parent).isEqualTo(modifier2)
- }
- }
-
- @Test
- fun returnsImmediateParentFromModifierChain_ignoresNonFocusModifiers() {
- // Arrange.
- // focusNode1--focusNode2--nonFocusNode--focusNode3
- val modifier1 = FocusModifier(Inactive)
- val modifier2 = FocusModifier(Inactive)
- val modifier3 = FocusModifier(Inactive)
- rule.setFocusableContent {
- Box(
- Modifier
- .focusTarget(modifier1)
- .focusProperties { canFocus = !deactivated }
- .focusTarget(modifier2)
- .background(color = Red)
- .focusTarget(modifier3)
- )
- }
-
- // Act.
- val parent = rule.runOnIdle {
- modifier3.parent
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(parent).isEqualTo(modifier2)
- }
- }
-
- @Test
- fun returnsLastFocusParentFromParentLayoutNode() {
- // Arrange.
- // parentLayoutNode--parentFocusNode1--parentFocusNode2
- // |
- // layoutNode--focusNode
- val parentFocusModifier1 = FocusModifier(Inactive)
- val parentFocusModifier2 = FocusModifier(Inactive)
- val focusModifier = FocusModifier(Inactive)
- rule.setFocusableContent {
- Box(
- Modifier
- .focusTarget(parentFocusModifier1)
- .focusProperties { canFocus = !deactivated }
- .focusTarget(parentFocusModifier2)
- ) {
- Box(Modifier.focusTarget(focusModifier))
- }
- }
-
- // Act.
- val parent = rule.runOnIdle {
- focusModifier.parent
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(parent).isEqualTo(parentFocusModifier2)
- }
- }
-
- @Test
- fun returnsImmediateParent() {
- // Arrange.
- // greatGrandparentLayoutNode--greatGrandparentFocusNode
- // |
- // grandparentLayoutNode--grandparentFocusNode
- // |
- // parentLayoutNode--parentFocusNode
- // |
- // layoutNode--focusNode
- val greatGrandparentFocusModifier = FocusModifier(Inactive)
- val grandparentFocusModifier = FocusModifier(Inactive)
- val parentFocusModifier = FocusModifier(Inactive)
- val focusModifier = FocusModifier(Inactive)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(greatGrandparentFocusModifier)) {
- Box(Modifier.focusTarget(grandparentFocusModifier)) {
- Box(Modifier
- .focusProperties { canFocus = !deactivated }
- .focusTarget(parentFocusModifier)
- ) {
- Box(Modifier.focusTarget(focusModifier))
- }
- }
- }
- }
-
- // Act.
- val parent = rule.runOnIdle {
- focusModifier.parent
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(parent).isEqualTo(parentFocusModifier)
- }
- }
-
- @Test
- fun ignoresIntermediateLayoutNodesThatDoNotHaveFocusNodes() {
- // Arrange.
- // grandparentLayoutNode--grandparentFocusNode
- // |
- // parentLayoutNode
- // |
- // layoutNode--focusNode
- val greatGrandparentFocusModifier = FocusModifier(Inactive)
- val grandparentFocusModifier = FocusModifier(Inactive)
- val focusModifier = FocusModifier(Inactive)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(greatGrandparentFocusModifier)) {
- Box(Modifier
- .focusProperties { canFocus = !deactivated }
- .focusTarget(grandparentFocusModifier)
- ) {
- Box {
- Box(Modifier.focusTarget(focusModifier))
- }
- }
- }
- }
-
- // Act.
- val parent = rule.runOnIdle {
- focusModifier.parent
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(parent).isEqualTo(grandparentFocusModifier)
- }
- }
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusAggregationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusAggregationTest.kt
index 4adc747..bff41d8 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusAggregationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusAggregationTest.kt
@@ -44,9 +44,11 @@
}
// Assert.
- assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.hasFocus).isFalse()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(focusState.hasFocus).isFalse()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -64,9 +66,11 @@
}
// Assert.
- assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.hasFocus).isFalse()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(focusState.hasFocus).isFalse()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -87,9 +91,11 @@
rule.runOnIdle { focusRequester.requestFocus() }
// Assert.
- assertThat(focusState.isFocused).isTrue()
- assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isTrue()
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -103,9 +109,11 @@
}
// Assert.
- assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.hasFocus).isFalse()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(focusState.hasFocus).isFalse()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -119,9 +127,11 @@
}
// Assert.
- assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.hasFocus).isFalse()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(focusState.hasFocus).isFalse()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -139,9 +149,11 @@
rule.runOnIdle { focusRequester.requestFocus() }
// Assert.
- assertThat(focusState.isFocused).isTrue()
- assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isTrue()
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -160,9 +172,11 @@
rule.runOnIdle { focusRequester.requestFocus() }
// Assert.
- assertThat(focusState.isFocused).isTrue()
- assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isTrue()
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -181,9 +195,11 @@
rule.runOnIdle { focusRequester.requestFocus() }
// Assert.
- assertThat(focusState.isFocused).isTrue()
- assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isTrue()
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -205,9 +221,11 @@
}
// Assert.
- assertThat(focusState.isFocused).isTrue()
- assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isCaptured).isTrue()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isTrue()
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isCaptured).isTrue()
+ }
}
@Test
@@ -229,9 +247,11 @@
}
// Assert.
- assertThat(focusState.isFocused).isTrue()
- assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isCaptured).isTrue()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isTrue()
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isCaptured).isTrue()
+ }
}
@Test
@@ -250,9 +270,11 @@
rule.runOnIdle { focusRequester.requestFocus() }
// Assert.
- assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -272,9 +294,11 @@
rule.runOnIdle { focusRequester.requestFocus() }
// Assert.
- assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -295,9 +319,11 @@
rule.runOnIdle { focusRequester.requestFocus() }
// Assert.
- assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
@Test
@@ -322,8 +348,10 @@
rule.runOnIdle { focusRequester.requestFocus() }
// Assert.
- assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isCaptured).isFalse()
+ rule.runOnIdle {
+ assertThat(focusState.isFocused).isFalse()
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isCaptured).isFalse()
+ }
}
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
index 88b4d3c..7b954bd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
@@ -19,9 +19,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -46,17 +43,16 @@
modifier = Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Active))
+ .focusTarget()
)
}
+ rule.runOnIdle { focusRequester.requestFocus() }
- rule.runOnIdle {
- // Act.
- focusRequester.requestFocus()
+ // Act.
+ rule.runOnIdle { focusRequester.requestFocus() }
- // Assert.
- assertThat(focusState.isFocused).isTrue()
- }
+ // Assert.
+ rule.runOnIdle { assertThat(focusState.isFocused).isTrue() }
}
@ExperimentalComposeUiApi
@@ -86,11 +82,11 @@
assertThat(focusState.hasFocus).isTrue()
}
- rule.runOnIdle {
- // Act.
- focusRequester.requestFocus()
+ // Act.
+ rule.runOnIdle { focusRequester.requestFocus() }
- // Assert.
+ // Assert.
+ rule.runOnIdle {
assertThat(focusState.isFocused).isTrue()
assertThat(childFocusState.isFocused).isFalse()
}
@@ -106,17 +102,20 @@
modifier = Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Captured))
+ .focusTarget()
)
}
-
rule.runOnIdle {
- // Act.
focusRequester.requestFocus()
-
- // Assert.
+ focusRequester.captureFocus()
assertThat(focusState.isCaptured).isTrue()
}
+
+ // Act.
+ rule.runOnIdle { focusRequester.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle { assertThat(focusState.isCaptured).isTrue() }
}
@Test
@@ -134,13 +133,11 @@
)
}
- rule.runOnIdle {
- // Act.
- focusRequester.requestFocus()
+ // Act.
+ rule.runOnIdle { focusRequester.requestFocus() }
- // Assert.
- assertThat(focusState.isDeactivated).isTrue()
- }
+ // Assert.
+ rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
}
@ExperimentalComposeUiApi
@@ -171,18 +168,16 @@
assertThat(childFocusState.isFocused).isTrue()
assertThat(focusState.hasFocus).isTrue()
assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.isDeactivated).isTrue()
}
- rule.runOnIdle {
- // Act.
- focusRequester.requestFocus()
+ // Act.
+ rule.runOnIdle { focusRequester.requestFocus() }
- // Assert.
+ // Assert.
+ rule.runOnIdle {
assertThat(childFocusState.isFocused).isTrue()
assertThat(focusState.hasFocus).isTrue()
assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.isDeactivated).isTrue()
}
}
@@ -196,17 +191,15 @@
modifier = Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
}
- rule.runOnIdle {
- // Act.
- focusRequester.requestFocus()
+ // Act.
+ rule.runOnIdle { focusRequester.requestFocus() }
- // Assert.
- assertThat(focusState.isFocused).isTrue()
- }
+ // Assert.
+ rule.runOnIdle { assertThat(focusState.isFocused).isTrue() }
}
@Test
@@ -236,18 +229,18 @@
.onFocusChanged { focusState5 = it }
.onFocusChanged { focusState6 = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
}
}
}
}
- rule.runOnIdle {
- // Act.
- focusRequester.requestFocus()
+ // Act.
+ rule.runOnIdle { focusRequester.requestFocus() }
- // Assert.
+ // Assert.
+ rule.runOnIdle {
assertThat(focusState1.isFocused).isTrue()
assertThat(focusState2.isFocused).isTrue()
assertThat(focusState3.isFocused).isTrue()
@@ -278,11 +271,11 @@
)
}
- rule.runOnIdle {
- // Act.
- focusRequester.requestFocus()
+ // Act.
+ rule.runOnIdle { focusRequester.requestFocus() }
- // Assert.
+ // Assert.
+ rule.runOnIdle {
assertThat(focusState1.hasFocus).isTrue()
assertThat(focusState2.hasFocus).isTrue()
assertThat(focusState3.isFocused).isTrue()
@@ -290,6 +283,3 @@
}
}
}
-
-private val FocusState.isDeactivated: Boolean
- get() = (this as FocusStateImpl).isDeactivated
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
index 71af6e1..7c6cc56 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusEventCountTest.kt
@@ -20,9 +20,9 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.test.junit4.createComposeRule
@@ -52,14 +52,15 @@
val FocusEventModifierCall: Modifier.((FocusState) -> Unit) -> Modifier = {
focusEventModifier(it)
}
- const val UseOnFocusEvent = "onFocusEvent"
- const val UseFocusEventModifier = "FocusEventModifier"
+ private const val UseOnFocusEvent = "onFocusEvent"
+ private const val UseFocusEventModifier = "FocusEventModifier"
@JvmStatic
@Parameterized.Parameters(name = ">
fun initParameters() = listOf(UseOnFocusEvent, UseFocusEventModifier)
private fun Modifier.focusEventModifier(event: (FocusState) -> Unit) = this.then(
+ @Suppress("DEPRECATION")
object : FocusEventModifier {
override fun onFocusEvent(focusState: FocusState) = event(focusState)
}
@@ -81,11 +82,7 @@
}
// Assert.
- rule.runOnIdle {
- assertThat(focusStates).isExactly(
- Inactive, // triggered by onFocusEvent node's onModifierChanged().
- )
- }
+ rule.runOnIdle { assertThat(focusStates).isExactly(Inactive) }
}
@Test
@@ -199,8 +196,8 @@
// Assert.
rule.runOnIdle {
assertThat(focusStates).isExactly(
- Inactive, // triggered by focus node's state change.
- Inactive, // triggered by onFocusEvent node's onModifierChanged().
+ Inactive, // triggered by clearFocus() of the active node.
+ Inactive, // triggered by onFocusEvent node's attach().
)
}
}
@@ -227,7 +224,63 @@
}
@Test
- fun addingFocusTarget_onFocusEventIsCalledTwice() {
+ fun removingInactiveFocusNode_withActiveChild_onFocusEventIsCalledOnce() {
+ // Arrange.
+ val focusStates = mutableListOf<FocusState>()
+ val focusRequester = FocusRequester()
+ var addFocusTarget by mutableStateOf(true)
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .onFocusEvent { focusStates.add(it) }
+ .then(if (addFocusTarget) Modifier.focusTarget() else Modifier)
+ .focusRequester(focusRequester)
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ focusStates.clear()
+ }
+
+ // Act.
+ rule.runOnIdle { addFocusTarget = false }
+
+ // Assert.
+ rule.runOnIdle { assertThat(focusStates).isExactly(Active) }
+ }
+
+ @Test
+ fun removingInactiveFocusNode_withActiveChildLayout_onFocusEventIsCalledOnce() {
+ // Arrange.
+ val focusStates = mutableListOf<FocusState>()
+ val focusRequester = FocusRequester()
+ var addFocusTarget by mutableStateOf(true)
+ rule.setFocusableContent {
+ Box(Modifier.onFocusEvent { focusStates.add(it) }) {
+ Box(if (addFocusTarget) Modifier.focusTarget() else Modifier) {
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .focusTarget()
+ )
+ }
+ }
+ }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ focusStates.clear()
+ }
+
+ // Act.
+ rule.runOnIdle { addFocusTarget = false }
+
+ // Assert.
+ rule.runOnIdle { assertThat(focusStates).isExactly(Active) }
+ }
+
+ @Test
+ fun addingFocusTarget_onFocusEventIsCalledOnce() {
// Arrange.
val focusStates = mutableListOf<FocusState>()
var addFocusTarget by mutableStateOf(false)
@@ -244,15 +297,11 @@
rule.runOnIdle { addFocusTarget = true }
// Assert.
- rule.runOnIdle {
- assertThat(focusStates).isExactly(
- Inactive, // triggered by focus node's SideEffect.
- )
- }
+ rule.runOnIdle { assertThat(focusStates).isExactly(Inactive) }
}
@Test
- fun addingEmptyFocusProperties_onFocusEventIsCalledTwice() {
+ fun addingEmptyFocusProperties_onFocusEventIsTriggered() {
// Arrange.
val focusStates = mutableListOf<FocusState>()
var addFocusProperties by mutableStateOf(false)
@@ -270,54 +319,168 @@
rule.runOnIdle { addFocusProperties = true }
// Assert.
- rule.runOnIdle {
- assertThat(focusStates).isExactly(
- Inactive, // triggered by focus node's property change.
- )
- }
+ rule.runOnIdle { assertThat(focusStates).isExactly(Inactive) }
}
@Test
- fun deactivatingFocusNode_onFocusEventIsCalledOnce() {
+ fun addingCanFocusProperty_onFocusEventIsTriggered() {
// Arrange.
val focusStates = mutableListOf<FocusState>()
- var deactiated by mutableStateOf(false)
+ var addFocusProperties by mutableStateOf(false)
rule.setFocusableContent {
Box(
modifier = Modifier
.onFocusEvent { focusStates.add(it) }
- .focusProperties { canFocus = !deactiated }
+ .then(
+ if (addFocusProperties) {
+ Modifier.focusProperties { canFocus = true }
+ } else {
+ Modifier
+ }
+ )
.focusTarget()
)
}
rule.runOnIdle { focusStates.clear() }
// Act.
- rule.runOnIdle { deactiated = true }
-
- // Assert.
- rule.runOnIdle { assertThat(focusStates).isExactly(Deactivated) }
- }
-
- @Test
- fun activatingFocusNode_onFocusEventIsCalledOnce() {
- // Arrange.
- val focusStates = mutableListOf<FocusState>()
- var deactiated by mutableStateOf(true)
- rule.setFocusableContent {
- Box(
- modifier = Modifier
- .onFocusEvent { focusStates.add(it) }
- .focusProperties { canFocus = !deactiated }
- .focusTarget()
- )
- }
- rule.runOnIdle { focusStates.clear() }
-
- // Act.
- rule.runOnIdle { deactiated = false }
+ rule.runOnIdle { addFocusProperties = true }
// Assert.
rule.runOnIdle { assertThat(focusStates).isExactly(Inactive) }
}
+
+ @Test
+ fun addingCantFocusProperty_noFocusEventIsTriggered() {
+ // Arrange.
+ val focusStates = mutableListOf<FocusState>()
+ var add by mutableStateOf(false)
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .onFocusEvent { focusStates.add(it) }
+ .then(if (add) Modifier.focusProperties { canFocus = false } else Modifier)
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusStates.clear() }
+
+ // Act.
+ rule.runOnIdle { add = true }
+
+ // Assert.
+ rule.runOnIdle { assertThat(focusStates).isExactly(Inactive) }
+ }
+
+ @Test
+ fun removingCanFocusProperty_onFocusEventIsTriggered() {
+ // Arrange.
+ val focusStates = mutableListOf<FocusState>()
+ var remove by mutableStateOf(false)
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .onFocusEvent { focusStates.add(it) }
+ .then(if (remove) Modifier else Modifier.focusProperties { canFocus = true })
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusStates.clear() }
+
+ // Act.
+ rule.runOnIdle { remove = true }
+
+ // Assert.
+ rule.runOnIdle { assertThat(focusStates).isExactly(Inactive) }
+ }
+
+ @Test
+ fun removingCantFocusProperty_onFocusEventIsTriggered() {
+ // Arrange.
+ val focusStates = mutableListOf<FocusState>()
+ var remove by mutableStateOf(false)
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .onFocusEvent { focusStates.add(it) }
+ .then(if (remove) Modifier else Modifier.focusProperties { canFocus = false })
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusStates.clear() }
+
+ // Act.
+ rule.runOnIdle { remove = true }
+
+ // Assert.
+ rule.runOnIdle { assertThat(focusStates).isExactly(Inactive) }
+ }
+
+ @Test
+ fun deactivatingFocusNode_noFocusEventIsCalledOnce() {
+ // Arrange.
+ val focusStates = mutableListOf<FocusState>()
+ var deactivated by mutableStateOf(false)
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .onFocusEvent { focusStates.add(it) }
+ .focusProperties { canFocus = !deactivated }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusStates.clear() }
+
+ // Act.
+ rule.runOnIdle { deactivated = true }
+
+ // Assert.
+ rule.runOnIdle { assertThat(focusStates).isEmpty() }
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun changingFocusProperty_onFocusEventIsNotCalled() {
+ // Arrange.
+ val focusStates = mutableListOf<FocusState>()
+ val (item1, item2) = FocusRequester.createRefs()
+ var nextItem by mutableStateOf(item1)
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .onFocusEvent { focusStates.add(it) }
+ .focusProperties { next = nextItem }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusStates.clear() }
+
+ // Act.
+ rule.runOnIdle { nextItem = item2 }
+
+ // Assert.
+ rule.runOnIdle { assertThat(focusStates).isEmpty() }
+ }
+
+ @Test
+ fun activatingFocusNode_doesNotTriggerFocusEvent() {
+ // Arrange.
+ val focusStates = mutableListOf<FocusState>()
+ var canFocus by mutableStateOf(false)
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .onFocusEvent { focusStates.add(it) }
+ .focusProperties { this.canFocus = canFocus }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusStates.clear() }
+
+ // Act.
+ rule.runOnIdle { canFocus = true }
+
+ // Assert.
+ rule.runOnIdle { assertThat(focusStates).isEmpty() }
+ }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
index 6f60d99..a462e04 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusManagerCompositionLocalTest.kt
@@ -16,14 +16,20 @@
package androidx.compose.ui.focus
+import android.view.View
import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusStateImpl.Active
+import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
+import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -38,11 +44,10 @@
fun clearFocus_singleLayout() {
// Arrange.
lateinit var focusManager: FocusManager
- lateinit var focusRequester: FocusRequester
lateinit var focusState: FocusState
+ val focusRequester = FocusRequester()
rule.setFocusableContent {
focusManager = LocalFocusManager.current
- focusRequester = remember { FocusRequester() }
Box(
modifier = Modifier
.focusRequester(focusRequester)
@@ -59,20 +64,22 @@
rule.runOnIdle { focusManager.clearFocus() }
// Assert.
- rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
+ rule.runOnIdle {
+ assertThat(focusManager.rootFocusState.isFocused).isTrue()
+ assertThat(focusState.isFocused).isFalse()
+ }
}
@Test
fun clearFocus_entireHierarchyIsCleared() {
// Arrange.
lateinit var focusManager: FocusManager
- lateinit var focusRequester: FocusRequester
lateinit var focusState: FocusState
lateinit var parentFocusState: FocusState
lateinit var grandparentFocusState: FocusState
+ val focusRequester = FocusRequester()
rule.setFocusableContent {
focusManager = LocalFocusManager.current
- focusRequester = remember { FocusRequester() }
Box(
modifier = Modifier
.onFocusChanged { grandparentFocusState = it }
@@ -109,4 +116,321 @@
assertThat(focusState.isFocused).isFalse()
}
}
+
+ @Test
+ fun takeFocus_whenRootIsInactive() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ lateinit var view: View
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ view = LocalView.current
+ Box(
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { view.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusManager.rootFocusState).isEqualTo(Active)
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ fun takeFocus_whenRootIsActive() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ lateinit var view: View
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ view = LocalView.current
+ Box(
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusManager.setRootFocusState(Active) }
+
+ // Act.
+ rule.runOnIdle { view.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusManager.rootFocusState).isEqualTo(Active)
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ @Test
+ fun takeFocus_whenRootIsActiveParent() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ lateinit var view: View
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ view = LocalView.current
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusRequester.requestFocus() }
+
+ // Act.
+ rule.runOnIdle { view.requestFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusManager.rootFocusState).isEqualTo(ActiveParent)
+ assertThat(focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun releaseFocus_whenRootIsInactive() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ lateinit var view: View
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ view = LocalView.current
+ Box(
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { view.clearFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ fun releaseFocus_whenRootIsActive() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ lateinit var view: View
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ view = LocalView.current
+ Box(
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusManager.setRootFocusState(Active) }
+
+ // Act.
+ rule.runOnIdle { view.clearFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ @Ignore("b/257499180")
+ @Test
+ fun releaseFocus_whenRootIsActiveParent() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ lateinit var view: View
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ view = LocalView.current
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusRequester.requestFocus() }
+
+ // Act.
+ rule.runOnIdle {
+ view.clearFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ @Test
+ fun clearFocus_whenRootIsInactive() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { focusManager.clearFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ @Ignore("b/257499180")
+ @Test
+ fun clearFocus_whenRootIsActive() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusManager.setRootFocusState(Active) }
+
+ // Act.
+ rule.runOnIdle { focusManager.clearFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusManager.rootFocusState).isEqualTo(Inactive)
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ @Test
+ fun clearFocus_whenRootIsActiveParent() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusRequester.requestFocus() }
+
+ // Act.
+ rule.runOnIdle { focusManager.clearFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ // TODO(b/257499180): Compose should not hold focus state when clear focus is requested.
+ assertThat(focusManager.rootFocusState).isEqualTo(Active)
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ @Test
+ fun clearFocus_whenHierarchyHasCapturedFocus() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ focusRequester.captureFocus()
+ }
+
+ // Act.
+ rule.runOnIdle { focusManager.clearFocus() }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusManager.rootFocusState).isEqualTo(ActiveParent)
+ assertThat(focusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun clearFocus_forced_whenHierarchyHasCapturedFocus() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ lateinit var focusState: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ focusManager = LocalFocusManager.current
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ focusRequester.captureFocus()
+ }
+
+ // Act.
+ rule.runOnIdle { focusManager.clearFocus(force = true) }
+
+ // Assert.
+ rule.runOnIdle {
+ // TODO(b/257499180): Compose should clear focus and send focus to the root view.
+ assertThat(focusManager.rootFocusState).isEqualTo(Active)
+ assertThat(focusState.isFocused).isFalse()
+ }
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ private val FocusManager.rootFocusState: FocusState
+ get() = (this as FocusOwnerImpl).rootFocusNode.focusState
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ private fun FocusManager.setRootFocusState(focusState: FocusStateImpl) {
+ (this as FocusOwnerImpl).rootFocusNode.focusStateImpl = focusState
+ }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
index 581f8fe7..e9f9614 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
@@ -19,25 +19,30 @@
import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.compose.ui.focus.focusRequester as modifierNodeFocusRequester
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
@MediumTest
-@RunWith(AndroidJUnit4::class)
-class FocusRequesterTest {
+@RunWith(Parameterized::class)
+class FocusRequesterTest(private val modifierNodeVersion: Boolean) {
@get:Rule
val rule = createComposeRule()
@@ -284,12 +289,12 @@
}
}
- @OptIn(ExperimentalComposeUiApi::class)
@Test
fun requestFocus_onDeactivatedParent_focusesOnChild() {
// Arrange.
lateinit var childFocusState: FocusState
- val (focusRequester, initialFocus) = FocusRequester.createRefs()
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
rule.setFocusableContent {
Column(
modifier = Modifier
@@ -363,6 +368,165 @@
}
}
+ @Test
+ fun requestFocus_DisabledParent_doesNotPerformImplicitEnterIfCanFocusIsFalse() {
+ // Arrange.
+ lateinit var childFocusState: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .focusProperties {
+ canFocus = false
+ @OptIn(ExperimentalComposeUiApi::class)
+ enter = { FocusRequester.Cancel }
+ }
+ .focusTarget()
+ ) {
+ Box(
+ modifier = Modifier
+ .onFocusChanged { childFocusState = it }
+ .focusTarget()
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ // Act.
+ focusRequester.requestFocus()
+
+ // Assert.
+ assertThat(childFocusState.isFocused).isFalse()
+ }
+ }
+
+ @Test
+ fun requestFocus_DisabledParents_selectsSiblingFromPathWhereCanFocusIsNotFalse() {
+ // Arrange.
+ lateinit var childFocusState: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .focusProperties { canFocus = false }
+ .focusTarget()
+ ) {
+ Box(
+ modifier = Modifier
+ .onFocusChanged { childFocusState = it }
+ .focusTarget()
+ )
+ Box(
+ modifier = Modifier
+ .focusProperties {
+ canFocus = false
+ @OptIn(ExperimentalComposeUiApi::class)
+ enter = { FocusRequester.Cancel }
+ }
+ .focusTarget()
+ ) {
+ Box(modifier = Modifier.focusTarget())
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ // Act.
+ focusRequester.requestFocus()
+
+ // Assert.
+ assertThat(childFocusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun requestFocus_intermediateDisabledParents_focusesOnLeftMostChild() {
+ // Arrange.
+ lateinit var childFocusState: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ Row(
+ modifier = Modifier
+ .size(100.dp)
+ .focusRequester(focusRequester)
+ .focusProperties { canFocus = false }
+ .focusTarget()
+ ) {
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .onFocusChanged { childFocusState = it }
+ .focusTarget()
+ )
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .focusProperties { canFocus = false }
+ .focusTarget()
+ ) {
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .focusTarget()
+ )
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ // Act.
+ focusRequester.requestFocus()
+
+ // Assert.
+ assertThat(childFocusState.isFocused).isTrue()
+ }
+ }
+
+ @Test
+ fun requestFocus_intermediateDisabledParents_focusesOnLeftMostChild_regardlessOfDepth() {
+ // Arrange.
+ lateinit var childFocusState: FocusState
+ val focusRequester = FocusRequester()
+ rule.setFocusableContent {
+ Row(
+ modifier = Modifier
+ .size(100.dp)
+ .focusRequester(focusRequester)
+ .focusProperties { canFocus = false }
+ .focusTarget()
+ ) {
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .focusProperties { canFocus = false }
+ .focusTarget()
+ ) {
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .onFocusChanged { childFocusState = it }
+ .focusTarget()
+ )
+ }
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .focusTarget()
+ )
+ }
+ }
+
+ rule.runOnIdle {
+ // Act.
+ focusRequester.requestFocus()
+
+ // Assert.
+ assertThat(childFocusState.isFocused).isTrue()
+ }
+ }
+
@OptIn(ExperimentalComposeUiApi::class)
@Test
fun requestFocus_onDeactivatedNode_performsFocusEnter() {
@@ -531,4 +695,28 @@
assertThat(focusState.isFocused).isTrue()
}
}
+
+ private fun Modifier.focusRequester(focusRequester: FocusRequester): Modifier {
+ return if (modifierNodeVersion) {
+ this.modifierNodeFocusRequester(focusRequester)
+ } else {
+ composed(debugInspectorInfo {
+ name = "focusRequester"
+ properties["focusRequester"] = focusRequester
+ }) {
+ remember(focusRequester) {
+ object : @Suppress("DEPRECATION") FocusRequesterModifier {
+ override val focusRequester: FocusRequester
+ get() = focusRequester
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "modifierNodeVersion = {0}")
+ fun initParameters() = listOf(true, false)
+ }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
index 630f5cc..10b7e0e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTargetAttachDetachTest.kt
@@ -118,7 +118,8 @@
var optionalFocusTarget by mutableStateOf(true)
rule.setFocusableContent {
Box(
- modifier = Modifier.onFocusChanged { focusState = it }
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
.focusRequester(focusRequester)
.then(if (optionalFocusTarget) Modifier.focusTarget() else Modifier)
)
@@ -143,7 +144,8 @@
var optionalFocusTarget by mutableStateOf(true)
rule.setFocusableContent {
Box(
- modifier = Modifier.onFocusChanged { focusState = it }
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
.focusRequester(focusRequester)
.then(if (optionalFocusTarget) Modifier.focusTarget() else Modifier)
) {
@@ -170,7 +172,8 @@
var optionalFocusTarget by mutableStateOf(true)
rule.setFocusableContent {
Box(
- modifier = Modifier.onFocusChanged { focusState = it }
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
.focusRequester(focusRequester)
.then(if (optionalFocusTarget) Modifier.focusTarget() else Modifier)
) {
@@ -202,22 +205,27 @@
.onFocusChanged { focusState = it }
.then(if (optionalFocusTarget) Modifier.focusTarget() else Modifier)
) {
- Box(modifier = Modifier
- .focusRequester(focusRequester)
- .focusTarget()
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .focusTarget()
)
}
}
rule.runOnIdle {
focusRequester.requestFocus()
assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isFocused).isFalse()
}
// Act.
rule.runOnIdle { optionalFocusTarget = false }
// Assert.
- rule.runOnIdle { assertThat(focusState.isFocused).isTrue() }
+ rule.runOnIdle {
+ assertThat(focusState.hasFocus).isTrue()
+ assertThat(focusState.isFocused).isTrue()
+ }
}
@Test
@@ -228,7 +236,8 @@
var optionalFocusTarget by mutableStateOf(true)
rule.setFocusableContent {
Box(
- modifier = Modifier.onFocusChanged { focusState = it }
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
.then(
if (optionalFocusTarget) {
Modifier
@@ -267,15 +276,18 @@
.focusTarget()
) {
Box(
- modifier = Modifier.onFocusChanged { focusState = it }.then(
- if (optionalFocusTargets) {
- Modifier.focusTarget()
- .focusRequester(focusRequester)
- .focusTarget()
- } else {
- Modifier
- }
- )
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
+ .then(
+ if (optionalFocusTargets) {
+ Modifier
+ .focusTarget()
+ .focusRequester(focusRequester)
+ .focusTarget()
+ } else {
+ Modifier
+ }
+ )
)
}
}
@@ -314,16 +326,17 @@
Modifier
)
) {
- Box(modifier = Modifier
- .focusRequester(focusRequester)
- .focusTarget()
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .focusTarget()
)
}
}
rule.runOnIdle {
focusRequester.requestFocus()
+ assertThat(focusState.isFocused).isFalse()
assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isDeactivated).isTrue()
}
// Act.
@@ -332,7 +345,6 @@
// Assert.
rule.runOnIdle {
assertThat(focusState.isFocused).isTrue()
- assertThat(focusState.isDeactivated).isFalse()
}
}
@@ -372,7 +384,6 @@
rule.runOnIdle {
focusRequester.requestFocus()
assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isDeactivated).isTrue()
}
// Act.
@@ -382,7 +393,6 @@
rule.runOnIdle {
assertThat(focusState.isFocused).isFalse()
assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isDeactivated).isTrue()
}
}
@@ -401,13 +411,13 @@
) {
Box(
modifier = Modifier.then(
- if (optionalFocusTarget)
- Modifier
- .focusProperties { canFocus = false }
- .focusTarget()
- else
- Modifier
- )
+ if (optionalFocusTarget)
+ Modifier
+ .focusProperties { canFocus = false }
+ .focusTarget()
+ else
+ Modifier
+ )
) {
Box(
modifier = Modifier
@@ -421,7 +431,6 @@
focusRequester.requestFocus()
assertThat(focusState.isFocused).isFalse()
assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isDeactivated).isTrue()
}
// Act.
@@ -431,7 +440,6 @@
rule.runOnIdle {
assertThat(focusState.isFocused).isFalse()
assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isDeactivated).isTrue()
}
}
@@ -475,7 +483,6 @@
rule.runOnIdle {
focusRequester.requestFocus()
assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isDeactivated).isTrue()
}
// Act.
@@ -485,12 +492,11 @@
rule.runOnIdle {
assertThat(focusState.isFocused).isFalse()
assertThat(focusState.hasFocus).isFalse()
- assertThat(focusState.isDeactivated).isTrue()
}
}
@Test
- fun removedNonDeactivatedParentAndActiveChild_grandParent_retainsNonDeactivatedState() {
+ fun removedDeactivatedParentAndActiveChild_grandParent_retainsNonDeactivatedState() {
// Arrange.
lateinit var focusState: FocusState
val focusRequester = FocusRequester()
@@ -503,13 +509,13 @@
) {
Box(
modifier = Modifier.then(
- if (optionalFocusTarget)
- Modifier
- .focusProperties { canFocus = false }
- .focusTarget()
- else
- Modifier
- )
+ if (optionalFocusTarget)
+ Modifier
+ .focusProperties { canFocus = false }
+ .focusTarget()
+ else
+ Modifier
+ )
) {
Box(
modifier = Modifier
@@ -527,7 +533,6 @@
rule.runOnIdle {
focusRequester.requestFocus()
assertThat(focusState.hasFocus).isTrue()
- assertThat(focusState.isDeactivated).isFalse()
}
// Act.
@@ -537,7 +542,6 @@
rule.runOnIdle {
assertThat(focusState.isFocused).isFalse()
assertThat(focusState.hasFocus).isFalse()
- assertThat(focusState.isDeactivated).isFalse()
}
}
@@ -549,18 +553,20 @@
var optionalFocusTarget by mutableStateOf(true)
rule.setFocusableContent {
Box(
- modifier = Modifier.onFocusChanged { focusState = it }
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
.then(if (optionalFocusTarget) Modifier.focusTarget() else Modifier)
.focusRequester(focusRequester)
.focusTarget()
)
}
+ rule.runOnIdle { focusRequester.requestFocus() }
// Act.
rule.runOnIdle { optionalFocusTarget = false }
// Assert.
- rule.runOnIdle { assertThat(focusState.isFocused).isFalse() }
+ rule.runOnIdle { assertThat(focusState.isFocused).isTrue() }
}
@Test
@@ -571,7 +577,8 @@
var addFocusTarget by mutableStateOf(false)
rule.setFocusableContent {
Box(
- modifier = Modifier.onFocusChanged { focusState = it }
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
.focusRequester(focusRequester)
.then(if (addFocusTarget) Modifier.focusTarget() else Modifier)
) {
@@ -598,7 +605,8 @@
var addFocusTarget by mutableStateOf(false)
rule.setFocusableContent {
Box(
- modifier = Modifier.onFocusChanged { focusState = it }
+ modifier = Modifier
+ .onFocusChanged { focusState = it }
.focusRequester(focusRequester)
.then(if (addFocusTarget) Modifier.focusTarget() else Modifier)
)
@@ -641,11 +649,9 @@
// Assert.
rule.runOnIdle {
assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.isDeactivated).isFalse()
}
}
- @Test
fun removingDeactivatedItem_withInactiveNextFocusTarget() {
// Arrange.
lateinit var focusState: FocusState
@@ -673,7 +679,6 @@
// Assert.
rule.runOnIdle {
assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.isDeactivated).isFalse()
}
}
@@ -708,10 +713,6 @@
// Assert.
rule.runOnIdle {
assertThat(focusState.isFocused).isFalse()
- assertThat(focusState.isDeactivated).isTrue()
}
}
}
-
-private val FocusState.isDeactivated: Boolean
- get() = (this as FocusStateImpl).isDeactivated
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
index cfb4a81..20fdc7a 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
@@ -21,10 +21,8 @@
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@@ -39,7 +37,7 @@
*/
internal fun ComposeContentTestRule.setFocusableContent(content: @Composable () -> Unit) {
setContent {
- Box(modifier = Modifier.requiredSize(10.dp, 10.dp)) { content() }
+ Box(modifier = Modifier.requiredSize(100.dp, 100.dp)) { content() }
}
}
@@ -91,13 +89,3 @@
fun IterableSubject.isExactly(vararg expected: Any?) {
return containsExactlyElementsIn(expected).inOrder()
}
-
-/**
- * focusTarget needs a SideEffect to work.
- */
-internal fun Modifier.focusTarget(focusModifier: FocusModifier) = composed {
- SideEffect {
- focusModifier.sendOnFocusEvent()
- }
- this.then(focusModifier).then(ResetFocusModifierLocals)
-}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
index 0ecc2d73a..30cf72a 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FreeFocusTest.kt
@@ -18,10 +18,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -46,15 +42,16 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Active))
+ .focusTarget()
)
}
+ rule.runOnIdle { focusRequester.requestFocus() }
+ // Act.
+ val success = rule.runOnIdle { focusRequester.freeFocus() }
+
+ // Assert.
rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
-
- // Assert.
assertThat(success).isTrue()
assertThat(focusState.isFocused).isTrue()
}
@@ -64,21 +61,28 @@
fun activeParent_freeFocus_retainFocusAsActiveParent() {
// Arrange.
lateinit var focusState: FocusState
+ val initialFocus = FocusRequester()
val focusRequester = FocusRequester()
rule.setFocusableContent {
Box(
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(ActiveParent))
- )
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .focusTarget())
+ }
}
+ rule.runOnIdle { initialFocus.requestFocus() }
+ // Act.
+ val success = rule.runOnIdle { focusRequester.freeFocus() }
+
+ // Assert.
rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
-
- // Assert.
assertThat(success).isFalse()
assertThat(focusState.hasFocus).isTrue()
}
@@ -94,42 +98,26 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Captured))
+ .focusTarget()
)
}
-
rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
+ focusRequester.requestFocus()
+ focusRequester.captureFocus()
+ assertThat(focusState.isFocused).isTrue()
+ assertThat(focusState.isCaptured).isTrue()
+ }
- // Assert.
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.freeFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
assertThat(success).isTrue()
assertThat(focusState.isFocused).isTrue()
- }
- }
-
- @Test
- fun deactivated_freeFocus_retainFocusAsDeactivated() {
- // Arrange.
- lateinit var focusState: FocusState
- val focusRequester = FocusRequester()
- rule.setFocusableContent {
- Box(
- Modifier
- .onFocusChanged { focusState = it }
- .focusRequester(focusRequester)
- .focusProperties { canFocus = false }
- .focusTarget(FocusModifier(Inactive))
- )
- }
-
- rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
-
- // Assert.
- assertThat(success).isFalse()
- assertThat(focusState.isDeactivated).isTrue()
+ assertThat(focusState.isCaptured).isFalse()
}
}
@@ -143,20 +131,19 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
}
- rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.freeFocus()
+ }
- // Assert.
+ // Assert.
+ rule.runOnIdle {
assertThat(success).isFalse()
assertThat(focusState.isFocused).isFalse()
}
}
}
-
-private val FocusState.isDeactivated: Boolean
- get() = (this as FocusStateImpl).isDeactivated
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
index b95a4d2..f388f84 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearchNextTest.kt
@@ -568,6 +568,6 @@
focusManager = LocalFocusManager.current
composable()
}
- rule.runOnIdle { (focusManager as FocusManagerImpl).takeFocus() }
+ rule.runOnIdle { (focusManager as FocusOwnerImpl).takeFocus() }
}
}
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 081774d..74a02fcf 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
@@ -573,6 +573,6 @@
focusManager = LocalFocusManager.current
composable()
}
- rule.runOnIdle { (focusManager as FocusManagerImpl).takeFocus() }
+ rule.runOnIdle { (focusManager as FocusOwnerImpl).takeFocus() }
}
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
index f6c8012..2f3e151 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/RequestFocusTest.kt
@@ -21,8 +21,6 @@
import androidx.compose.ui.focus.FocusStateImpl.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
-import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -41,19 +39,26 @@
@Test
fun active_isUnchanged() {
// Arrange.
- val focusModifier = FocusModifier(Active)
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(focusModifier))
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
}
+ rule.runOnIdle { focusRequester.requestFocus() }
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(focusModifier.focusState).isEqualTo(Active)
+ assertThat(focusState).isEqualTo(Active)
}
}
@@ -61,545 +66,698 @@
fun captured_isUnchanged() {
// Arrange.
- val focusModifier = FocusModifier(Captured)
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(focusModifier))
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle {
+ focusRequester.requestFocus()
+ focusRequester.captureFocus()
}
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(focusModifier.focusState).isEqualTo(Captured)
+ assertThat(focusState).isEqualTo(Captured)
}
}
@Test
fun deactivated_isUnchanged() {
// Arrange.
- val focusModifier = FocusModifier(Inactive)
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
rule.setFocusableContent {
Box(Modifier
+ .focusRequester(focusRequester)
.focusProperties { canFocus = false }
- .focusTarget(focusModifier)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
)
}
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(focusModifier.focusState).isEqualTo(Deactivated)
- }
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun activeParent_withNoFocusedChild_throwsException() {
- // Arrange.
- val focusModifier = FocusModifier(ActiveParent)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(focusModifier))
- }
-
- // Act.
- rule.runOnIdle {
- focusModifier.requestFocus()
+ assertThat(focusState).isEqualTo(Inactive)
}
}
@Test
fun activeParent_propagateFocus() {
// Arrange.
- val focusModifier = FocusModifier(ActiveParent)
- val childFocusModifier = FocusModifier(Active)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+ lateinit var childFocusState: FocusState
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(focusModifier)) {
- Box(Modifier.focusTarget(childFocusModifier))
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()) {
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .onFocusChanged { childFocusState = it }
+ .focusTarget())
}
}
rule.runOnIdle {
- focusModifier.focusedChild = childFocusModifier
+ initialFocus.requestFocus()
}
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(focusModifier.focusState).isEqualTo(Active)
- assertThat(focusModifier.focusedChild).isNull()
- assertThat(childFocusModifier.focusState).isEqualTo(Inactive)
- }
- }
-
- @Test(expected = IllegalArgumentException::class)
- fun deactivatedParent_withNoFocusedChild_throwsException() {
- // Arrange.
- val focusModifier = FocusModifier(DeactivatedParent)
- rule.setFocusableContent {
- Box(Modifier.focusTarget(focusModifier))
- }
-
- // Act.
- rule.runOnIdle {
- focusModifier.requestFocus()
+ assertThat(focusState).isEqualTo(Active)
+ assertThat(childFocusState).isEqualTo(Inactive)
}
}
@Test
fun deactivatedParent_propagateFocus() {
// Arrange.
- val focusModifier = FocusModifier(ActiveParent)
- val childFocusModifier = FocusModifier(Active)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
+ lateinit var childFocusState: FocusState
rule.setFocusableContent {
- Box(Modifier
- .focusProperties { canFocus = false }
- .focusTarget(focusModifier)
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .focusProperties { canFocus = false }
+ .onFocusChanged { focusState = it }
+ .focusTarget()
) {
- Box(Modifier.focusTarget(childFocusModifier))
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .onFocusChanged { childFocusState = it }
+ .focusTarget()
+ )
}
}
rule.runOnIdle {
- focusModifier.focusedChild = childFocusModifier
+ initialFocus.requestFocus()
}
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
// Unchanged.
- assertThat(focusModifier.focusState).isEqualTo(DeactivatedParent)
- assertThat(childFocusModifier.focusState).isEqualTo(Active)
+ assertThat(focusState).isEqualTo(ActiveParent)
+ assertThat(childFocusState).isEqualTo(Active)
}
}
@Test
fun deactivatedParent_activeChild_propagateFocus() {
// Arrange.
- val focusModifier = FocusModifier(ActiveParent)
- val childFocusModifier = FocusModifier(Active)
- val grandchildFocusModifier = FocusModifier(Inactive)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+
+ lateinit var focusState: FocusState
+ lateinit var childFocusState: FocusState
+ lateinit var grandChildFocusState: FocusState
rule.setFocusableContent {
- Box(Modifier
- .focusProperties { canFocus = false }
- .focusTarget(focusModifier)
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .focusProperties { canFocus = false }
+ .onFocusChanged { focusState = it }
+ .focusTarget()
) {
- Box(Modifier.focusTarget(childFocusModifier)) {
- Box(Modifier.focusTarget(grandchildFocusModifier))
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .onFocusChanged { childFocusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .onFocusChanged { grandChildFocusState = it }
+ .focusTarget()
+ )
}
}
}
rule.runOnIdle {
- focusModifier.focusedChild = childFocusModifier
+ initialFocus.requestFocus()
}
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(focusModifier.focusState).isEqualTo(DeactivatedParent)
- assertThat(childFocusModifier.focusState).isEqualTo(Active)
- assertThat(childFocusModifier.focusedChild).isNull()
- assertThat(grandchildFocusModifier.focusState).isEqualTo(Inactive)
+ assertThat(focusState).isEqualTo(ActiveParent)
+ assertThat(childFocusState).isEqualTo(Active)
+ assertThat(grandChildFocusState).isEqualTo(Inactive)
}
}
@Test
fun inactiveRoot_propagateFocusSendsRequestToOwner_systemCanGrantFocus() {
// Arrange.
- val rootFocusModifier = FocusModifier(Inactive)
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(rootFocusModifier))
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
}
// Act.
rule.runOnIdle {
- rootFocusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(rootFocusModifier.focusState).isEqualTo(Active)
+ assertThat(focusState).isEqualTo(Active)
}
}
@Test
fun inactiveRootWithChildren_propagateFocusSendsRequestToOwner_systemCanGrantFocus() {
// Arrange.
- val rootFocusModifier = FocusModifier(Inactive)
- val childFocusModifier = FocusModifier(Inactive)
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
+ lateinit var childFocusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(rootFocusModifier)) {
- Box(Modifier.focusTarget(childFocusModifier))
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .onFocusChanged { childFocusState = it }
+ .focusTarget())
}
}
// Act.
rule.runOnIdle {
- rootFocusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(rootFocusModifier.focusState).isEqualTo(Active)
- assertThat(childFocusModifier.focusState).isEqualTo(Inactive)
+ assertThat(focusState).isEqualTo(Active)
+ assertThat(childFocusState).isEqualTo(Inactive)
}
}
@Test
fun inactiveNonRootWithChildren() {
// Arrange.
- val parentFocusModifier = FocusModifier(Active)
- val focusModifier = FocusModifier(Inactive)
- val childFocusModifier = FocusModifier(Inactive)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
+ lateinit var childFocusState: FocusState
+ lateinit var parentFocusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(Modifier.focusTarget(focusModifier)) {
- Box(Modifier.focusTarget(childFocusModifier))
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .onFocusChanged { parentFocusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .onFocusChanged { childFocusState = it }
+ .focusTarget()
+ )
}
}
}
+ rule.runOnIdle { initialFocus.requestFocus() }
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(parentFocusModifier.focusState).isEqualTo(ActiveParent)
- assertThat(focusModifier.focusState).isEqualTo(Active)
- assertThat(childFocusModifier.focusState).isEqualTo(Inactive)
+ assertThat(parentFocusState).isEqualTo(ActiveParent)
+ assertThat(focusState).isEqualTo(Active)
+ assertThat(childFocusState).isEqualTo(Inactive)
}
}
@Test
fun rootNode() {
// Arrange.
- val rootFocusModifier = FocusModifier(Inactive)
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(rootFocusModifier))
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget())
}
// Act.
rule.runOnIdle {
- rootFocusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(rootFocusModifier.focusState).isEqualTo(Active)
+ assertThat(focusState).isEqualTo(Active)
}
}
@Test
fun rootNodeWithChildren() {
// Arrange.
- val rootFocusModifier = FocusModifier(Inactive)
- val childFocusModifier = FocusModifier(Inactive)
+ lateinit var focusState: FocusState
+ val focusRequester = FocusRequester()
rule.setFocusableContent {
- Box(Modifier.focusTarget(rootFocusModifier)) {
- Box(Modifier.focusTarget(childFocusModifier))
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ ) {
+ Box(Modifier.focusTarget())
}
}
// Act.
rule.runOnIdle {
- rootFocusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(rootFocusModifier.focusState).isEqualTo(Active)
+ assertThat(focusState).isEqualTo(Active)
}
}
@Test
fun parentNodeWithNoFocusedAncestor() {
// Arrange.
- val grandParentFocusModifier = FocusModifier(Inactive)
- val parentFocusModifier = FocusModifier(Inactive)
- val childFocusModifier = FocusModifier(Inactive)
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(grandParentFocusModifier)) {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(Modifier.focusTarget(childFocusModifier))
+ Box(Modifier.focusTarget()) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ ) {
+ Box(Modifier.focusTarget())
}
}
}
// Act.
rule.runOnIdle {
- parentFocusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(parentFocusModifier.focusState).isEqualTo(Active)
+ assertThat(focusState).isEqualTo(Active)
}
}
@Test
fun parentNodeWithNoFocusedAncestor_childRequestsFocus() {
// Arrange.
- val grandParentFocusModifier = FocusModifier(Inactive)
- val parentFocusModifier = FocusModifier(Inactive)
- val childFocusModifier = FocusModifier(Inactive)
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(grandParentFocusModifier)) {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(Modifier.focusTarget(childFocusModifier))
+ Box(Modifier.focusTarget()) {
+ Box(
+ Modifier
+ .onFocusChanged { focusState = it }
+ .focusTarget()) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .focusTarget())
}
}
}
// Act.
rule.runOnIdle {
- childFocusModifier.requestFocus()
+ focusRequester.requestFocus()
}
+
// Assert.
rule.runOnIdle {
- assertThat(parentFocusModifier.focusState).isEqualTo(ActiveParent)
+ assertThat(focusState).isEqualTo(ActiveParent)
}
}
@Test
fun childNodeWithNoFocusedAncestor() {
// Arrange.
- val grandParentFocusModifier = FocusModifier(Inactive)
- val parentFocusModifier = FocusModifier(Inactive)
- val childFocusModifier = FocusModifier(Inactive)
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(grandParentFocusModifier)) {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(Modifier.focusTarget(childFocusModifier))
+ Box(Modifier.focusTarget()) {
+ Box(Modifier.focusTarget()) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget())
}
}
}
// Act.
rule.runOnIdle {
- childFocusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(childFocusModifier.focusState).isEqualTo(Active)
+ assertThat(focusState).isEqualTo(Active)
}
}
@Test
fun requestFocus_parentIsFocused() {
// Arrange.
- val parentFocusModifier = FocusModifier(Active)
- val focusModifier = FocusModifier(Inactive)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+ lateinit var parentFocusState: FocusState
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(Modifier.focusTarget(focusModifier))
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .onFocusChanged { parentFocusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
}
}
+ rule.runOnIdle { initialFocus.requestFocus() }
// After executing requestFocus, siblingNode will be 'Active'.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(parentFocusModifier.focusState).isEqualTo(ActiveParent)
- assertThat(focusModifier.focusState).isEqualTo(Active)
+ assertThat(parentFocusState).isEqualTo(ActiveParent)
+ assertThat(focusState).isEqualTo(Active)
}
}
@Test
fun requestFocus_childIsFocused() {
// Arrange.
- val parentFocusModifier = FocusModifier(ActiveParent)
- val focusModifier = FocusModifier(Active)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+ lateinit var parentFocusState: FocusState
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(Modifier.focusTarget(focusModifier))
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { parentFocusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .onFocusChanged { focusState = it }
+ .focusTarget())
}
}
- rule.runOnIdle {
- parentFocusModifier.focusedChild = focusModifier
- }
+ rule.runOnIdle { initialFocus.requestFocus() }
// Act.
rule.runOnIdle {
- parentFocusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(parentFocusModifier.focusState).isEqualTo(Active)
- assertThat(focusModifier.focusState).isEqualTo(Inactive)
+ assertThat(parentFocusState).isEqualTo(Active)
+ assertThat(focusState).isEqualTo(Inactive)
}
}
@Test
fun requestFocus_childHasCapturedFocus() {
// Arrange.
- val parentFocusModifier = FocusModifier(ActiveParent)
- val focusModifier = FocusModifier(Captured)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
+ lateinit var childFocusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(Modifier.focusTarget(focusModifier))
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .onFocusChanged { childFocusState = it }
+ .focusTarget()
+ )
}
}
rule.runOnIdle {
- parentFocusModifier.focusedChild = focusModifier
+ initialFocus.requestFocus()
+ initialFocus.captureFocus()
}
// Act.
rule.runOnIdle {
- parentFocusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(parentFocusModifier.focusState).isEqualTo(ActiveParent)
- assertThat(focusModifier.focusState).isEqualTo(Captured)
+ assertThat(focusState).isEqualTo(ActiveParent)
+ assertThat(childFocusState).isEqualTo(Captured)
}
}
@Test
fun requestFocus_siblingIsFocused() {
// Arrange.
- val parentFocusModifier = FocusModifier(ActiveParent)
- val focusModifier = FocusModifier(Inactive)
- val siblingModifier = FocusModifier(Active)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+ lateinit var parentFocusState: FocusState
+ lateinit var focusState: FocusState
+ lateinit var siblingFocusState: FocusState
+
rule.setFocusableContent {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(Modifier.focusTarget(focusModifier))
- Box(Modifier.focusTarget(siblingModifier))
+ Box(
+ Modifier
+ .onFocusChanged { parentFocusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .onFocusChanged { siblingFocusState = it }
+ .focusTarget()
+ )
}
}
- rule.runOnIdle {
- parentFocusModifier.focusedChild = siblingModifier
- }
+ rule.runOnIdle { initialFocus.requestFocus() }
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(parentFocusModifier.focusState).isEqualTo(ActiveParent)
- assertThat(focusModifier.focusState).isEqualTo(Active)
- assertThat(siblingModifier.focusState).isEqualTo(Inactive)
+ assertThat(parentFocusState).isEqualTo(ActiveParent)
+ assertThat(focusState).isEqualTo(Active)
+ assertThat(siblingFocusState).isEqualTo(Inactive)
}
}
@Test
fun requestFocus_siblingHasCapturedFocused() {
// Arrange.
- val parentFocusModifier = FocusModifier(ActiveParent)
- val focusModifier = FocusModifier(Inactive)
- val siblingModifier = FocusModifier(Captured)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+ lateinit var parentFocusState: FocusState
+ lateinit var focusState: FocusState
+ lateinit var siblingFocusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(parentFocusModifier)) {
- Box(Modifier.focusTarget(focusModifier))
- Box(Modifier.focusTarget(siblingModifier))
+ Box(
+ Modifier
+ .onFocusChanged { parentFocusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget())
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .onFocusChanged { siblingFocusState = it }
+ .focusTarget())
}
}
rule.runOnIdle {
- parentFocusModifier.focusedChild = siblingModifier
+ initialFocus.requestFocus()
+ initialFocus.captureFocus()
}
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(parentFocusModifier.focusState).isEqualTo(ActiveParent)
- assertThat(focusModifier.focusState).isEqualTo(Inactive)
- assertThat(siblingModifier.focusState).isEqualTo(Captured)
+ assertThat(parentFocusState).isEqualTo(ActiveParent)
+ assertThat(focusState).isEqualTo(Inactive)
+ assertThat(siblingFocusState).isEqualTo(Captured)
}
}
@Test
fun requestFocus_cousinIsFocused() {
// Arrange.
- val grandParentModifier = FocusModifier(ActiveParent)
- val parentModifier = FocusModifier(Inactive)
- val focusModifier = FocusModifier(Inactive)
- val auntModifier = FocusModifier(ActiveParent)
- val cousinModifier = FocusModifier(Active)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(grandParentModifier)) {
- Box(Modifier.focusTarget(parentModifier)) {
- Box(Modifier.focusTarget(focusModifier))
+ Box(Modifier.focusTarget()) {
+ Box(Modifier.focusTarget()) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
}
- Box(Modifier.focusTarget(auntModifier)) {
- Box(Modifier.focusTarget(cousinModifier))
+ Box(Modifier.focusTarget()) {
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .focusTarget())
}
}
}
rule.runOnIdle {
- grandParentModifier.focusedChild = auntModifier
- auntModifier.focusedChild = cousinModifier
- }
-
- // Verify Setup.
- rule.runOnIdle {
- assertThat(cousinModifier.focusState).isEqualTo(Active)
- assertThat(focusModifier.focusState).isEqualTo(Inactive)
+ initialFocus.requestFocus()
}
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(cousinModifier.focusState).isEqualTo(Inactive)
- assertThat(focusModifier.focusState).isEqualTo(Active)
+ assertThat(focusState).isEqualTo(Active)
}
}
@Test
fun requestFocus_grandParentIsFocused() {
// Arrange.
- val grandParentModifier = FocusModifier(Active)
- val parentModifier = FocusModifier(Inactive)
- val focusModifier = FocusModifier(Inactive)
+ val initialFocus = FocusRequester()
+ val focusRequester = FocusRequester()
+ lateinit var grandParentFocusState: FocusState
+ lateinit var parentFocusState: FocusState
+ lateinit var focusState: FocusState
rule.setFocusableContent {
- Box(Modifier.focusTarget(grandParentModifier)) {
- Box(Modifier.focusTarget(parentModifier)) {
- Box(Modifier.focusTarget(focusModifier))
+ Box(
+ Modifier
+ .focusRequester(initialFocus)
+ .onFocusChanged { grandParentFocusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .onFocusChanged { parentFocusState = it }
+ .focusTarget()
+ ) {
+ Box(
+ Modifier
+ .focusRequester(focusRequester)
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
}
}
}
+ rule.runOnIdle { initialFocus.requestFocus() }
// Act.
rule.runOnIdle {
- focusModifier.requestFocus()
+ focusRequester.requestFocus()
}
// Assert.
rule.runOnIdle {
- assertThat(grandParentModifier.focusState).isEqualTo(ActiveParent)
- assertThat(parentModifier.focusState).isEqualTo(ActiveParent)
- assertThat(focusModifier.focusState).isEqualTo(Active)
+ assertThat(grandParentFocusState).isEqualTo(ActiveParent)
+ assertThat(parentFocusState).isEqualTo(ActiveParent)
+ assertThat(focusState).isEqualTo(Active)
}
}
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
index 84ee29a..1feafc4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterCaptureFocusTest.kt
@@ -18,9 +18,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -45,15 +42,18 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Active))
+ .focusTarget()
)
}
+ rule.runOnIdle { focusRequester.requestFocus() }
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.captureFocus()
+ }
+
+ // Assert.
rule.runOnIdle {
- // Act.
- val success = focusRequester.captureFocus()
-
- // Assert.
assertThat(success).isTrue()
assertThat(focusState.isCaptured).isTrue()
}
@@ -69,15 +69,19 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Captured))
+ .focusTarget()
)
}
-
rule.runOnIdle {
- // Act.
- val success = focusRequester.captureFocus()
+ focusRequester.requestFocus()
+ focusRequester.captureFocus()
+ }
- // Assert.
+ // Act.
+ val success = rule.runOnIdle { focusRequester.captureFocus() }
+
+ // Assert.
+ rule.runOnIdle {
assertThat(success).isTrue()
assertThat(focusState.isCaptured).isTrue()
}
@@ -93,15 +97,17 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
}
- rule.runOnIdle {
- // Act.
- val success = focusRequester.captureFocus()
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.captureFocus()
+ }
- // Assert.
+ // Assert.
+ rule.runOnIdle {
assertThat(success).isFalse()
assertThat(focusState.isFocused).isFalse()
}
@@ -112,27 +118,32 @@
// Arrange.
lateinit var focusState1: FocusState
lateinit var focusState2: FocusState
+ val initialFocus = FocusRequester()
val focusRequester = FocusRequester()
rule.setFocusableContent {
Box(
Modifier
.onFocusChanged { focusState1 = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
Box(
Modifier
.onFocusChanged { focusState2 = it }
+ .focusRequester(initialFocus)
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Active))
+ .focusTarget()
)
}
+ rule.runOnIdle { initialFocus.requestFocus() }
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.captureFocus()
+ }
+
+ // Assert.
rule.runOnIdle {
- // Act.
- val success = focusRequester.captureFocus()
-
- // Assert.
assertThat(success).isTrue()
assertThat(focusState1.isFocused).isFalse()
assertThat(focusState2.isCaptured).isTrue()
@@ -144,27 +155,33 @@
// Arrange.
lateinit var focusState1: FocusState
lateinit var focusState2: FocusState
+ val initialFocus = FocusRequester()
val focusRequester = FocusRequester()
rule.setFocusableContent {
Box(
Modifier
.onFocusChanged { focusState1 = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
Box(
Modifier
.onFocusChanged { focusState2 = it }
+ .focusRequester(initialFocus)
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Captured))
+ .focusTarget()
)
}
-
rule.runOnIdle {
- // Act.
- val success = focusRequester.captureFocus()
+ initialFocus.requestFocus()
+ initialFocus.captureFocus()
+ }
- // Assert.
+ // Act.
+ val success = rule.runOnIdle { focusRequester.captureFocus() }
+
+ // Assert.
+ rule.runOnIdle {
assertThat(success).isTrue()
assertThat(focusState1.isFocused).isFalse()
assertThat(focusState2.isCaptured).isTrue()
@@ -182,21 +199,23 @@
Modifier
.onFocusChanged { focusState1 = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
Box(
Modifier
.onFocusChanged { focusState2 = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
}
- rule.runOnIdle {
- // Act.
- val success = focusRequester.captureFocus()
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.captureFocus()
+ }
- // Assert.
+ // Assert.
+ rule.runOnIdle {
assertThat(success).isFalse()
assertThat(focusState1.isFocused).isFalse()
assertThat(focusState2.isFocused).isFalse()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
index 9805036..f44e08a 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/ReusedFocusRequesterFreeFocusTest.kt
@@ -18,9 +18,6 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -45,15 +42,18 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Active))
+ .focusTarget()
)
}
+ rule.runOnIdle { focusRequester.requestFocus() }
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.freeFocus()
+ }
+
+ // Assert.
rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
-
- // Assert.
assertThat(success).isTrue()
assertThat(focusState.isFocused).isTrue()
}
@@ -69,15 +69,19 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Captured))
+ .focusTarget()
)
}
-
rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
+ focusRequester.requestFocus()
+ focusRequester.captureFocus()
+ }
- // Assert.
+ // Act.
+ val success = rule.runOnIdle { focusRequester.freeFocus() }
+
+ // Assert.
+ rule.runOnIdle {
assertThat(success).isTrue()
assertThat(focusState.isFocused).isTrue()
}
@@ -93,15 +97,17 @@
Modifier
.onFocusChanged { focusState = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
}
- rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.freeFocus()
+ }
- // Assert.
+ // Assert.
+ rule.runOnIdle {
assertThat(success).isFalse()
assertThat(focusState.isFocused).isFalse()
}
@@ -112,27 +118,32 @@
// Arrange.
lateinit var focusState1: FocusState
lateinit var focusState2: FocusState
+ val initialFocus = FocusRequester()
val focusRequester = FocusRequester()
rule.setFocusableContent {
Box(
Modifier
.onFocusChanged { focusState1 = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
Box(
Modifier
.onFocusChanged { focusState2 = it }
+ .focusRequester(initialFocus)
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Active))
+ .focusTarget()
)
}
+ rule.runOnIdle { initialFocus.requestFocus() }
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.freeFocus()
+ }
+
+ // Assert.
rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
-
- // Assert.
assertThat(success).isTrue()
assertThat(focusState1.isFocused).isFalse()
assertThat(focusState2.isFocused).isTrue()
@@ -144,27 +155,35 @@
// Arrange.
lateinit var focusState1: FocusState
lateinit var focusState2: FocusState
+ val initialFocus = FocusRequester()
val focusRequester = FocusRequester()
rule.setFocusableContent {
Box(
Modifier
.onFocusChanged { focusState1 = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
Box(
Modifier
.onFocusChanged { focusState2 = it }
+ .focusRequester(initialFocus)
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Captured))
+ .focusTarget()
)
}
-
rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
+ initialFocus.requestFocus()
+ initialFocus.captureFocus()
+ }
- // Assert.
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.freeFocus()
+ }
+
+ // Assert.
+ rule.runOnIdle {
assertThat(success).isTrue()
assertThat(focusState1.isFocused).isFalse()
assertThat(focusState2.isFocused).isTrue()
@@ -182,21 +201,23 @@
Modifier
.onFocusChanged { focusState1 = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
Box(
Modifier
.onFocusChanged { focusState2 = it }
.focusRequester(focusRequester)
- .focusTarget(FocusModifier(Inactive))
+ .focusTarget()
)
}
- rule.runOnIdle {
- // Act.
- val success = focusRequester.freeFocus()
+ // Act.
+ val success = rule.runOnIdle {
+ focusRequester.freeFocus()
+ }
- // Assert.
+ // Assert.
+ rule.runOnIdle {
assertThat(success).isFalse()
assertThat(focusState1.isFocused).isFalse()
assertThat(focusState2.isFocused).isFalse()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
index cfa3544..3350d69 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.input.focus
+import android.view.KeyEvent as AndroidKeyEvent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
@@ -25,21 +26,26 @@
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.focus.setFocusableContent
+import androidx.compose.ui.input.focus.FocusAwareEventPropagationTest.NodeType.KeyInput
+import androidx.compose.ui.input.focus.FocusAwareEventPropagationTest.NodeType.RotaryInput
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyInputInputModifierNodeImpl
+import androidx.compose.ui.input.rotary.RotaryInputModifierNodeImpl
+import androidx.compose.ui.input.rotary.RotaryScrollEvent
+import androidx.compose.ui.node.modifierElementOf
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyPress
+import androidx.compose.ui.test.performRotaryScrollInput
import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import androidx.compose.ui.input.rotary.RotaryScrollEvent as FocusAwareTestEvent
-import androidx.compose.ui.input.rotary.onPreRotaryScrollEvent as onPreFocusAwareEvent
-import androidx.compose.ui.input.rotary.onRotaryScrollEvent as onFocusAwareEvent
-import androidx.compose.ui.test.performRotaryScrollInput as performFocusAwareInput
+import org.junit.runners.Parameterized
/**
* Focus-aware event propagation test.
@@ -50,20 +56,31 @@
*/
@OptIn(ExperimentalComposeUiApi::class)
@MediumTest
-@RunWith(AndroidJUnit4::class)
-class FocusAwareEventPropagationTest {
+@RunWith(Parameterized::class)
+class FocusAwareEventPropagationTest(private val nodeType: NodeType) {
@get:Rule
val rule = createComposeRule()
@OptIn(ExperimentalComposeUiApi::class)
- private val sentEvent: FocusAwareTestEvent =
- FocusAwareTestEvent(1f, 2f, 3L)
- private var receivedEvent: FocusAwareTestEvent? = null
+ private val sentEvent: Any = when (nodeType) {
+ KeyInput ->
+ KeyEvent(AndroidKeyEvent(AndroidKeyEvent.ACTION_DOWN, AndroidKeyEvent.KEYCODE_A))
+ RotaryInput ->
+ RotaryScrollEvent(1f, 1f, 0L)
+ }
+ private var receivedEvent: Any? = null
private val initialFocus = FocusRequester()
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "node = {0}")
+ fun initParameters() = arrayOf(KeyInput, RotaryInput)
+ }
+
@Test
fun noFocusable_doesNotDeliverEvent() {
// Arrange.
+ var error: IllegalStateException? = null
rule.setContent {
Box(
modifier = Modifier.onFocusAwareEvent {
@@ -74,15 +91,24 @@
}
// Act.
- rule.onRoot().performFocusAwareInput(sentEvent)
+ try {
+ rule.onRoot().performFocusAwareInput(sentEvent)
+ } catch (exception: IllegalStateException) {
+ error = exception
+ }
// Assert.
assertThat(receivedEvent).isNull()
+ when (nodeType) {
+ KeyInput -> assertThat(error!!.message).contains("do not have an active focus target")
+ RotaryInput -> assertThat(error).isNull()
+ }
}
@Test
fun unfocusedFocusable_doesNotDeliverEvent() {
// Arrange.
+ var error: IllegalStateException? = null
rule.setFocusableContent {
Box(
modifier = Modifier
@@ -95,10 +121,18 @@
}
// Act.
- rule.onRoot().performFocusAwareInput(sentEvent)
+ try {
+ rule.onRoot().performFocusAwareInput(sentEvent)
+ } catch (exception: IllegalStateException) {
+ error = exception
+ }
// Assert.
assertThat(receivedEvent).isNull()
+ when (nodeType) {
+ KeyInput -> assertThat(error!!.message).contains("do not have an active focus target")
+ RotaryInput -> assertThat(error).isNull()
+ }
}
@Test
@@ -120,7 +154,10 @@
rule.onRoot().performFocusAwareInput(sentEvent)
// Assert.
- assertThat(receivedEvent).isNull()
+ when (nodeType) {
+ KeyInput -> assertThat(receivedEvent).isEqualTo(sentEvent)
+ RotaryInput -> assertThat(receivedEvent).isNull()
+ }
}
@Test
@@ -141,7 +178,10 @@
rule.onRoot().performFocusAwareInput(sentEvent)
// Assert.
- assertThat(receivedEvent).isNull()
+ when (nodeType) {
+ KeyInput -> assertThat(receivedEvent).isEqualTo(sentEvent)
+ RotaryInput -> assertThat(receivedEvent).isNull()
+ }
}
@Test
@@ -162,15 +202,11 @@
rule.onRoot().performFocusAwareInput(sentEvent)
// Assert.
- rule.runOnIdle {
- // performFocusAwareInput generates a vertical scroll
- assertThat(sentEvent.verticalScrollPixels)
- .isEqualTo(receivedEvent?.verticalScrollPixels)
- }
+ rule.runOnIdle { assertThat(sentEvent).isEqualTo(receivedEvent) }
}
@Test
- fun onPreviewKeyEvent_triggered() {
+ fun onPreFocusAwareEvent_triggered() {
// Arrange.
ContentWithInitialFocus {
Box(
@@ -187,11 +223,7 @@
rule.onRoot().performFocusAwareInput(sentEvent)
// Assert.
- rule.runOnIdle {
- // performFocusAwareInput generates a vertical scroll
- assertThat(sentEvent.verticalScrollPixels)
- .isEqualTo(receivedEvent?.verticalScrollPixels)
- }
+ rule.runOnIdle { assertThat(sentEvent).isEqualTo(receivedEvent) }
}
@Test
@@ -472,10 +504,19 @@
.then(if (initiallyFocused) Modifier.focusRequester(initialFocus) else Modifier)
.focusTarget()
- private fun SemanticsNodeInteraction.performFocusAwareInput(sentEvent: FocusAwareTestEvent) {
- @OptIn(ExperimentalTestApi::class)
- performFocusAwareInput {
- rotateToScrollVertically(sentEvent.verticalScrollPixels)
+ private fun SemanticsNodeInteraction.performFocusAwareInput(sentEvent: Any) {
+ when (nodeType) {
+ KeyInput -> {
+ check(sentEvent is KeyEvent)
+ performKeyPress(sentEvent)
+ }
+ RotaryInput -> {
+ check(sentEvent is RotaryScrollEvent)
+ @OptIn(ExperimentalTestApi::class)
+ performRotaryScrollInput {
+ rotateToScrollVertically(sentEvent.verticalScrollPixels)
+ }
+ }
}
}
@@ -485,4 +526,54 @@
}
rule.runOnIdle { initialFocus.requestFocus() }
}
+
+ private fun Modifier.onFocusAwareEvent(onEvent: (Any) -> Boolean): Modifier = this.then(
+ when (nodeType) {
+ KeyInput -> modifierElementOf(
+ key = onEvent,
+ create = { KeyInputInputModifierNodeImpl( },
+ update = { it. },
+ definitions = {
+ name = "onEvent"
+ properties["onEvent"] = onEvent
+ }
+ )
+
+ RotaryInput -> modifierElementOf(
+ key = onEvent,
+ create = { RotaryInputModifierNodeImpl( },
+ update = { it. },
+ definitions = {
+ name = "onEvent"
+ properties["onEvent"] = onEvent
+ }
+ )
+ }
+ )
+
+ private fun Modifier.onPreFocusAwareEvent(onEvent: (Any) -> Boolean): Modifier = this.then(
+ when (nodeType) {
+ KeyInput -> modifierElementOf(
+ key = onEvent,
+ create = { KeyInputInputModifierNodeImpl( },
+ update = { it. },
+ definitions = {
+ name = "onEvent"
+ properties["onEvent"] = onEvent
+ }
+ )
+
+ RotaryInput -> modifierElementOf(
+ key = onEvent,
+ create = { RotaryInputModifierNodeImpl( },
+ update = { it. },
+ definitions = {
+ name = "onEvent"
+ properties["onEvent"] = onEvent
+ }
+ )
+ }
+ )
+
+ enum class NodeType { KeyInput, RotaryInput }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
index cc1722b..0c88d69 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/ProcessKeyInputTest.kt
@@ -16,17 +16,20 @@
package androidx.compose.ui.input.key
+import android.view.KeyEvent as AndroidKeyEvent
+import android.view.KeyEvent.KEYCODE_A as KeyCodeA
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.ACTION_UP
import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.focus.setFocusableContent
-import android.view.KeyEvent.KEYCODE_A as KeyCodeA
-import android.view.KeyEvent as AndroidKeyEvent
-import android.view.KeyEvent.ACTION_DOWN
-import android.view.KeyEvent.ACTION_UP
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.focus.focusTarget
+import androidx.compose.ui.focus.setFocusableContent
import androidx.compose.ui.input.key.Key.Companion.A
import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
@@ -40,7 +43,6 @@
import org.junit.Test
import org.junit.runner.RunWith
-@Suppress("DEPRECATION")
@MediumTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalComposeUiApi::class)
@@ -52,7 +54,7 @@
fun noRootFocusTarget_throwsException() {
// Arrange.
rule.setContent {
- Box(modifier = KeyInputModifier(null, null))
+ Box(modifier = Modifier.onKeyEvent { false })
}
// Act.
@@ -75,7 +77,9 @@
// Arrange.
rule.setFocusableContent {
- Box(modifier = Modifier.focusTarget().onKeyEvent { true })
+ Box(modifier = Modifier
+ .focusTarget()
+ .onKeyEvent { true })
}
// Act.
@@ -219,6 +223,82 @@
}
@Test
+ fun onKeyEvent_afterUpdate() {
+ // Arrange.
+ val focusRequester = FocusRequester()
+ var keyEventFromOnKeyEvent1: KeyEvent? = null
+ var keyEventFromOnKeyEvent2: KeyEvent? = null
+ var onKeyEvent: (event: KeyEvent) -> Boolean by mutableStateOf(
+ value = {
+ keyEventFromOnKeyEvent1 = it
+ true
+ }
+ )
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onKeyEvent(onKeyEvent)
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusRequester.requestFocus() }
+
+ // Act.
+ rule.runOnIdle {
+ >
+ keyEventFromOnKeyEvent2 = it
+ true
+ }
+ }
+ rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(keyEventFromOnKeyEvent1).isNull()
+ assertThat(keyEventFromOnKeyEvent2).isNotNull()
+ }
+ }
+
+ @Test
+ fun onPreviewKeyEvent_afterUpdate() {
+ // Arrange.
+ val focusRequester = FocusRequester()
+ var keyEventFromOnPreviewKeyEvent1: KeyEvent? = null
+ var keyEventFromOnPreviewKeyEvent2: KeyEvent? = null
+ var onPreviewKeyEvent: (event: KeyEvent) -> Boolean by mutableStateOf(
+ value = {
+ keyEventFromOnPreviewKeyEvent1 = it
+ true
+ }
+ )
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onPreviewKeyEvent(onPreviewKeyEvent)
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusRequester.requestFocus() }
+
+ // Act.
+ rule.runOnIdle {
+ >
+ keyEventFromOnPreviewKeyEvent2 = it
+ true
+ }
+ }
+ rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(keyEventFromOnPreviewKeyEvent1).isNull()
+ assertThat(keyEventFromOnPreviewKeyEvent2).isNotNull()
+ }
+ }
+
+ @Test
fun parent_child() {
// Arrange.
val focusRequester = FocusRequester()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index 2d2932e..272cf37 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -23,7 +23,7 @@
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusOwner
import androidx.compose.ui.geometry.MutableRect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -3644,7 +3644,7 @@
get() = TODO("Not yet implemented")
override val pointerIconService: PointerIconService
get() = TODO("Not yet implemented")
- override val focusManager: FocusManager
+ override val focusOwner: FocusOwner
get() = TODO("Not yet implemented")
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index e8bf395..c56ae6e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -24,7 +24,7 @@
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusOwner
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.hapticfeedback.HapticFeedback
@@ -3307,7 +3307,7 @@
get() = TODO("Not yet implemented")
override val pointerIconService: PointerIconService
get() = TODO("Not yet implemented")
- override val focusManager: FocusManager
+ override val focusOwner: FocusOwner
get() = TODO("Not yet implemented")
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt
index e58a234..00613cd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEventTest.kt
@@ -22,11 +22,15 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusTarget
+import androidx.compose.ui.focus.setFocusableContent
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createComposeRule
@@ -214,6 +218,84 @@
}
}
+ @Test
+ fun onRotaryKeyEvent_afterUpdate() {
+ // Arrange.
+ val focusRequester = FocusRequester()
+ var keyEventFromOnKeyEvent1: RotaryScrollEvent? = null
+ var keyEventFromOnKeyEvent2: RotaryScrollEvent? = null
+ var onRotaryScrollEvent: (event: RotaryScrollEvent) -> Boolean by mutableStateOf(
+ value = {
+ keyEventFromOnKeyEvent1 = it
+ true
+ }
+ )
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onRotaryScrollEvent(onRotaryScrollEvent)
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusRequester.requestFocus() }
+
+ // Act.
+ rule.runOnIdle {
+ >
+ keyEventFromOnKeyEvent2 = it
+ true
+ }
+ }
+ @OptIn(ExperimentalTestApi::class)
+ rule.onRoot().performRotaryScrollInput { rotateToScrollVertically(3.0f) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(keyEventFromOnKeyEvent1).isNull()
+ assertThat(keyEventFromOnKeyEvent2).isNotNull()
+ }
+ }
+
+ @Test
+ fun onRotaryPreviewKeyEvent_afterUpdate() {
+ // Arrange.
+ val focusRequester = FocusRequester()
+ var keyEventFromOnPreRotaryScrollEvent1: RotaryScrollEvent? = null
+ var keyEventFromOnPreRotaryScrollEvent2: RotaryScrollEvent? = null
+ var onPreRotaryScrollEvent: (event: RotaryScrollEvent) -> Boolean by mutableStateOf(
+ value = {
+ keyEventFromOnPreRotaryScrollEvent1 = it
+ true
+ }
+ )
+ rule.setFocusableContent {
+ Box(
+ modifier = Modifier
+ .focusRequester(focusRequester)
+ .onPreRotaryScrollEvent(onPreRotaryScrollEvent)
+ .focusTarget()
+ )
+ }
+ rule.runOnIdle { focusRequester.requestFocus() }
+
+ // Act.
+ rule.runOnIdle {
+ >
+ keyEventFromOnPreRotaryScrollEvent2 = it
+ true
+ }
+ }
+ @OptIn(ExperimentalTestApi::class)
+ rule.onRoot().performRotaryScrollInput { rotateToScrollVertically(3.0f) }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(keyEventFromOnPreRotaryScrollEvent1).isNull()
+ assertThat(keyEventFromOnPreRotaryScrollEvent2).isNotNull()
+ }
+ }
+
private fun Modifier.focusable(initiallyFocused: Boolean = false) = this
.then(if (initiallyFocused) Modifier.focusRequester(initialFocus) else Modifier)
.focusTarget()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index a24680c..0d06482 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -19,7 +19,7 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusOwner
import androidx.compose.ui.geometry.MutableRect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
@@ -153,7 +153,7 @@
get() = TODO("Not yet implemented")
override val pointerIconService: PointerIconService
get() = TODO("Not yet implemented")
- override val focusManager: FocusManager
+ override val focusOwner: FocusOwner
get() = TODO("Not yet implemented")
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
index 40d0700..f228f88 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
@@ -21,6 +21,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusTargetModifierNode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
@@ -135,6 +136,113 @@
}
@Test
+ fun visitSubtreeIf_stopsIfWeReturnFalse() {
+ // Arrange.
+ val (node1, node2, node3) = List(3) { object : Modifier.Node() {} }
+ val (node4, node5, node6) = List(3) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(modifier = modifierElementOf { node1 }) {
+ Box(modifier = modifierElementOf { node2 }) {
+ Box(modifier = modifierElementOf { node3 })
+ Box(modifier = modifierElementOf { node4 }) {
+ Box(modifier = modifierElementOf { node6 })
+ }
+ Box(modifier = modifierElementOf { node5 })
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node1.visitSubtreeIf(Nodes.Any) {
+ visitedChildren.add(it)
+ // return false if we encounter node4
+ it != node4
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(node2, node3, node4, node5).inOrder()
+ }
+
+ @Test
+ fun visitSubtreeIf_continuesIfWeReturnTrue() {
+ // Arrange.
+ val (node1, node2, node3) = List(3) { object : Modifier.Node() {} }
+ val (node4, node5, node6) = List(3) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(modifier = modifierElementOf { node1 }) {
+ Box(modifier = modifierElementOf { node2 }) {
+ Box(modifier = modifierElementOf { node3 })
+ Box(modifier = modifierElementOf { node4 }) {
+ Box(modifier = modifierElementOf { node6 })
+ }
+ Box(modifier = modifierElementOf { node5 })
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node1.visitSubtreeIf(Nodes.Any) {
+ visitedChildren.add(it)
+ true
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren).containsExactly(node2, node3, node4, node6, node5).inOrder()
+ }
+
+ @Test
+ fun visitSubtree_visitsItemsInCurrentLayoutNode() {
+ // Arrange.
+ val (node1, node2, node3, node4, node5) = List(6) { object : Modifier.Node() {} }
+ val (node6, node7, node8, node9, node10) = List(6) { object : Modifier.Node() {} }
+ val visitedChildren = mutableListOf<Modifier.Node>()
+ rule.setContent {
+ Box(
+ modifier = modifierElementOf { node1 }
+ .then(modifierElementOf { node2 })
+ ) {
+ Box(
+ modifier = modifierElementOf { node3 }
+ .then(modifierElementOf { node4 })
+ ) {
+ Box(
+ modifier = modifierElementOf { node7 }
+ .then(modifierElementOf { node8 })
+ )
+ }
+ Box(
+ modifier = modifierElementOf { node5 }
+ .then(modifierElementOf { node6 })
+ ) {
+ Box(
+ modifier = modifierElementOf { node9 }
+ .then(modifierElementOf { node10 })
+ )
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle {
+ node1.visitSubtreeIf(Nodes.Any) {
+ visitedChildren.add(it)
+ true
+ }
+ }
+
+ // Assert.
+ assertThat(visitedChildren)
+ .containsExactly(node2, node3, node4, node7, node8, node5, node6, node9, node10)
+ .inOrder()
+ }
+
+ @Test
fun visitAncestorWithinCurrentLayoutNode_immediateParent() {
// Arrange.
val (node1, node2) = List(2) { object : Modifier.Node() {} }
@@ -218,6 +326,83 @@
}
@Test
+ fun nearestAncestorInDifferentLayoutNode_nonContiguousParentLayoutNode() {
+ // Arrange.
+ val (node1, node2) = List(2) { object : Modifier.Node() {} }
+ rule.setContent {
+ Box(modifier = modifierElementOf { node1 }) {
+ Box {
+ Box(modifier = modifierElementOf { node2 })
+ }
+ }
+ }
+
+ // Act.
+ val parent = rule.runOnIdle {
+ node2.nearestAncestor(Nodes.Any)
+ }
+
+ // Assert.
+ assertThat(parent).isEqualTo(node1)
+ }
+
+ @Test
+ fun visitAncestors_sameLayoutNode_calledDuringOnDetach() {
+ // Arrange.
+ val (node1, node2) = List(5) { object : Modifier.Node() {} }
+ val visitedAncestors = mutableListOf<Modifier.Node>()
+ val detachableNode = DetachableNode { node ->
+ node.visitAncestors(Nodes.Any) { visitedAncestors.add(it) }
+ }
+ val removeNode = mutableStateOf(false)
+ rule.setContent {
+ Box(
+ modifier = modifierElementOf { node1 }
+ .then(modifierElementOf { node2 })
+ .then(if (removeNode.value) Modifier else modifierElementOf { detachableNode })
+ )
+ }
+
+ // Act.
+ rule.runOnIdle { removeNode.value = true }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(visitedAncestors)
+ .containsAtLeastElementsIn(arrayOf(node2, node1))
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun visitAncestors_multipleLayoutNodes_calledDuringOnDetach() {
+ // Arrange.
+ val (node1, node2) = List(5) { object : Modifier.Node() {} }
+ val visitedAncestors = mutableListOf<Modifier.Node>()
+ val detachableNode = DetachableNode { node ->
+ node.visitAncestors(Nodes.Any) { visitedAncestors.add(it) }
+ }
+ val removeNode = mutableStateOf(false)
+ rule.setContent {
+ Box(modifierElementOf { node1 }) {
+ Box(modifierElementOf { node2 }) {
+ Box(if (removeNode.value) Modifier else modifierElementOf { detachableNode })
+ }
+ }
+ }
+
+ // Act.
+ rule.runOnIdle { removeNode.value = true }
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(visitedAncestors)
+ .containsAtLeastElementsIn(arrayOf(node2, node1))
+ .inOrder()
+ }
+ }
+
+ @Test
fun nearestAncestorWithinCurrentLayoutNode_immediateParent() {
// Arrange.
val (node1, node2) = List(2) { object : Modifier.Node() {} }
@@ -278,54 +463,123 @@
}
@Test
- fun visitAncestors_sameLayoutNode_calledDuringOnDetach() {
+ fun findAncestors() {
// Arrange.
- val (node1, node2) = List(5) { object : Modifier.Node() {} }
- val visitedAncestors = mutableListOf<Modifier.Node>()
- val detachableNode = DetachableNode {
- it.visitAncestors(Nodes.Any) { node ->
- visitedAncestors.add(node)
- }
- }
- val removeNode = mutableStateOf(false)
+ val (node1, node2, node3, node4) = List(4) { FocusTargetModifierNode() }
+ val (node5, node6, node7, node8) = List(4) { FocusTargetModifierNode() }
rule.setContent {
Box(
modifier = modifierElementOf { node1 }
.then(modifierElementOf { node2 })
- .then(if (removeNode.value) Modifier else modifierElementOf { detachableNode })
- )
- }
-
- // Act.
- rule.runOnIdle { removeNode.value = true }
-
- // Assert.
- rule.runOnIdle {
- assertThat(visitedAncestors)
- .containsAtLeastElementsIn(arrayOf(node2, node1))
- .inOrder()
- }
- }
-
- @Test
- fun nearestAncestorInDifferentLayoutNode_nonContiguousParentLayoutNode() {
- // Arrange.
- val (node1, node2) = List(2) { object : Modifier.Node() {} }
- rule.setContent {
- Box(modifier = modifierElementOf { node1 }) {
+ ) {
Box {
- Box(modifier = modifierElementOf { node2 })
+ Box(modifier = modifierElementOf { node3 })
+ Box(
+ modifier = modifierElementOf { node4 }
+ .then(modifierElementOf { node5 })
+ ) {
+ Box(
+ modifier = modifierElementOf { node6 }
+ .then(modifierElementOf { node7 })
+ )
+ }
+ Box(modifier = modifierElementOf { node8 })
}
}
}
// Act.
- val parent = rule.runOnIdle {
- node2.nearestAncestor(Nodes.Any)
+ val ancestors = rule.runOnIdle {
+ node6.ancestors(Nodes.FocusTarget)
}
// Assert.
- assertThat(parent).isEqualTo(node1)
+ // This test returns all ancestors, even the root focus node. so we drop that one.
+ assertThat(ancestors?.dropLast(1)).containsExactly(node5, node4, node2, node1).inOrder()
+ }
+
+ @Test
+ fun firstChild_currentLayoutNode() {
+ // Arrange.
+ val (node1, node2, node3) = List(3) { object : Modifier.Node() {} }
+ rule.setContent {
+ Box(
+ modifier = modifierElementOf { node1 }
+ .then(modifierElementOf { node2 })
+ .then(modifierElementOf { node3 })
+ )
+ }
+
+ // Act.
+ val child = rule.runOnIdle {
+ node1.firstChild(Nodes.Any)
+ }
+
+ // Assert.
+ assertThat(child).isEqualTo(node2)
+ }
+
+ @Test
+ fun firstChild_currentLayoutNode_nonContiguousChild() {
+ // Arrange.
+ val (node1, node2) = List(3) { object : Modifier.Node() {} }
+ rule.setContent {
+ Box(
+ modifier = modifierElementOf { node1 }
+ .otherModifier()
+ .then(modifierElementOf { node2 })
+ )
+ }
+
+ // Act.
+ val child = rule.runOnIdle {
+ node1.firstChild(Nodes.Any)
+ }
+
+ // Assert.
+ assertThat(child).isEqualTo(node2)
+ }
+
+ @Test
+ fun firstChild_differentLayoutNode() {
+ // Arrange.
+ val (node1, node2, node3) = List(3) { object : Modifier.Node() {} }
+ rule.setContent {
+ Box(modifier = modifierElementOf { node1 }) {
+ Box(modifier = modifierElementOf { node2 }
+ .then(modifierElementOf { node3 }))
+ }
+ }
+
+ // Act.
+ val child = rule.runOnIdle {
+ node1.firstChild(Nodes.Any)
+ }
+
+ // Assert.
+ assertThat(child).isEqualTo(node2)
+ }
+
+ fun firstChild_differentLayoutNode_nonContiguousChild() {
+ // Arrange.
+ val (node1, node2) = List(3) { object : Modifier.Node() {} }
+ rule.setContent {
+ Box(modifier = modifierElementOf { node1 }) {
+ Box {
+ Box(modifier = Modifier.otherModifier()) {
+ Box(modifier = modifierElementOf { node2 })
+ }
+ }
+ }
+ }
+
+ // Act.
+ val child = rule.runOnIdle {
+ node1.firstChild(Nodes.Any)
+ }
+
+ // Assert.
+ assertThat(child).isEqualTo(node2)
}
@Test
@@ -350,20 +604,16 @@
}
private fun Modifier.otherModifier(): Modifier = this.then(Modifier)
-}
-@ExperimentalComposeUiApi
-internal inline fun <reified T : Modifier.Node> modifierElementOf(
- crossinline create: () -> T,
-): Modifier = object : ModifierNodeElement<T>(null, true, {}) {
- override fun create(): T = create()
- override fun update(node: T): T = node
-}
+ @Suppress("ModifierInspectorInfo") // b/251831790.
+ @ExperimentalComposeUiApi
+ private inline fun <reified T : Modifier.Node> modifierElementOf(crossinline create: () -> T) =
+ modifierElementOf(create) { name = "testNode" }
-@OptIn(ExperimentalComposeUiApi::class)
-private class DetachableNode(val onDetach: (DetachableNode) -> Unit) : Modifier.Node() {
- override fun onDetach() {
- onDetach.invoke(this)
- super.onDetach()
+ private class DetachableNode(val onDetach: (DetachableNode) -> Unit) : Modifier.Node() {
+ override fun onDetach() {
+ onDetach.invoke(this)
+ super.onDetach()
+ }
}
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt
index b7279e2..7bc20da 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt
@@ -26,6 +26,7 @@
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
@@ -54,6 +55,9 @@
val obj = object : Modifier.Node() {}
invalidate = { obj.invalidateSubtree() }
obj
+ },
+ definitions = debugInspectorInfo {
+ name = "Invalidate Subtree Modifier.Node"
}
)
rule.setContent {
@@ -103,6 +107,9 @@
val obj = object : Modifier.Node() {}
invalidate = { obj.invalidateSubtree() }
obj
+ },
+ definitions = debugInspectorInfo {
+ name = "Invalidate Subtree Modifier.Node"
}
)
rule.setContent {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeCoordinatorInitializationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeCoordinatorInitializationTest.kt
index ea142f4..d4d1129 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeCoordinatorInitializationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeCoordinatorInitializationTest.kt
@@ -21,10 +21,9 @@
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusModifier
-import androidx.compose.ui.focus.FocusStateImpl
+import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.focusTarget
-import androidx.compose.ui.input.key.KeyInputModifier
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.pointer.PointerInputModifier
import androidx.compose.ui.input.pointer.PointerInteropFilter
import androidx.compose.ui.test.junit4.createComposeRule
@@ -44,32 +43,20 @@
@Test
fun initializeIsCalledWhenFocusNodeIsCreated() {
// Arrange.
- val focusModifier = FocusModifier(FocusStateImpl.Inactive)
+ var focusState: FocusState? = null
// Act.
rule.setContent {
- Box(Modifier.focusTarget(focusModifier))
+ Box(
+ Modifier
+ .onFocusChanged { focusState = it }
+ .focusTarget()
+ )
}
// Assert.
rule.runOnIdle {
- assertThat(focusModifier).isNotNull()
- }
- }
-
- @Test
- fun initializeIsCalledWhenKeyInputNodeIsCreated() {
- // Arrange.
- val keyInputModifier = KeyInputModifier(null, null)
-
- // Act.
- rule.setContent {
- Box(modifier = keyInputModifier)
- }
-
- // Assert.
- rule.runOnIdle {
- assertThat(keyInputModifier.layoutNode).isNotNull()
+ assertThat(focusState).isNotNull()
}
}
@@ -90,44 +77,6 @@
}
}
- @Test
- fun initializeIsCalledWhenFocusNodeIsReused() {
- // Arrange.
- lateinit var focusModifier: FocusModifier
- lateinit var scope: RecomposeScope
- rule.setContent {
- scope = currentRecomposeScope
- focusModifier = FocusModifier(FocusStateImpl.Inactive)
- Box(Modifier.focusTarget(focusModifier))
- }
-
- // Act.
- rule.runOnIdle { scope.invalidate() }
-
- // Assert.
- rule.runOnIdle { assertThat(focusModifier).isNotNull() }
- }
-
- @Test
- fun initializeIsCalledWhenKeyInputNodeIsReused() {
- // Arrange.
- lateinit var keyInputModifier: KeyInputModifier
- lateinit var scope: RecomposeScope
- rule.setContent {
- scope = currentRecomposeScope
- keyInputModifier = KeyInputModifier(null, null)
- Box(modifier = keyInputModifier)
- }
-
- // Act.
- rule.runOnIdle { scope.invalidate() }
-
- // Assert.
- rule.runOnIdle {
- assertThat(keyInputModifier.layoutNode).isNotNull()
- }
- }
-
@ExperimentalComposeUiApi
@Test
fun initializeIsCalledWhenPointerInputNodeIsReused() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt
index 1bfa2ff..d6ceb13 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt
@@ -44,7 +44,7 @@
var callbackInvoked = false
val observerNode = TestObserverNode { callbackInvoked = true }
rule.setContent {
- Box(Modifier.modifierElementOf { observerNode })
+ Box(Modifier.modifierElementOf(observerNode))
}
// Act.
@@ -66,7 +66,7 @@
var callbackInvoked = false
val observerNode = TestObserverNode { callbackInvoked = true }
rule.setContent {
- Box(Modifier.modifierElementOf { observerNode })
+ Box(Modifier.modifierElementOf(observerNode))
}
// Act.
@@ -114,7 +114,7 @@
val observerNode = TestObserverNode { callbackInvoked = true }
var attached by mutableStateOf(true)
rule.setContent {
- Box(if (attached) modifierElementOf { observerNode } else Modifier)
+ Box(if (attached) Modifier.modifierElementOf(observerNode) else Modifier)
}
// Act.
@@ -139,7 +139,7 @@
val observerNode = TestObserverNode { callbackInvoked = true }
var attached by mutableStateOf(true)
rule.setContent {
- Box(if (attached) modifierElementOf { observerNode } else Modifier)
+ Box(if (attached) Modifier.modifierElementOf(observerNode) else Modifier)
}
// Act.
@@ -161,10 +161,8 @@
}
@ExperimentalComposeUiApi
- private inline fun <reified T : Modifier.Node> Modifier.modifierElementOf(
- crossinline create: () -> T,
- ): Modifier {
- return this.then(modifierElementOf(create) { name = "testNode" })
+ private inline fun <reified T : Modifier.Node> Modifier.modifierElementOf(node: T): Modifier {
+ return this.then(modifierElementOf(create = { node }, definitions = { name = "testNode" }))
}
class TestObserverNode(
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index d329cf2..f50648d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -73,9 +73,8 @@
import androidx.compose.ui.focus.FocusDirection.Companion.Previous
import androidx.compose.ui.focus.FocusDirection.Companion.Right
import androidx.compose.ui.focus.FocusDirection.Companion.Up
-import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.focus.FocusManagerImpl
-import androidx.compose.ui.focus.focusRect
+import androidx.compose.ui.focus.FocusOwner
+import androidx.compose.ui.focus.FocusOwnerImpl
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
@@ -101,9 +100,9 @@
import androidx.compose.ui.input.key.Key.Companion.Tab
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
-import androidx.compose.ui.input.key.KeyInputModifier
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.AndroidPointerIcon
import androidx.compose.ui.input.pointer.AndroidPointerIconType
@@ -196,9 +195,7 @@
properties = {}
)
- private val _focusManager: FocusManagerImpl = FocusManagerImpl()
- override val focusManager: FocusManager
- get() = _focusManager
+ override val focusOwner: FocusOwner = FocusOwnerImpl { registerOnEndApplyChangesListener(it) }
private val _windowInfo: WindowInfoImpl = WindowInfoImpl()
override val windowInfo: WindowInfo
@@ -206,16 +203,13 @@
// TODO(b/177931787) : Consider creating a KeyInputManager like we have for FocusManager so
// that this common logic can be used by all owners.
- private val keyInputModifier: KeyInputModifier = KeyInputModifier(
- >
- val focusDirection = getFocusDirection(it)
- if (focusDirection == null || it.type != KeyDown) return@KeyInputModifier false
+ private val keyInputModifier = Modifier.onKeyEvent {
+ val focusDirection = getFocusDirection(it)
+ if (focusDirection == null || it.type != KeyDown) return@onKeyEvent false
- // Consume the key event if we moved focus.
- focusManager.moveFocus(focusDirection)
- },
- >
- )
+ // Consume the key event if we moved focus.
+ focusOwner.moveFocus(focusDirection)
+ }
private val rotaryInputModifier = Modifier.onRotaryScrollEvent {
// TODO(b/210748692): call focusManager.moveFocus() in response to rotary events.
@@ -259,7 +253,7 @@
it.modifier = Modifier
.then(semanticsModifier)
.then(rotaryInputModifier)
- .then(_focusManager.modifier)
+ .then(focusOwner.modifier)
.then(keyInputModifier)
.then(scrollContainerInfo)
}
@@ -401,7 +395,6 @@
// executed whenever the touch mode changes.
private val touchModeChangeListener = ViewTreeObserver.OnTouchModeChangeListener { touchMode ->
_inputModeManager.inputMode = if (touchMode) Touch else Keyboard
- _focusManager.fetchUpdatedFocusProperties()
}
private val textInputServiceAndroid = TextInputServiceAndroid(this)
@@ -605,11 +598,11 @@
* system for accurate focus searching and so ViewRootImpl will scroll correctly.
*/
override fun getFocusedRect(rect: Rect) {
- _focusManager.getActiveFocusModifier()?.focusRect()?.let {
- rect.left = it.left.roundToInt()
- rect.top = it.top.roundToInt()
- rect.right = it.right.roundToInt()
- rect.bottom = it.bottom.roundToInt()
+ focusOwner.getFocusRect()?.run {
+ rect.left = left.roundToInt()
+ rect.top = top.roundToInt()
+ rect.right = right.roundToInt()
+ rect.bottom = bottom.roundToInt()
} ?: super.getFocusedRect(rect)
}
@@ -621,9 +614,7 @@
override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
Log.d(FocusTag, "Owner FocusChanged($gainFocus)")
- with(_focusManager) {
- if (gainFocus) takeFocus() else releaseFocus()
- }
+ if (gainFocus) focusOwner.takeFocus() else focusOwner.releaseFocus()
}
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
@@ -646,7 +637,7 @@
}
override fun sendKeyEvent(keyEvent: KeyEvent): Boolean {
- return keyInputModifier.processKeyInput(keyEvent)
+ return focusOwner.dispatchKeyEvent(keyEvent)
}
override fun dispatchKeyEvent(event: AndroidKeyEvent) =
@@ -1268,7 +1259,7 @@
horizontalScrollPixels = axisValue * getScaledHorizontalScrollFactor(config, context),
uptimeMillis = event.eventTime
)
- return _focusManager.getActiveFocusModifier()?.propagateRotaryEvent(rotaryEvent) ?: false
+ return focusOwner.dispatchRotaryEvent(rotaryEvent)
}
private fun handleMotionEvent(motionEvent: MotionEvent): ProcessResult {
@@ -1569,7 +1560,7 @@
if (superclassInitComplete) {
layoutDirectionFromInt(layoutDirection).let {
this.layoutDirection = it
- _focusManager.layoutDirection = it
+ focusOwner.layoutDirection = it
}
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 111d00e..7ce8bbb 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -1423,7 +1423,7 @@
}
AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS -> {
return if (node.unmergedConfig.getOrNull(SemanticsProperties.Focused) == true) {
- view.focusManager.clearFocus()
+ view.focusOwner.clearFocus()
true
} else {
false
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposedModifier.kt
index 51901fc..a06b7ea 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposedModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposedModifier.kt
@@ -18,13 +18,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composer
-import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.focus.FocusEventModifier
-import androidx.compose.ui.focus.FocusEventModifierLocal
-import androidx.compose.ui.focus.FocusRequesterModifier
-import androidx.compose.ui.focus.FocusRequesterModifierLocal
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.NoInspectorInfo
@@ -251,14 +245,7 @@
*/
@Suppress("ModifierFactoryExtensionFunction")
fun Composer.materialize(modifier: Modifier): Modifier {
- if (modifier.all {
- // onFocusEvent is implemented now with ModifierLocals and SideEffects, but
- // FocusEventModifier needs to have composition to do the same. The following
- // check for FocusEventModifier is only needed until the modifier is removed.
- // The same is true for FocusRequesterModifier and focusTarget()
- it !is ComposedModifier && it !is FocusEventModifier && it !is FocusRequesterModifier
- }
- ) {
+ if (modifier.all { it !is ComposedModifier }) {
return modifier
}
@@ -273,31 +260,12 @@
val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->
acc.then(
if (element is ComposedModifier) {
- @kotlin.Suppress("UNCHECKED_CAST")
+ @Suppress("UNCHECKED_CAST")
val factory = element.factory as Modifier.(Composer, Int) -> Modifier
val composedMod = factory(Modifier, this, 0)
materialize(composedMod)
} else {
- // onFocusEvent is implemented now with ModifierLocals and SideEffects, but
- // FocusEventModifier needs to have composition to do the same. The following
- // check for FocusEventModifier is only needed until the modifier is removed.
- var newElement: Modifier = element
- if (element is FocusEventModifier) {
- @Suppress("UNCHECKED_CAST")
- val factory = WrapFocusEventModifier
- as (FocusEventModifier, Composer, Int) -> Modifier
-
- newElement = newElement.then(factory(element, this, 0))
- }
- // The same is true for FocusRequesterModifier and focusTarget()
- if (element is FocusRequesterModifier) {
- @Suppress("UNCHECKED_CAST")
- val factory = WrapFocusRequesterModifier
- as (FocusRequesterModifier, Composer, Int) -> Modifier
-
- newElement = newElement.then(factory(element, this, 0))
- }
- newElement
+ element
}
)
}
@@ -305,19 +273,3 @@
endReplaceableGroup()
return result
}
-
-private val WrapFocusEventModifier: @Composable (FocusEventModifier) -> Modifier = { mod ->
- val modifier = remember(mod) {
- FocusEventModifierLocal(mod::onFocusEvent)
- }
- SideEffect {
- modifier.notifyIfNoFocusModifiers()
- }
- modifier
-}
-
-private val WrapFocusRequesterModifier: @Composable (FocusRequesterModifier) -> Modifier = { mod ->
- remember(mod) {
- FocusRequesterModifierLocal(mod.focusRequester)
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/BeyondBoundsLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/BeyondBoundsLayout.kt
index 5e5191e..4f4e15f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/BeyondBoundsLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/BeyondBoundsLayout.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.focus
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.layout.BeyondBoundsLayout.BeyondBoundsScope
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Above
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.After
@@ -24,7 +25,8 @@
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
-internal fun <T> FocusModifier.searchBeyondBounds(
+@ExperimentalComposeUiApi
+internal fun <T> FocusTargetModifierNode.searchBeyondBounds(
direction: FocusDirection,
block: BeyondBoundsScope.() -> T?
): T? {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
index 9440c6b..54ee8d3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
@@ -16,12 +16,9 @@
package androidx.compose.ui.focus
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.node.modifierElementOf
/**
* Add this modifier to a component to observe focus state events. [onFocusChanged] is invoked
@@ -33,18 +30,29 @@
* Note: If you want to be notified every time the internal focus state is written to (even if it
* hasn't changed), use [onFocusEvent] instead.
*/
-fun Modifier.onFocusChanged(onFocusChanged: (FocusState) -> Unit): Modifier =
- composed(
- inspectorInfo = debugInspectorInfo {
+@Suppress("ModifierInspectorInfo")
+fun Modifier.onFocusChanged(onFocusChanged: (FocusState) -> Unit): Modifier = this.then(
+ @OptIn(ExperimentalComposeUiApi::class)
+ modifierElementOf(
+ key = onFocusChanged,
+ create = { FocusChangedModifierNode(onFocusChanged) },
+ update = { it. },
+ definitions = {
name = "onFocusChanged"
properties["onFocusChanged"] = onFocusChanged
}
- ) {
- val focusState: MutableState<FocusState?> = remember { mutableStateOf(null) }
- Modifier.onFocusEvent {
- if (focusState.value != it) {
- focusState.value = it
- onFocusChanged(it)
- }
+ )
+)
+
+@ExperimentalComposeUiApi
+private class FocusChangedModifierNode(
+ var onFocusChanged: (FocusState) -> Unit
+) : FocusEventModifierNode, Modifier.Node() {
+ var focusState: FocusState? = null
+ override fun onFocusEvent(focusState: FocusState) {
+ if (this.focusState != focusState) {
+ this.focusState = focusState
+ this.onFocusChanged.invoke(focusState)
}
}
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt
index ddfc229..4b175e1 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt
@@ -16,29 +16,37 @@
package androidx.compose.ui.focus
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.collection.MutableVector
-import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.focus.FocusStateImpl.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
-import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
import androidx.compose.ui.focus.FocusStateImpl.Inactive
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ModifierLocalReadScope
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.modifier.modifierLocalOf
-import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.modifierElementOf
+import androidx.compose.ui.node.visitAncestors
+import androidx.compose.ui.node.visitChildren
+
+/**
+ * Implement this interface create a modifier node that can be used to observe focus state changes
+ * to a [FocusTargetModifierNode] down the hierarchy.
+ */
+@ExperimentalComposeUiApi
+interface FocusEventModifierNode : DelegatableNode {
+
+ /**
+ * A parent FocusEventNode is notified of [FocusState] changes to the [FocusTargetModifierNode]
+ * associated with this [FocusEventModifierNode].
+ */
+ fun onFocusEvent(focusState: FocusState)
+}
/**
* A [modifier][Modifier.Element] that can be used to observe focus state events.
*/
+@Deprecated("Use FocusEventModifierNode instead")
@JvmDefaultWithCompatibility
interface FocusEventModifier : Modifier.Element {
/**
@@ -47,120 +55,62 @@
fun onFocusEvent(focusState: FocusState)
}
-internal val ModifierLocalFocusEvent = modifierLocalOf<FocusEventModifierLocal?> { null }
+@OptIn(ExperimentalComposeUiApi::class)
+internal class FocusEventModifierNodeImpl(
+ var onFocusEvent: (FocusState) -> Unit
+) : FocusEventModifierNode, Modifier.Node() {
-internal class FocusEventModifierLocal(
- val onFocusEvent: (FocusState) -> Unit,
-) : ModifierLocalProvider<FocusEventModifierLocal?>, ModifierLocalConsumer {
- // parent/children form three of FocusEventModifierLocals
- private var parent: FocusEventModifierLocal? = null
- private val children = mutableVectorOf<FocusEventModifierLocal>()
-
- /**
- * This is the list of modifiers that contribute to the focus event's state.
- * When there are multiple, all FocusModifier states must be considered when notifying an event.
- */
- private val focusModifiers = mutableVectorOf<FocusModifier>()
-
- override val key: ProvidableModifierLocal<FocusEventModifierLocal?>
- get() = ModifierLocalFocusEvent
- override val value: FocusEventModifierLocal
- get() = this
-
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {
- val newParent = ModifierLocalFocusEvent.current
- if (newParent != parent) {
- parent?.let { parent ->
- parent.children -= this@FocusEventModifierLocal
- parent.removeFocusModifiers(focusModifiers)
- }
- parent = newParent
- if (newParent != null) {
- newParent.children += this@FocusEventModifierLocal
- newParent.addFocusModifiers(focusModifiers)
- }
- }
- parent = ModifierLocalFocusEvent.current
+ override fun onFocusEvent(focusState: FocusState) {
+ this.onFocusEvent.invoke(focusState)
}
+}
- fun addFocusModifier(focusModifier: FocusModifier) {
- focusModifiers += focusModifier
- parent?.addFocusModifier(focusModifier)
- }
-
- private fun addFocusModifiers(modifiers: MutableVector<FocusModifier>) {
- focusModifiers.addAll(modifiers)
- parent?.addFocusModifiers(modifiers)
- }
-
- fun removeFocusModifier(focusModifier: FocusModifier) {
- focusModifiers -= focusModifier
- parent?.removeFocusModifier(focusModifier)
- }
-
- private fun removeFocusModifiers(modifiers: MutableVector<FocusModifier>) {
- focusModifiers.removeAll(modifiers)
- parent?.removeFocusModifiers(modifiers)
- }
-
- fun propagateFocusEvent() {
- val notifiedState = when (focusModifiers.size) {
- 0 -> Inactive
- 1 -> focusModifiers[0].focusState
- else -> {
- // We have multiple children, so we have to recalculate the focus state
- var focusedChild: FocusModifier? = null
- var allChildrenDisabled: Boolean? = null
- focusModifiers.forEach {
- when (it.focusState) {
- Active,
- ActiveParent,
- Captured,
- DeactivatedParent -> {
- focusedChild = it
- allChildrenDisabled = false
- }
- Deactivated -> if (allChildrenDisabled == null) {
- allChildrenDisabled = true
- }
- Inactive -> allChildrenDisabled = false
- }
- }
-
- focusedChild?.focusState ?: if (allChildrenDisabled == true) {
- Deactivated
- } else {
- Inactive
- }
- }
- }
- onFocusEvent(notifiedState)
- parent?.propagateFocusEvent()
- }
-
- fun notifyIfNoFocusModifiers() {
- if (focusModifiers.isEmpty()) {
- onFocusEvent(FocusStateImpl.Inactive)
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun FocusEventModifierNode.getFocusState(): FocusState {
+ visitChildren(Nodes.FocusTarget) {
+ when (val focusState = it.focusStateImpl) {
+ // If we find a focused child, we use that child's state as the aggregated state.
+ Active, ActiveParent, Captured -> return focusState
+ // We use the Inactive state only if we don't have a focused child.
+ // ie. we ignore this child if another child provides aggregated state.
+ Inactive -> return@visitChildren
}
}
+ return Inactive
}
/**
* Add this modifier to a component to observe focus state events.
*/
-fun Modifier.onFocusEvent(onFocusEvent: (FocusState) -> Unit): Modifier = composed(
- debugInspectorInfo {
- name = "onFocusEvent"
- properties["onFocusEvent"] = onFocusEvent
- }
-) {
- val modifier = remember(onFocusEvent) {
- FocusEventModifierLocal(>
- }
+@Suppress("ModifierInspectorInfo") // b/251831790.
+fun Modifier.onFocusEvent(onFocusEvent: (FocusState) -> Unit): Modifier = this.then(
+ @OptIn(ExperimentalComposeUiApi::class)
+ modifierElementOf(
+ key = onFocusEvent,
+ create = { FocusEventModifierNodeImpl(onFocusEvent) },
+ update = { it. },
+ definitions = {
+ name = "onFocusEvent"
+ properties["onFocusEvent"] = onFocusEvent
+ }
+ )
+)
- SideEffect {
- modifier.notifyIfNoFocusModifiers()
- }
+/**
+ * Sends a "Focus Event" up the hierarchy that asks all [FocusEventModifierNode]s to recompute their
+ * observed focus state.
+ *
+ * Make this public after [FocusTargetModifierNode] is made public.
+ */
+@ExperimentalComposeUiApi
+internal fun FocusTargetModifierNode.refreshFocusEventNodes() {
+ visitAncestors(Nodes.FocusEvent or Nodes.FocusTarget) {
+ // If we reach the previous focus target node, we have gone too far, as
+ // this is applies to the another focus event.
+ if (it.isKind(Nodes.FocusTarget)) return
- modifier
+ // TODO(251833873): Consider caching it.getFocusState().
+ check(it is FocusEventModifierNode)
+ it.onFocusEvent(it.getFocusState())
+ }
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
new file mode 100644
index 0000000..8421e1b
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusInvalidationManager.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2022 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.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.focus.FocusStateImpl.Inactive
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.visitChildren
+
+/**
+ * The [FocusInvalidationManager] allows us to schedule focus related nodes for invalidation.
+ * These nodes are invalidated after onApplyChanges. It does this by registering an
+ * onApplyChangesListener when nodes are scheduled for invalidation.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+internal class FocusInvalidationManager(
+ private val onRequestApplyChangesListener: (() -> Unit) -> Unit
+) {
+ private var focusTargetNodes = mutableSetOf<FocusTargetModifierNode>()
+ private var focusEventNodes = mutableSetOf<FocusEventModifierNode>()
+ private var focusPropertiesNodes = mutableSetOf<FocusPropertiesModifierNode>()
+
+ fun scheduleInvalidation(node: FocusTargetModifierNode) {
+ focusTargetNodes.scheduleInvalidation(node)
+ }
+
+ fun scheduleInvalidation(node: FocusEventModifierNode) {
+ focusEventNodes.scheduleInvalidation(node)
+ }
+
+ fun scheduleInvalidation(node: FocusPropertiesModifierNode) {
+ focusPropertiesNodes.scheduleInvalidation(node)
+ }
+
+ private fun <T> MutableSet<T>.scheduleInvalidation(node: T) {
+ // We don't schedule a node if it is already scheduled during this composition.
+ if (contains(node)) return
+
+ add(node)
+
+ // If this is the first node scheduled for invalidation,
+ // we set up a listener that runs after onApplyChanges.
+ if (focusTargetNodes.size + focusEventNodes.size + focusPropertiesNodes.size == 1) {
+ onRequestApplyChangesListener.invoke(invalidateNodes)
+ }
+ }
+
+ private val invalidateNodes: () -> Unit = {
+ // Process all the invalidated FocusProperties nodes.
+ focusPropertiesNodes.forEach {
+ it.visitChildren(Nodes.FocusTarget) { focusTarget ->
+ focusTargetNodes.add(focusTarget)
+ }
+ }
+ focusPropertiesNodes.clear()
+
+ // Process all the focus events nodes.
+ val focusTargetsWithInvalidatedFocusEvents = mutableSetOf<FocusTargetModifierNode>()
+ focusEventNodes.forEach { focusEventNode ->
+ // When focus nodes are removed, the corresponding focus events are scheduled for
+ // invalidation. If the focus event was also removed, we don't need to invalidate it.
+ if (!focusEventNode.node.isAttached) return@forEach
+
+ var requiresUpdate = true
+ var aggregatedNode = false
+ var focusTarget: FocusTargetModifierNode? = null
+ focusEventNode.visitChildren(Nodes.FocusTarget) {
+
+ // If there are multiple focus targets associated with this focus event node,
+ // we need to calculate the aggregated state.
+ if (focusTarget != null) {
+ aggregatedNode = true
+ }
+
+ focusTarget = it
+
+ // If the associated focus node is already scheduled for invalidation, it will
+ // send an onFocusEvent if the invalidation causes a focus state change.
+ // However this onFocusEvent was invalidated, so we have to ensure that we call
+ // onFocusEvent even if the focus state didn't change.
+ if (focusTargetNodes.contains(it)) {
+ requiresUpdate = false
+ focusTargetsWithInvalidatedFocusEvents.add(it)
+ return@visitChildren
+ }
+ }
+
+ if (requiresUpdate) {
+ focusEventNode.onFocusEvent(
+ if (aggregatedNode) {
+ focusEventNode.getFocusState()
+ } else {
+ focusTarget?.focusState ?: Inactive
+ }
+ )
+ }
+ }
+ focusEventNodes.clear()
+
+ // Process all the focus target nodes.
+ focusTargetNodes.forEach {
+ // We don't need to invalidate the focus target if it was scheduled for invalidation
+ // earlier in the composition but was then removed.
+ if (!it.isAttached) return@forEach
+
+ val preInvalidationState = it.focusState
+ it.invalidateFocus()
+ if (preInvalidationState != it.focusState ||
+ focusTargetsWithInvalidatedFocusEvents.contains(it)) {
+ it.refreshFocusEventNodes()
+ }
+ }
+ focusTargetNodes.clear()
+ focusTargetsWithInvalidatedFocusEvents.clear()
+
+ check(focusPropertiesNodes.isEmpty())
+ check(focusEventNodes.isEmpty())
+ check(focusTargetNodes.isEmpty())
+ }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
index 9bc7c0b..f7574c4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
@@ -25,11 +25,22 @@
import androidx.compose.ui.focus.FocusStateImpl.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
-import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
import androidx.compose.ui.focus.FocusStateImpl.Inactive
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyInputModifierNode
+import androidx.compose.ui.input.rotary.RotaryScrollEvent
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.NodeKind
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.ancestors
+import androidx.compose.ui.node.modifierElementOf
+import androidx.compose.ui.node.nearestAncestor
+import androidx.compose.ui.node.visitLocalChildren
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachReversed
@JvmDefaultWithCompatibility
interface FocusManager {
@@ -60,45 +71,54 @@
/**
* The focus manager is used by different [Owner][androidx.compose.ui.node.Owner] implementations
* to control focus.
- *
- * @param focusModifier The modifier that will be used as the root focus modifier.
*/
-internal class FocusManagerImpl(
- private val focusModifier: FocusModifier = FocusModifier(Inactive)
-) : FocusManager {
+internal class FocusOwnerImpl(onRequestApplyChangesListener: (() -> Unit) -> Unit) : FocusOwner {
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ internal var rootFocusNode = FocusTargetModifierNode()
+
+ private val focusInvalidationManager = FocusInvalidationManager(onRequestApplyChangesListener)
/**
* A [Modifier] that can be added to the [Owners][androidx.compose.ui.node.Owner] modifier
* list that contains the modifiers required by the focus system. (Eg, a root focus modifier).
*/
// TODO(b/168831247): return an empty Modifier when there are no focusable children.
- val modifier: Modifier = Modifier.focusTarget(focusModifier)
+ @Suppress("ModifierInspectorInfo") // b/251831790.
+ @OptIn(ExperimentalComposeUiApi::class)
+ override val modifier: Modifier = modifierElementOf(
+ create = { rootFocusNode },
+ definitions = { name = "RootFocusTarget" }
+ )
- lateinit var layoutDirection: LayoutDirection
+ override lateinit var layoutDirection: LayoutDirection
/**
* The [Owner][androidx.compose.ui.node.Owner] calls this function when it gains focus. This
- * informs the [focus manager][FocusManagerImpl] that the
+ * informs the [focus manager][FocusOwnerImpl] that the
* [Owner][androidx.compose.ui.node.Owner] gained focus, and that it should propagate this
* focus to one of the focus modifiers in the component hierarchy.
*/
- fun takeFocus() {
+ override fun takeFocus() {
// If the focus state is not Inactive, it indicates that the focus state is already
// set (possibly by dispatchWindowFocusChanged). So we don't update the state.
- if (focusModifier.focusState == Inactive) {
- focusModifier.focusState = Active
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (rootFocusNode.focusStateImpl == Inactive) {
+ rootFocusNode.focusStateImpl = Active
// TODO(b/152535715): propagate focus to children based on child focusability.
+ // moveFocus(FocusDirection.Enter)
}
}
/**
* The [Owner][androidx.compose.ui.node.Owner] calls this function when it loses focus. This
- * informs the [focus manager][FocusManagerImpl] that the
+ * informs the [focus manager][FocusOwnerImpl] that the
* [Owner][androidx.compose.ui.node.Owner] lost focus, and that it should clear focus from
* all the focus modifiers in the component hierarchy.
*/
- fun releaseFocus() {
- focusModifier.clearFocus(forcedClear = true)
+ override fun releaseFocus() {
+ @OptIn(ExperimentalComposeUiApi::class)
+ rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
}
/**
@@ -111,14 +131,18 @@
* component.
*/
override fun clearFocus(force: Boolean) {
+ clearFocus(force, refreshFocusEvents = true)
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ override fun clearFocus(force: Boolean, refreshFocusEvents: Boolean) {
// If this hierarchy had focus before clearing it, it indicates that the host view has
// focus. So after clearing focus within the compose hierarchy, we should restore focus to
// the root focus modifier to maintain consistency with the host view.
- val rootInitialState = focusModifier.focusState
- if (focusModifier.clearFocus(force)) {
- focusModifier.focusState = when (rootInitialState) {
+ val rootInitialState = rootFocusNode.focusStateImpl
+ if (rootFocusNode.clearFocus(force, refreshFocusEvents)) {
+ rootFocusNode.focusStateImpl = when (rootInitialState) {
Active, ActiveParent, Captured -> Active
- Deactivated, DeactivatedParent -> Deactivated
Inactive -> Inactive
}
}
@@ -129,22 +153,23 @@
*
* @return true if focus was moved successfully. false if the focused item is unchanged.
*/
+ @OptIn(ExperimentalComposeUiApi::class)
override fun moveFocus(focusDirection: FocusDirection): Boolean {
// If there is no active node in this sub-hierarchy, we can't move focus.
- val source = focusModifier.findActiveFocusNode() ?: return false
+ val source = rootFocusNode.findActiveFocusNode() ?: return false
// Check if a custom focus traversal order is specified.
- val nextFocusRequester = source.customFocusSearch(focusDirection, layoutDirection)
-
- return when (nextFocusRequester) {
+ return when (val next = source.customFocusSearch(focusDirection, layoutDirection)) {
@OptIn(ExperimentalComposeUiApi::class)
Cancel -> false
Default -> {
val foundNextItem =
- focusModifier.focusSearch(focusDirection, layoutDirection) { destination ->
+ rootFocusNode.focusSearch(focusDirection, layoutDirection) { destination ->
if (destination == source) return@focusSearch false
- checkNotNull(destination.parent) { "Focus search landed at the root." }
+ checkNotNull(destination.nearestAncestor(Nodes.FocusTarget)) {
+ "Focus search landed at the root."
+ }
// If we found a potential next item, move focus to it.
destination.requestFocus()
true
@@ -154,33 +179,98 @@
}
else -> {
// TODO(b/175899786): We ideally need to check if the nextFocusRequester points to
- // something that is visible and focusable in the current mode (Touch/Non-Touch mode).
- nextFocusRequester.requestFocus()
+ // something that is visible and focusable in the current mode (Touch/Non-Touch
+ // mode).
+ next.requestFocus()
true
}
}
}
/**
- * Runs the focus properties block for all [focusProperties] modifiers to fetch updated
- * [FocusProperties].
- *
- * The [focusProperties] block is run automatically whenever the properties change, and you
- * rarely need to invoke this function manually. However, if you have a situation where you want
- * to change a property, and need to see the change in the current snapshot, use this API.
+ * Dispatches a key event through the compose hierarchy.
*/
- fun fetchUpdatedFocusProperties() {
- focusModifier.updateProperties()
+ @OptIn(ExperimentalComposeUiApi::class)
+ override fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean {
+ val activeFocusTarget = rootFocusNode.findActiveFocusNode()
+ checkNotNull(activeFocusTarget) {
+ "Event can't be processed because we do not have an active focus target."
+ }
+ val focusedKeyInputNode = activeFocusTarget.lastLocalKeyInputNode()
+ ?: activeFocusTarget.nearestAncestor(Nodes.KeyInput)
+
+ focusedKeyInputNode?.traverseAncestors(
+ type = Nodes.KeyInput,
+ if (it.onPreKeyEvent(keyEvent)) return true },
+ if (it.onKeyEvent(keyEvent)) return true }
+ )
+
+ return false
}
/**
- * Searches for the currently focused item.
- *
- * @return the currently focused item.
+ * Dispatches a rotary scroll event through the compose hierarchy.
*/
- @Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
- internal fun getActiveFocusModifier(): FocusModifier? {
- return focusModifier.findActiveItem()
+ @OptIn(ExperimentalComposeUiApi::class)
+ override fun dispatchRotaryEvent(event: RotaryScrollEvent): Boolean {
+ val focusedRotaryInputNode = rootFocusNode.findActiveFocusNode()
+ ?.nearestAncestor(Nodes.RotaryInput)
+
+ focusedRotaryInputNode?.traverseAncestors(
+ type = Nodes.RotaryInput,
+ if (it.onPreRotaryScrollEvent(event)) return true },
+ if (it.onRotaryScrollEvent(event)) return true }
+ )
+
+ return false
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ override fun scheduleInvalidation(node: FocusTargetModifierNode) {
+ focusInvalidationManager.scheduleInvalidation(node)
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ override fun scheduleInvalidation(node: FocusEventModifierNode) {
+ focusInvalidationManager.scheduleInvalidation(node)
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ override fun scheduleInvalidation(node: FocusPropertiesModifierNode) {
+ focusInvalidationManager.scheduleInvalidation(node)
+ }
+
+ @ExperimentalComposeUiApi
+ private inline fun <reified T : DelegatableNode> T.traverseAncestors(
+ type: NodeKind<T>,
+ onPreVisit: (T) -> Unit,
+ onVisit: (T) -> Unit
+ ) {
+ val ancestors = ancestors(type)
+ ancestors?.fastForEachReversed(onPreVisit)
+ onPreVisit(this)
+ onVisit(this)
+ ancestors?.fastForEach(onVisit)
+ }
+
+ /**
+ * Searches for the currently focused item, and returns its coordinates as a rect.
+ */
+ override fun getFocusRect(): Rect? {
+ @OptIn(ExperimentalComposeUiApi::class)
+ return rootFocusNode.findActiveFocusNode()?.focusRect()
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ private fun DelegatableNode.lastLocalKeyInputNode(): KeyInputModifierNode? {
+ var focusedKeyInputNode: KeyInputModifierNode? = null
+ visitLocalChildren(Nodes.FocusTarget or Nodes.KeyInput) { modifierNode ->
+ if (modifierNode.isKind(Nodes.FocusTarget)) return focusedKeyInputNode
+
+ check(modifierNode is KeyInputModifierNode)
+ focusedKeyInputNode = modifierNode
+ }
+ return focusedKeyInputNode
}
// TODO(b/144116848): This is a hack to make Next/Previous wrap around. This must be
@@ -188,14 +278,16 @@
// will then pass focus to other views, and ultimately return back to this compose view.
private fun wrapAroundFocus(focusDirection: FocusDirection): Boolean {
// Wrap is not supported when this sub-hierarchy doesn't have focus.
- if (!focusModifier.focusState.hasFocus || focusModifier.focusState.isFocused) return false
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (!rootFocusNode.focusState.hasFocus || rootFocusNode.focusState.isFocused) return false
// Next and Previous wraps around.
when (focusDirection) {
Next, Previous -> {
// Clear Focus to send focus the root node.
clearFocus(force = false)
- if (!focusModifier.focusState.isFocused) return false
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (!rootFocusNode.focusState.isFocused) return false
// Wrap around by calling moveFocus after the root gains focus.
return moveFocus(focusDirection)
@@ -205,25 +297,3 @@
}
}
}
-
-private fun FocusModifier.updateProperties() {
- // Update the focus node with the current focus properties.
- refreshFocusProperties()
-
- // Update the focus properties for all children.
- children.forEach { it.updateProperties() }
-}
-
-/**
- * Find the focus modifier in this sub-hierarchy that is currently focused.
- */
-@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
-private fun FocusModifier.findActiveItem(): FocusModifier? {
- return when (focusState) {
- Active, Captured -> this
- ActiveParent, DeactivatedParent -> {
- focusedChild?.findActiveItem() ?: error("no child")
- }
- Deactivated, Inactive -> null
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt
index 1a71a0b9e..cf58238 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt
@@ -16,166 +16,108 @@
package androidx.compose.ui.focus
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.focus.FocusStateImpl.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
-import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
import androidx.compose.ui.focus.FocusStateImpl.Inactive
-import androidx.compose.ui.input.focus.FocusAwareInputModifier
-import androidx.compose.ui.input.key.KeyInputModifier
-import androidx.compose.ui.input.key.ModifierLocalKeyInput
-import androidx.compose.ui.input.rotary.ModifierLocalRotaryScrollParent
-import androidx.compose.ui.input.rotary.RotaryScrollEvent
import androidx.compose.ui.layout.BeyondBoundsLayout
-import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.layout.OnPlacedModifier
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ModifierLocalReadScope
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.modifier.modifierLocalOf
-import androidx.compose.ui.node.NodeCoordinator
-import androidx.compose.ui.node.OwnerScope
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.NoInspectorInfo
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.modifier.ModifierLocalNode
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.ObserverNode
+import androidx.compose.ui.node.modifierElementOf
+import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requireOwner
+import androidx.compose.ui.node.visitAncestors
/**
- * Used to build a tree of [FocusModifier] elements. This contains the parent.
+ * This modifier node can be used to create a modifier that makes a component focusable.
+ * Use a different instance of [FocusTargetModifierNode] for each focusable component.
*/
-internal val ModifierLocalParentFocusModifier = modifierLocalOf<FocusModifier?> { null }
-
-/**
- * A [Modifier.Element] that wraps makes the modifiers on the right into a Focusable. Use a
- * different instance of [FocusModifier] for each focusable component.
- */
-internal class FocusModifier(
- initialFocus: FocusStateImpl,
- // TODO(b/172265016): Make this a required parameter and remove the default value.
- // Set this value in AndroidComposeView, and other places where we create a focus modifier
- // using this internal constructor.
- inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo
-) : ModifierLocalConsumer,
- ModifierLocalProvider<FocusModifier?>,
- OwnerScope,
- OnPlacedModifier,
- InspectorValueInfo(inspectorInfo) {
- // TODO(b/188684110): Move focusState and focusedChild to ModifiedFocusNode and make this
- // modifier stateless.
- var parent: FocusModifier? = null
- val children = mutableVectorOf<FocusModifier>()
- var focusState: FocusStateImpl = initialFocus
- set(value) {
- field = value
- sendOnFocusEvent()
- }
- var focusedChild: FocusModifier? = null
- var focusEventListener: FocusEventModifierLocal? = null
- @OptIn(ExperimentalComposeUiApi::class)
- private var rotaryScrollParent: FocusAwareInputModifier<RotaryScrollEvent>? = null
- lateinit var modifierLocalReadScope: ModifierLocalReadScope
- var beyondBoundsLayoutParent: BeyondBoundsLayout? = null
- var focusPropertiesModifier: FocusPropertiesModifier? = null
- val focusProperties: FocusProperties = FocusPropertiesImpl()
- var focusRequester: FocusRequesterModifierLocal? = null
- var coordinator: NodeCoordinator? = null
- var focusRequestedOnPlaced = false
-
+@ExperimentalComposeUiApi
+class FocusTargetModifierNode : ObserverNode, ModifierLocalNode, Modifier.Node() {
/**
- * The KeyInputModifier that this FocusModifier comes after.
+ * The [FocusState] associated with this [FocusTargetModifierNode].
*/
- var keyInputModifier: KeyInputModifier? = null
- private set
+ val focusState: FocusState
+ get() = focusStateImpl
- /**
- * All KeyInputModifiers that read this [FocusModifier] in the
- * [ModifierLocalParentFocusModifier].
- */
- val keyInputChildren = mutableVectorOf<KeyInputModifier>()
+ internal var focusStateImpl = Inactive
+ internal val beyondBoundsLayoutParent: BeyondBoundsLayout?
+ get() = ModifierLocalBeyondBoundsLayout.current
- // Reading the FocusProperties ModifierLocal.
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
- modifierLocalReadScope = scope
+ override fun onObservedReadsChanged() {
+ val previousFocusState = focusState
+ invalidateFocus()
+ if (previousFocusState != focusState) refreshFocusEventNodes()
+ }
- with(scope) {
- parent = ModifierLocalParentFocusModifier.current.also { newParent ->
- if (newParent != parent) {
- if (newParent == null) {
- when (focusState) {
- Active, Captured -> coordinator?.layoutNode?.owner
- ?.focusManager?.clearFocus(force = true)
- ActiveParent, DeactivatedParent, Deactivated, Inactive -> {
- // do nothing.
- }
- }
- }
- parent?.children?.remove(this@FocusModifier)
- newParent?.children?.add(this@FocusModifier)
- }
- }
- focusEventListener = ModifierLocalFocusEvent.current.also { newFocusEventListener ->
- if (newFocusEventListener != focusEventListener) {
- focusEventListener?.removeFocusModifier(this@FocusModifier)
- newFocusEventListener?.addFocusModifier(this@FocusModifier)
- }
- }
- focusRequester = ModifierLocalFocusRequester.current.also { newFocusRequester ->
- if (newFocusRequester != focusRequester) {
- focusRequester?.removeFocusModifier(this@FocusModifier)
- newFocusRequester?.addFocusModifier(this@FocusModifier)
- }
- }
- @OptIn(ExperimentalComposeUiApi::class)
- rotaryScrollParent = ModifierLocalRotaryScrollParent.current
- beyondBoundsLayoutParent = ModifierLocalBeyondBoundsLayout.current
- keyInputModifier = ModifierLocalKeyInput.current
- focusPropertiesModifier = ModifierLocalFocusProperties.current
+ internal fun onRemoved() {
+ when (focusState) {
+ // Clear focus from the current FocusTarget.
+ // This currently clears focus from the entire hierarchy, but we can change the
+ // implementation so that focus is sent to the immediate focus parent.
+ Active, Captured -> requireOwner().focusOwner.clearFocus(force = true)
- // Update the focus node with the current focus properties.
- refreshFocusProperties()
+ ActiveParent, Inactive -> scheduleInvalidationForFocusEvents()
}
}
+ internal fun invalidateFocus() {
+ when (focusState) {
+ // Clear focus from the current FocusTarget.
+ // This currently clears focus from the entire hierarchy, but we can change the
+ // implementation so that focus is sent to the immediate focus parent.
+ Active, Captured -> {
+ lateinit var focusProperties: FocusProperties
+ observeReads {
+ focusProperties = fetchFocusProperties()
+ }
+ if (!focusProperties.canFocus) {
+ requireOwner().focusOwner.clearFocus(force = true)
+ }
+ }
+
+ ActiveParent, Inactive -> {}
+ }
+ }
+
+ /**
+ * Visits parent [FocusPropertiesModifierNode]s and runs
+ * [FocusPropertiesModifierNode.modifyFocusProperties] on each parent.
+ * This effectively collects an aggregated focus state.
+ */
@ExperimentalComposeUiApi
- fun propagateRotaryEvent(event: RotaryScrollEvent): Boolean {
- return rotaryScrollParent?.propagateFocusAwareEvent(event) ?: false
+ internal fun fetchFocusProperties(): FocusProperties {
+ val properties = FocusPropertiesImpl()
+ visitAncestors(Nodes.FocusProperties or Nodes.FocusTarget) {
+ // If we reach the previous default focus properties node, we have gone too far, as
+ // this is applies to the parent focus modifier.
+ if (it.isKind(Nodes.FocusTarget)) return properties
+
+ // Parent can override any values set by this
+ check(it is FocusPropertiesModifierNode)
+ it.modifyFocusProperties(properties)
+ }
+ return properties
}
- // For the RefreshFocusProperties observation. This shouldn't change on the root, so
- // we don't need to keep lambdas around for the root element.
- override val isValid: Boolean
- get() = parent != null
+ internal fun scheduleInvalidationForFocusEvents() {
+ visitAncestors(Nodes.FocusEvent or Nodes.FocusTarget) {
+ if (it.isKind(Nodes.FocusTarget)) return@visitAncestors
- companion object {
- val RefreshFocusProperties: (FocusModifier) -> Unit = { focusModifier ->
- focusModifier.refreshFocusProperties()
+ check(it is FocusEventModifierNode)
+ requireOwner().focusOwner.scheduleInvalidation(it)
}
}
- override val key: ProvidableModifierLocal<FocusModifier?>
- get() = ModifierLocalParentFocusModifier
- override val value: FocusModifier
- get() = this
-
- override fun onPlaced(coordinates: LayoutCoordinates) {
- val wasNull = coordinator == null
- coordinator = coordinates as NodeCoordinator
- if (wasNull) {
- refreshFocusProperties()
- }
- if (focusRequestedOnPlaced) {
- focusRequestedOnPlaced = false
- requestFocus()
- }
+ internal companion object {
+ internal val FocusTargetModifierElement = modifierElementOf(
+ create = { FocusTargetModifierNode() },
+ definitions = { name = "focusTarget" }
+ )
}
}
@@ -192,13 +134,8 @@
*
* @sample androidx.compose.ui.samples.FocusableSampleUsingLowerLevelFocusTarget
*/
-fun Modifier.focusTarget(): Modifier = composed(debugInspectorInfo { name = "focusTarget" }) {
- val focusModifier = remember { FocusModifier(Inactive) }
- SideEffect {
- focusModifier.sendOnFocusEvent()
- }
- focusTarget(focusModifier)
-}
+@OptIn(ExperimentalComposeUiApi::class)
+fun Modifier.focusTarget(): Modifier = this then FocusTargetModifierNode.FocusTargetModifierElement
/**
* Add this modifier to a component to make it focusable.
@@ -207,76 +144,4 @@
"Replaced by focusTarget",
ReplaceWith("focusTarget()", "androidx.compose.ui.focus.focusTarget")
)
-fun Modifier.focusModifier(): Modifier = composed(debugInspectorInfo { name = "focusModifier" }) {
- val focusModifier = remember { FocusModifier(Inactive) }
- SideEffect {
- focusModifier.sendOnFocusEvent()
- }
- focusTarget(focusModifier)
-}
-
-/**
- * A helper function that allows you to pass in an instance of FocusModifier.
- * This is only used internally, to set the root focus modifier or in tests where we need to set an
- * initial focus state or inspect the focus modifier state after running some operation.
- */
-internal fun Modifier.focusTarget(focusModifier: FocusModifier): Modifier {
- return this.then(focusModifier).then(ResetFocusModifierLocals)
-}
-
-/**
- * The Focus Modifier reads the state of some Modifier Locals that are set by the parents. Consider
- * the following example:
- *
- * Box(
- * Modifier
- * .focusRequester(item1)
- * .onFocusChanged { ... }
- * .focusProperties {
- * canFocus = false
- * next = item2
- * }
- * .focusTarget() // focusModifier1
- * ) {
- * Box(
- * Modifier.focusTarget() // focusModifier2
- * )
- * }
- *
- * Here, the focusRequester, onFocusChanged, and focusProperties modifiers provide
- * modifier local values that are intended for focusModifier1.
- *
- * We don't want these modifier locals to be read by focusModifier2.
- *
- * Add this modifier after every FocusModifier to reset all the focus related modifier locals, so
- * that focus modifiers further down the tree do not read these values.
- */
-internal val ResetFocusModifierLocals: Modifier = Modifier
- // Reset the FocusProperties modifier local.
- .then(
- object : ModifierLocalProvider<FocusPropertiesModifier?> {
- override val key: ProvidableModifierLocal<FocusPropertiesModifier?>
- get() = ModifierLocalFocusProperties
- override val value: FocusPropertiesModifier?
- get() = null
- }
-
- )
- // Update the FocusEvent listener modifier local value to null.
- .then(
- object : ModifierLocalProvider<FocusEventModifierLocal?> {
- override val key: ProvidableModifierLocal<FocusEventModifierLocal?>
- get() = ModifierLocalFocusEvent
- override val value: FocusEventModifierLocal?
- get() = null
- }
- )
- // Update the FocusRequesters modifier local value to null.
- .then(
- object : ModifierLocalProvider<FocusRequesterModifierLocal?> {
- override val key: ProvidableModifierLocal<FocusRequesterModifierLocal?>
- get() = ModifierLocalFocusRequester
- override val value: FocusRequesterModifierLocal?
- get() = null
- }
- )
\ No newline at end of file
+fun Modifier.focusModifier(): Modifier = focusTarget()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOrderModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOrderModifier.kt
index f011da1..d82bd8e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOrderModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOrderModifier.kt
@@ -212,10 +212,12 @@
* focus search.
* @param layoutDirection the current system [LayoutDirection].
*/
-internal fun FocusModifier.customFocusSearch(
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun FocusTargetModifierNode.customFocusSearch(
focusDirection: FocusDirection,
layoutDirection: LayoutDirection
): FocusRequester {
+ val focusProperties = fetchFocusProperties()
return when (focusDirection) {
Next -> focusProperties.next
Previous -> focusProperties.previous
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
new file mode 100644
index 0000000..7393f77
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2022 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.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.rotary.RotaryScrollEvent
+import androidx.compose.ui.unit.LayoutDirection
+
+/**
+ * The focus owner provides some internal APIs that are not exposed by focus manager.
+ */
+internal interface FocusOwner : FocusManager {
+
+ /**
+ * A [Modifier] that can be added to the [Owners][androidx.compose.ui.node.Owner] modifier
+ * list that contains the modifiers required by the focus system. (Eg, a root focus modifier).
+ */
+ val modifier: Modifier
+
+ /**
+ * The owner sets the layoutDirection that is then used during focus search.
+ */
+ var layoutDirection: LayoutDirection
+
+ /**
+ * The [Owner][androidx.compose.ui.node.Owner] calls this function when it gains focus. This
+ * informs the [focus manager][FocusOwnerImpl] that the
+ * [Owner][androidx.compose.ui.node.Owner] gained focus, and that it should propagate this
+ * focus to one of the focus modifiers in the component hierarchy.
+ */
+ fun takeFocus()
+
+ /**
+ * The [Owner][androidx.compose.ui.node.Owner] calls this function when it loses focus. This
+ * informs the [focus manager][FocusOwnerImpl] that the
+ * [Owner][androidx.compose.ui.node.Owner] lost focus, and that it should clear focus from
+ * all the focus modifiers in the component hierarchy.
+ */
+ fun releaseFocus()
+
+ /**
+ * Call this function to set the focus to the root focus modifier.
+ *
+ * @param force: Whether we should forcefully clear focus regardless of whether we have
+ * any components that have captured focus.
+ *
+ * @param refreshFocusEvents: Whether we should send an event up the hierarchy to update
+ * the associated onFocusEvent nodes.
+ *
+ * This could be used to clear focus when a user clicks on empty space outside a focusable
+ * component.
+ */
+ fun clearFocus(force: Boolean, refreshFocusEvents: Boolean)
+
+ /**
+ * Searches for the currently focused item, and returns its coordinates as a rect.
+ */
+ fun getFocusRect(): Rect?
+
+ /**
+ * Dispatches a key event through the compose hierarchy.
+ */
+ fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean
+
+ /**
+ * Dispatches a rotary scroll event through the compose hierarchy.
+ */
+ @OptIn(ExperimentalComposeUiApi::class)
+ fun dispatchRotaryEvent(event: RotaryScrollEvent): Boolean
+
+ /**
+ * Schedule a FocusTarget node to be invalidated after onApplyChanges.
+ */
+ @OptIn(ExperimentalComposeUiApi::class)
+ fun scheduleInvalidation(node: FocusTargetModifierNode)
+
+ /**
+ * Schedule a FocusEvent node to be invalidated after onApplyChanges.
+ */
+ @OptIn(ExperimentalComposeUiApi::class)
+ fun scheduleInvalidation(node: FocusEventModifierNode)
+
+ /**
+ * Schedule a FocusProperties node to be invalidated after onApplyChanges.
+ */
+ @OptIn(ExperimentalComposeUiApi::class)
+ fun scheduleInvalidation(node: FocusPropertiesModifierNode)
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt
index 54178d3..5dd4a6e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt
@@ -16,31 +16,32 @@
package androidx.compose.ui.focus
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ModifierLocalReadScope
-import androidx.compose.ui.modifier.modifierLocalOf
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.modifierElementOf
+import androidx.compose.ui.node.requireOwner
+import androidx.compose.ui.node.visitChildren
/**
- * A Modifier local that stores [FocusProperties] for a sub-hierarchy.
- *
- * @see [focusProperties]
+ * Implement this interface create a modifier node that can be used to modify the focus properties
+ * of the associated [FocusTargetModifierNode].
*/
-internal val ModifierLocalFocusProperties =
- modifierLocalOf<FocusPropertiesModifier?> { null }
+@ExperimentalComposeUiApi
+interface FocusPropertiesModifierNode : DelegatableNode {
+ /**
+ * A parent can modify the focus properties associated with the nearest
+ * [FocusTargetModifierNode] child node. If a [FocusTargetModifierNode] has multiple parent
+ * [FocusPropertiesModifierNode]s, properties set by a parent higher up in the hierarchy
+ * overwrite properties set by those that are lower in the hierarchy.
+ */
+ fun modifyFocusProperties(focusProperties: FocusProperties)
+}
/**
- * Properties that are applied to [focusTarget]s that can read the [ModifierLocalFocusProperties]
- * Modifier Local.
+ * Properties that are applied to [focusTarget] that is the first child of the
+ * [FocusPropertiesModifierNode] that sets these properties.
*
* @see [focusProperties]
*/
@@ -178,53 +179,30 @@
*
* @sample androidx.compose.ui.samples.FocusPropertiesSample
*/
+@Suppress("ModifierInspectorInfo") // b/251831790.
fun Modifier.focusProperties(scope: FocusProperties.() -> Unit): Modifier = this.then(
- FocusPropertiesModifier(
- focusPropertiesScope = scope,
- inspectorInfo = debugInspectorInfo {
+ @OptIn(ExperimentalComposeUiApi::class)
+ modifierElementOf(
+ key = scope,
+ create = { FocusPropertiesModifierNodeImpl(scope) },
+ update = { it.focusPropertiesScope = scope },
+ definitions = {
name = "focusProperties"
properties["scope"] = scope
}
)
)
-@Stable
-internal class FocusPropertiesModifier(
- val focusPropertiesScope: FocusProperties.() -> Unit,
- inspectorInfo: InspectorInfo.() -> Unit
-) : ModifierLocalConsumer,
- ModifierLocalProvider<FocusPropertiesModifier?>,
- InspectorValueInfo(inspectorInfo) {
+@OptIn(ExperimentalComposeUiApi::class)
+internal class FocusPropertiesModifierNodeImpl(
+ internal var focusPropertiesScope: FocusProperties.() -> Unit,
+) : FocusPropertiesModifierNode, Modifier.Node() {
- private var parent: FocusPropertiesModifier? by mutableStateOf(null)
-
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
- parent = scope.run { ModifierLocalFocusProperties.current }
- }
-
- override val key = ModifierLocalFocusProperties
-
- override val value: FocusPropertiesModifier
- get() = this
-
- override fun equals(other: Any?) =
- other is FocusPropertiesModifier && focusPropertiesScope == other.focusPropertiesScope
-
- override fun hashCode() = focusPropertiesScope.hashCode()
-
- fun calculateProperties(focusProperties: FocusProperties) {
- // Populate with the specified focus properties.
+ override fun modifyFocusProperties(focusProperties: FocusProperties) {
focusProperties.apply(focusPropertiesScope)
-
- // Parent can override any values set by this
- parent?.calculateProperties(focusProperties)
}
}
-internal fun FocusModifier.setUpdatedProperties(properties: FocusProperties) {
- if (properties.canFocus) activateNode() else deactivateNode()
-}
-
internal class FocusPropertiesImpl : FocusProperties {
override var canFocus: Boolean = true
override var next: FocusRequester = FocusRequester.Default
@@ -241,29 +219,11 @@
override var exit: (FocusDirection) -> FocusRequester = { FocusRequester.Default }
}
-internal fun FocusProperties.clear() {
- canFocus = true
- next = FocusRequester.Default
- previous = FocusRequester.Default
- up = FocusRequester.Default
- down = FocusRequester.Default
- left = FocusRequester.Default
- right = FocusRequester.Default
- start = FocusRequester.Default
- end = FocusRequester.Default
- @OptIn(ExperimentalComposeUiApi::class)
- enter = { FocusRequester.Default }
- @OptIn(ExperimentalComposeUiApi::class)
- exit = { FocusRequester.Default }
-}
-
-internal fun FocusModifier.refreshFocusProperties() {
- val coordinator = coordinator ?: return
- focusProperties.clear()
- coordinator.layoutNode.owner?.snapshotObserver?.observeReads(this,
- FocusModifier.RefreshFocusProperties
- ) {
- focusPropertiesModifier?.calculateProperties(focusProperties)
+@ExperimentalComposeUiApi
+internal fun FocusPropertiesModifierNode.scheduleInvalidationOfAssociatedFocusTargets() {
+ visitChildren(Nodes.FocusTarget) {
+ // Schedule invalidation for the focus target,
+ // which will cause it to recalculate focus properties.
+ requireOwner().focusOwner.scheduleInvalidation(it)
}
- setUpdatedProperties(focusProperties)
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
index 3d819cd..2c66b55 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
@@ -19,6 +19,8 @@
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.visitChildren
private const val focusRequesterNotInitialized = """
FocusRequester is not initialized. Here are some possible fixes:
@@ -40,8 +42,8 @@
*/
class FocusRequester {
- internal val focusRequesterModifierLocals: MutableVector<FocusRequesterModifierLocal> =
- mutableVectorOf()
+ @OptIn(ExperimentalComposeUiApi::class)
+ internal val focusRequesterNodes: MutableVector<FocusRequesterModifierNode> = mutableVectorOf()
/**
* Use this function to request focus. If the system grants focus to a component associated
@@ -51,12 +53,12 @@
* @sample androidx.compose.ui.samples.RequestFocusSample
*/
fun requestFocus() {
- check(focusRequesterModifierLocals.isNotEmpty()) { focusRequesterNotInitialized }
- performRequestFocus {
- it.requestFocus()
- // TODO(b/245755256): Make focusModifier.requestFocus() return a Boolean.
- true
- }
+ @OptIn(ExperimentalComposeUiApi::class)
+ check(focusRequesterNodes.isNotEmpty()) { focusRequesterNotInitialized }
+ // TODO(b/245755256): Add another API that returns a Boolean indicating
+ // whether requestFocus succeeded or not.
+ @OptIn(ExperimentalComposeUiApi::class)
+ findFocusTarget { it.requestFocus() }
}
/**
@@ -69,15 +71,22 @@
* associated with this [FocusRequester].
*/
@OptIn(ExperimentalComposeUiApi::class)
- internal fun performRequestFocus(onFound: (FocusModifier) -> Boolean): Boolean? = when (this) {
- Cancel -> false
- Default -> null
- else -> {
- var success = false
- focusRequesterModifierLocals.forEach {
- it.findFocusNode()?.let { success = onFound.invoke(it) || success }
+ internal fun findFocusTarget(onFound: (FocusTargetModifierNode) -> Boolean): Boolean? {
+ return when (this) {
+ Cancel -> false
+ Default -> null
+ else -> {
+ var success: Boolean? = null
+ focusRequesterNodes.forEach { node ->
+ node.visitChildren(Nodes.FocusTarget) {
+ if (onFound(it)) {
+ success = true
+ return@forEach
+ }
+ }
+ }
+ success
}
- success
}
}
@@ -96,17 +105,15 @@
*
* @sample androidx.compose.ui.samples.CaptureFocusSample
*/
+ @OptIn(ExperimentalComposeUiApi::class)
fun captureFocus(): Boolean {
- check(focusRequesterModifierLocals.isNotEmpty()) { focusRequesterNotInitialized }
- var success = false
- focusRequesterModifierLocals.forEach {
- it.findFocusNode()?.apply {
- if (captureFocus()) {
- success = true
- }
+ check(focusRequesterNodes.isNotEmpty()) { focusRequesterNotInitialized }
+ focusRequesterNodes.forEach {
+ if (it.captureFocus()) {
+ return true
}
}
- return success
+ return false
}
/**
@@ -123,17 +130,15 @@
*
* @sample androidx.compose.ui.samples.CaptureFocusSample
*/
+ @OptIn(ExperimentalComposeUiApi::class)
fun freeFocus(): Boolean {
- check(focusRequesterModifierLocals.isNotEmpty()) { focusRequesterNotInitialized }
- var success = false
- focusRequesterModifierLocals.forEach {
- it.findFocusNode()?.apply {
- if (freeFocus()) {
- success = true
- }
+ check(focusRequesterNodes.isNotEmpty()) { focusRequesterNotInitialized }
+ focusRequesterNodes.forEach {
+ if (it.freeFocus()) {
+ return true
}
}
- return success
+ return false
}
companion object {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifier.kt
index 664b1ec..757a52a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifier.kt
@@ -16,18 +16,20 @@
package androidx.compose.ui.focus
-import androidx.compose.runtime.collection.MutableVector
-import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ModifierLocalReadScope
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.modifier.modifierLocalOf
-import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.modifierElementOf
+import androidx.compose.ui.node.visitChildren
+
+/**
+ * Implement this interface to create a modifier node that can be used to request changes in
+ * the focus state of a [FocusTargetModifierNode] down the hierarchy.
+ */
+@ExperimentalComposeUiApi
+interface FocusRequesterModifierNode : DelegatableNode
/**
* A [modifier][Modifier.Element] that is used to pass in a [FocusRequester] that can be used to
@@ -38,6 +40,7 @@
* @see FocusRequester
* @see Modifier.focusRequester
*/
+@Deprecated("Use FocusRequesterModifierNode instead")
@JvmDefaultWithCompatibility
interface FocusRequesterModifier : Modifier.Element {
/**
@@ -48,78 +51,81 @@
val focusRequester: FocusRequester
}
-internal val ModifierLocalFocusRequester = modifierLocalOf<FocusRequesterModifierLocal?> { null }
-
-internal class FocusRequesterModifierLocal(
- val focusRequester: FocusRequester
-) : ModifierLocalConsumer, ModifierLocalProvider<FocusRequesterModifierLocal?> {
- private var parent: FocusRequesterModifierLocal? = null
- private val focusModifiers = mutableVectorOf<FocusModifier>()
-
- init {
- focusRequester.focusRequesterModifierLocals += this
+/**
+ * Use this function to request focus. If the system grants focus to a component associated
+ * with this [FocusRequester], its [onFocusChanged] modifiers will receive a [FocusState] object
+ * where [FocusState.isFocused] is true.
+ *
+ * @sample androidx.compose.ui.samples.RequestFocusSample
+ */
+@ExperimentalComposeUiApi
+fun FocusRequesterModifierNode.requestFocus(): Boolean {
+ visitChildren(Nodes.FocusTarget) {
+ if (it.requestFocus()) return true
}
+ return false
+}
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {
- val newParent = ModifierLocalFocusRequester.current
- if (newParent != parent) {
- parent?.removeFocusModifiers(focusModifiers)
- newParent?.addFocusModifiers(focusModifiers)
- parent = newParent
+/**
+ * Deny requests to clear focus.
+ *
+ * Use this function to send a request to capture focus. If a component captures focus,
+ * it will send a [FocusState] object to its associated [onFocusChanged]
+ * modifiers where [FocusState.isCaptured]() == true.
+ *
+ * When a component is in a Captured state, all focus requests from other components are
+ * declined.
+ *
+ * @return true if the focus was successfully captured by one of the
+ * [focus][focusTarget] modifiers associated with this [FocusRequester]. False otherwise.
+ *
+ * @sample androidx.compose.ui.samples.CaptureFocusSample
+ */
+@ExperimentalComposeUiApi
+fun FocusRequesterModifierNode.captureFocus(): Boolean {
+ visitChildren(Nodes.FocusTarget) {
+ if (it.captureFocus()) {
+ // it.refreshFocusEventNodes()
+ return true
}
}
+ return false
+}
- override val key: ProvidableModifierLocal<FocusRequesterModifierLocal?>
- get() = ModifierLocalFocusRequester
- override val value: FocusRequesterModifierLocal
- get() = this
+/**
+ * Use this function to send a request to free focus when one of the components associated
+ * with this [FocusRequester] is in a Captured state. If a component frees focus,
+ * it will send a [FocusState] object to its associated [onFocusChanged]
+ * modifiers where [FocusState.isCaptured]() == false.
+ *
+ * When a component is in a Captured state, all focus requests from other components are
+ * declined.
+ *.
+ * @return true if the captured focus was successfully released. i.e. At the end of this
+ * operation, one of the components associated with this [focusRequester] freed focus.
+ *
+ * @sample androidx.compose.ui.samples.CaptureFocusSample
+ */
+@ExperimentalComposeUiApi
+fun FocusRequesterModifierNode.freeFocus(): Boolean {
+ visitChildren(Nodes.FocusTarget) {
+ if (it.freeFocus()) return true
+ }
+ return false
+}
- fun addFocusModifier(focusModifier: FocusModifier) {
- focusModifiers += focusModifier
- parent?.addFocusModifier(focusModifier)
+@OptIn(ExperimentalComposeUiApi::class)
+internal class FocusRequesterModifierNodeImpl(
+ var focusRequester: FocusRequester
+) : FocusRequesterModifierNode, Modifier.Node() {
+ override fun onAttach() {
+ super.onAttach()
+ focusRequester.focusRequesterNodes += this
}
- fun addFocusModifiers(newModifiers: MutableVector<FocusModifier>) {
- focusModifiers.addAll(newModifiers)
- parent?.addFocusModifiers(newModifiers)
- }
-
- fun removeFocusModifier(focusModifier: FocusModifier) {
- focusModifiers -= focusModifier
- parent?.removeFocusModifier(focusModifier)
- }
-
- fun removeFocusModifiers(removedModifiers: MutableVector<FocusModifier>) {
- focusModifiers.removeAll(removedModifiers)
- parent?.removeFocusModifiers(removedModifiers)
- }
-
- @Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
- fun findFocusNode(): FocusModifier? {
- // find the first child:
- val first = focusModifiers.fold(null as FocusModifier?) { mod1, mod2 ->
- var layoutNode1 = mod1?.coordinator?.layoutNode ?: return@fold mod2
- var layoutNode2 = mod2.coordinator?.layoutNode ?: return@fold mod1
-
- while (layoutNode1.depth > layoutNode2.depth) {
- layoutNode1 = layoutNode1.parent!!
- }
-
- while (layoutNode2.depth > layoutNode1.depth) {
- layoutNode2 = layoutNode2.parent!!
- }
-
- while (layoutNode1.parent != layoutNode2.parent) {
- layoutNode1 = layoutNode1.parent!!
- layoutNode2 = layoutNode2.parent!!
- }
- val children = layoutNode1.parent!!.children
- val index1 = children.indexOf(layoutNode1)
- val index2 = children.indexOf(layoutNode2)
- if (index1 < index2) mod1 else mod2
- }
-
- return first
+ override fun onDetach() {
+ focusRequester.focusRequesterNodes -= this
+ super.onDetach()
}
}
@@ -128,12 +134,20 @@
*
* @sample androidx.compose.ui.samples.RequestFocusSample
*/
-fun Modifier.focusRequester(focusRequester: FocusRequester): Modifier =
- composed(debugInspectorInfo {
- name = "focusRequester"
- properties["focusRequester"] = focusRequester
- }) {
- remember(focusRequester) {
- FocusRequesterModifierLocal(focusRequester)
+@Suppress("ModifierInspectorInfo") // b/251831790.
+fun Modifier.focusRequester(focusRequester: FocusRequester): Modifier = this.then(
+ @OptIn(ExperimentalComposeUiApi::class)
+ modifierElementOf(
+ key = focusRequester,
+ create = { FocusRequesterModifierNodeImpl(focusRequester) },
+ update = {
+ it.focusRequester.focusRequesterNodes -= it
+ it.focusRequester = focusRequester
+ it.focusRequester.focusRequesterNodes += it
+ },
+ definitions = {
+ name = "focusRequester"
+ properties["focusRequester"] = focusRequester
}
- }
+ )
+)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
index 4dd57ea..1400773 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusState.kt
@@ -17,8 +17,8 @@
package androidx.compose.ui.focus
/**
- * The focus state of a [FocusModifier]. Use [onFocusChanged] or [onFocusEvent] modifiers to
- * access [FocusState].
+ * The focus state of a [FocusTargetModifierNode]. Use [onFocusChanged] or [onFocusEvent] modifiers
+ * to access [FocusState].
*
* @sample androidx.compose.ui.samples.FocusableSample
*/
@@ -69,12 +69,6 @@
*/
Captured,
- /** The focusable component is not currently focusable. (eg. A disabled button). */
- Deactivated,
-
- /** One of the descendants of this deactivated component is Active. */
- DeactivatedParent,
-
/**
* The focusable component does not receive any key events. (ie it is not active, nor are any
* of its descendants active).
@@ -84,30 +78,18 @@
override val isFocused: Boolean
get() = when (this) {
Captured, Active -> true
- ActiveParent, Deactivated, DeactivatedParent, Inactive -> false
+ ActiveParent, Inactive -> false
}
override val hasFocus: Boolean
get() = when (this) {
- Active, ActiveParent, Captured, DeactivatedParent -> true
- Deactivated, Inactive -> false
+ Active, ActiveParent, Captured -> true
+ Inactive -> false
}
override val isCaptured: Boolean
get() = when (this) {
Captured -> true
- Active, ActiveParent, Deactivated, DeactivatedParent, Inactive -> false
- }
-
- /**
- * Whether the focusable component is deactivated.
- *
- * TODO(ralu): Consider making this public when we can add methods to interfaces without
- * breaking compatibility.
- */
- val isDeactivated: Boolean
- get() = when (this) {
- Active, ActiveParent, Captured, Inactive -> false
- Deactivated, DeactivatedParent -> true
+ Active, ActiveParent, Inactive -> false
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
index 2eea72e..831b425 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTransactions.kt
@@ -20,77 +20,43 @@
import androidx.compose.ui.focus.FocusStateImpl.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
-import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
import androidx.compose.ui.focus.FocusStateImpl.Inactive
+import androidx.compose.ui.node.Nodes.FocusTarget
+import androidx.compose.ui.node.nearestAncestor
+import androidx.compose.ui.node.observeReads
/**
* Request focus for this node.
*
- * In Compose, the parent [FocusNode][FocusModifier] controls focus for its focusable
+ * In Compose, the parent [FocusNode][FocusTargetModifierNode] controls focus for its focusable
* children. Calling this function will send a focus request to this
- * [FocusNode][FocusModifier]'s parent [FocusNode][FocusModifier].
+ * [FocusNode][FocusTargetModifierNode]'s parent [FocusNode][FocusTargetModifierNode].
*/
-internal fun FocusModifier.requestFocus() {
- if (coordinator?.layoutNode?.owner == null) {
- // Not placed yet. Try requestFocus() after placement.
- focusRequestedOnPlaced = true
- return
+@ExperimentalComposeUiApi
+internal fun FocusTargetModifierNode.requestFocus(): Boolean {
+ check(node.isAttached)
+ val focusProperties = fetchFocusProperties()
+ // If the node is deactivated, we perform a moveFocus(Enter).
+ if (!focusProperties.canFocus) {
+ return findChildCorrespondingToFocusEnter(FocusDirection.Enter) {
+ it.requestFocus()
+ }
}
- when (focusState) {
+ when (focusStateImpl) {
Active, Captured -> {
// There is no change in focus state, but we send a focus event to notify the user
// that the focus request is completed.
- sendOnFocusEvent()
+ refreshFocusEventNodes()
+ return true
}
- ActiveParent -> if (clearChildFocus()) grantFocus()
- Deactivated, DeactivatedParent -> {
- // If the node is deactivated, we perform a moveFocus(Enter).
- @OptIn(ExperimentalComposeUiApi::class)
- findChildCorrespondingToFocusEnter(FocusDirection.Enter) {
- it.requestFocus()
- true
- }
+ ActiveParent -> return (clearChildFocus() && grantFocus()).also { success ->
+ if (success) refreshFocusEventNodes()
}
- Inactive -> {
- val focusParent = parent
- if (focusParent != null) {
- focusParent.requestFocusForChild(this)
- } else if (requestFocusForOwner()) {
- grantFocus()
- }
- }
- }
-}
-
-/**
- * Activate this node so that it can be focused.
- *
- * Deactivated nodes are excluded from focus search, and reject requests to gain focus.
- * Calling this function activates a deactivated node.
- */
-internal fun FocusModifier.activateNode() {
- when (focusState) {
- ActiveParent, Active, Captured, Inactive -> {}
- Deactivated -> focusState = Inactive
- DeactivatedParent -> focusState = ActiveParent
- }
-}
-
-/**
- * Deactivate this node so that it can't be focused.
- *
- * Deactivated nodes are excluded from focus search.
- */
-internal fun FocusModifier.deactivateNode() {
- when (focusState) {
- ActiveParent -> focusState = DeactivatedParent
- Active, Captured -> {
- coordinator?.layoutNode?.owner?.focusManager?.clearFocus(force = true)
- focusState = Deactivated
- }
- Inactive -> focusState = Deactivated
- Deactivated, DeactivatedParent -> {}
+ Inactive -> return nearestAncestor(FocusTarget)
+ ?.requestFocusForChild(this)
+ ?: (requestFocusForOwner() && grantFocus()).also { success ->
+ if (success) refreshFocusEventNodes()
+ }
}
}
@@ -102,13 +68,15 @@
*
* @return true if the focus was successfully captured. False otherwise.
*/
-internal fun FocusModifier.captureFocus() = when (focusState) {
+@ExperimentalComposeUiApi
+internal fun FocusTargetModifierNode.captureFocus() = when (focusStateImpl) {
Active -> {
- focusState = Captured
+ focusStateImpl = Captured
+ refreshFocusEventNodes()
true
}
Captured -> true
- ActiveParent, Deactivated, DeactivatedParent, Inactive -> false
+ ActiveParent, Inactive -> false
}
/**
@@ -118,63 +86,60 @@
*
* @return true if the captured focus was released. False Otherwise.
*/
-internal fun FocusModifier.freeFocus() = when (focusState) {
+@ExperimentalComposeUiApi
+internal fun FocusTargetModifierNode.freeFocus() = when (focusStateImpl) {
Captured -> {
- focusState = Active
+ focusStateImpl = Active
+ refreshFocusEventNodes()
true
}
Active -> true
- ActiveParent, Deactivated, DeactivatedParent, Inactive -> false
+ ActiveParent, Inactive -> false
}
/**
* This function clears focus from this node.
*
- * Note: This function should only be called by a parent [focus node][FocusModifier] to
- * clear focus from one of its child [focus node][FocusModifier]s. It does not change the
+ * Note: This function should only be called by a parent [focus node][FocusTargetModifierNode] to
+ * clear focus from one of its child [focus node][FocusTargetModifierNode]s. It does not change the
* state of the parent.
*/
-internal fun FocusModifier.clearFocus(forcedClear: Boolean = false): Boolean {
- return when (focusState) {
- Active -> {
- focusState = Inactive
- true
- }
- /**
- * If the node is [ActiveParent], we need to clear focus from the [Active] descendant
- * first, before clearing focus from this node.
- */
- ActiveParent -> if (clearChildFocus()) {
- focusState = Inactive
- true
- } else {
- false
- }
- /**
- * If the node is [DeactivatedParent], we need to clear focus from the [Active] descendant
- * first, before clearing focus from this node.
- */
- DeactivatedParent -> if (clearChildFocus()) {
- focusState = Deactivated
- true
- } else {
- false
- }
-
- /**
- * If the node is [Captured], deny requests to clear focus, except for a forced clear.
- */
- Captured -> {
- if (forcedClear) {
- focusState = Inactive
- }
- forcedClear
- }
- /**
- * Nothing to do if the node is not focused.
- */
- Inactive, Deactivated -> true
+@ExperimentalComposeUiApi
+internal fun FocusTargetModifierNode.clearFocus(
+ forced: Boolean = false,
+ refreshFocusEvents: Boolean
+): Boolean = when (focusStateImpl) {
+ Active -> {
+ focusStateImpl = Inactive
+ if (refreshFocusEvents) refreshFocusEventNodes()
+ true
}
+ /**
+ * If the node is [ActiveParent], we need to clear focus from the [Active] descendant
+ * first, before clearing focus from this node.
+ */
+ ActiveParent -> if (clearChildFocus(forced, refreshFocusEvents)) {
+ focusStateImpl = Inactive
+ if (refreshFocusEvents) refreshFocusEventNodes()
+ true
+ } else {
+ false
+ }
+
+ /**
+ * If the node is [Captured], deny requests to clear focus, except for a forced clear.
+ */
+ Captured -> {
+ if (forced) {
+ focusStateImpl = Inactive
+ if (refreshFocusEvents) refreshFocusEventNodes()
+ }
+ forced
+ }
+ /**
+ * Nothing to do if the node is not focused.
+ */
+ Inactive -> true
}
/**
@@ -182,82 +147,84 @@
* Note: This is a private function that just changes the state of this node and does not affect any
* other nodes in the hierarchy.
*/
-private fun FocusModifier.grantFocus() {
+@OptIn(ExperimentalComposeUiApi::class)
+private fun FocusTargetModifierNode.grantFocus(): Boolean {
+ // When we grant focus to this node, we need to observe changes to the canFocus property.
+ // If canFocus is set to false, we need to clear focus.
+ observeReads { fetchFocusProperties() }
// No Focused Children, or we don't want to propagate focus to children.
- focusState = when (focusState) {
- Inactive, Active, ActiveParent -> Active
- Captured -> Captured
- Deactivated, DeactivatedParent -> error("Granting focus to a deactivated node.")
+ when (focusStateImpl) {
+ Inactive, ActiveParent -> focusStateImpl = Active
+ Active, Captured -> { /* Already focused. */ }
}
-}
-
-/**
- * This function grants focus to the specified child.
- * Note: This is a private function and should only be called by a parent to grant focus to one of
- * its child. It does not affect any other nodes in the hierarchy.
- */
-private fun FocusModifier.grantFocusToChild(childNode: FocusModifier): Boolean {
- // It's very important that the child node is set before dispatching grantFocus, otherwise a
- // child may end up indirectly trying to walk the focus tree and get a null child.
- focusedChild = childNode
- childNode.grantFocus()
return true
}
/** This function clears any focus from the focused child. */
-private fun FocusModifier.clearChildFocus(): Boolean {
- return if (requireNotNull(focusedChild).clearFocus()) {
- focusedChild = null
- true
- } else {
- false
- }
+@ExperimentalComposeUiApi
+private fun FocusTargetModifierNode.clearChildFocus(
+ forced: Boolean = false,
+ refreshFocusEvents: Boolean = true
+): Boolean {
+ return activeChild?.clearFocus(forced, refreshFocusEvents) ?: true
}
/**
- * Focusable children of this [focus node][FocusModifier] can use this function to request
+ * Focusable children of this [focus node][FocusTargetModifierNode] can use this function to request
* focus.
*
* @param childNode: The node that is requesting focus.
* @return true if focus was granted, false otherwise.
*/
-private fun FocusModifier.requestFocusForChild(childNode: FocusModifier): Boolean {
+@OptIn(ExperimentalComposeUiApi::class)
+private fun FocusTargetModifierNode.requestFocusForChild(
+ childNode: FocusTargetModifierNode
+): Boolean {
// Only this node's children can ask for focus.
- if (childNode !in children) {
+ if (childNode.nearestAncestor(FocusTarget) != this) {
error("Non child node cannot request focus.")
}
- return when (focusState) {
+ return when (focusStateImpl) {
// If this node is [Active], it can give focus to the requesting child.
- Active -> {
- focusState = ActiveParent
- grantFocusToChild(childNode)
+ Active -> childNode.grantFocus().also { success ->
+ if (success) {
+ focusStateImpl = ActiveParent
+ childNode.refreshFocusEventNodes()
+ refreshFocusEventNodes()
+ }
}
// If this node is [ActiveParent] ie, one of the parent's descendants is [Active],
// remove focus from the currently focused child and grant it to the requesting child.
- ActiveParent -> if (clearChildFocus()) grantFocusToChild(childNode) else false
-
- DeactivatedParent -> when {
- // DeactivatedParent && NoFocusChild is used to indicate an intermediate state where
- // this parent requested focus so that it can transfer it to a child.
- focusedChild == null -> grantFocusToChild(childNode)
- clearChildFocus() -> grantFocusToChild(childNode)
- else -> false
+ ActiveParent -> {
+ checkNotNull(activeChild)
+ (clearChildFocus() && childNode.grantFocus()).also { success ->
+ if (success) childNode.refreshFocusEventNodes()
+ }
}
// If this node is not [Active], we must gain focus first before granting it
// to the requesting child.
Inactive -> {
- val focusParent = parent
+ val focusParent = nearestAncestor(FocusTarget)
when {
// If this node is the root, request focus from the compose owner.
focusParent == null && requestFocusForOwner() -> {
- focusState = Active
+ focusStateImpl = Active
+ refreshFocusEventNodes()
requestFocusForChild(childNode)
}
// For non-root nodes, request focus for this node before the child.
- focusParent != null && focusParent.requestFocusForChild(this) ->
- requestFocusForChild(childNode)
+ // We request focus even if this is a deactivated node, as we will end up taking
+ // focus away and granting it to the child.
+ focusParent != null && focusParent.requestFocusForChild(this) -> {
+ requestFocusForChild(childNode).also {
+ // Verify that focus state was granted to the child.
+ // If this child didn't take focus then we can end up in a situation where
+ // a deactivated parent is focused.
+ check(this.focusState == ActiveParent)
+ }
+ }
// Could not gain focus, so have no focus to give.
else -> false
@@ -265,24 +232,10 @@
}
// If this node is [Captured], decline requests from the children.
Captured -> false
- // If this node is [Deactivated], send a requestFocusForChild to its parent to attempt to
- // change its state to [DeactivatedParent] before granting focus to the child.
- Deactivated -> {
- activateNode()
- val childGrantedFocus = requestFocusForChild(childNode)
- deactivateNode()
- childGrantedFocus
- }
}
}
-private fun FocusModifier.requestFocusForOwner(): Boolean {
+@OptIn(ExperimentalComposeUiApi::class)
+private fun FocusTargetModifierNode.requestFocusForOwner(): Boolean {
return coordinator?.layoutNode?.owner?.requestFocus() ?: error("Owner not initialized.")
}
-
-/**
- * Send the current [FocusModifier.focusState] to all [onFocusEvent] listeners.
- */
-internal fun FocusModifier.sendOnFocusEvent() {
- focusEventListener?.propagateFocusEvent()
-}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
index 4e8d3db..c5e7f0b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
@@ -17,7 +17,6 @@
package androidx.compose.ui.focus
import androidx.compose.runtime.collection.MutableVector
-import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.focus.FocusDirection.Companion.Down
import androidx.compose.ui.focus.FocusDirection.Companion.Enter
@@ -32,12 +31,14 @@
import androidx.compose.ui.focus.FocusStateImpl.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
-import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.input.key.KeyInputModifier
import androidx.compose.ui.layout.findRootCoordinates
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.visitAncestors
+import androidx.compose.ui.node.visitChildren
+import androidx.compose.ui.node.visitSubtreeIf
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.LayoutDirection.Ltr
import androidx.compose.ui.unit.LayoutDirection.Rtl
@@ -174,10 +175,11 @@
* @param onFound This lambda is invoked if focus search finds the next focus node.
* @return if no focus node is found, we return false. otherwise we return the result of [onFound].
*/
-internal fun FocusModifier.focusSearch(
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun FocusTargetModifierNode.focusSearch(
focusDirection: FocusDirection,
layoutDirection: LayoutDirection,
- onFound: (FocusModifier) -> Boolean
+ onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
return when (focusDirection) {
Next, Previous -> oneDimensionalFocusSearch(focusDirection, onFound)
@@ -189,105 +191,95 @@
findActiveFocusNode()?.twoDimensionalFocusSearch(direction, onFound) ?: false
}
@OptIn(ExperimentalComposeUiApi::class)
- Exit -> findActiveFocusNode()?.findActiveParent().let {
- if (it == this || it == null) false else onFound.invoke(it)
+ Exit -> findActiveFocusNode()?.findNonDeactivatedParent().let {
+ if (it == null || it == this) false else onFound.invoke(it)
}
else -> error(invalidFocusDirection)
}
}
-@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
-internal fun FocusModifier.findActiveFocusNode(): FocusModifier? {
- return when (focusState) {
- Active, Captured -> this
- ActiveParent, DeactivatedParent -> focusedChild?.findActiveFocusNode()
- Inactive, Deactivated -> null
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun FocusTargetModifierNode.findActiveFocusNode(): FocusTargetModifierNode? {
+ when (focusStateImpl) {
+ Active, Captured -> return this
+ ActiveParent -> {
+ visitChildren(Nodes.FocusTarget) { node ->
+ node.findActiveFocusNode()?.let { return it }
+ }
+ return null
+ }
+ Inactive -> return null
}
}
@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
-internal fun FocusModifier.findActiveParent(): FocusModifier? = parent?.let {
- when (focusState) {
- Active, Captured, Deactivated, DeactivatedParent, Inactive -> it.findActiveParent()
- ActiveParent -> this
- }
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun FocusTargetModifierNode.findNonDeactivatedParent(): FocusTargetModifierNode? {
+ visitAncestors(Nodes.FocusTarget) {
+ if (it.fetchFocusProperties().canFocus) return it
}
+ return null
+}
/**
* Returns the bounding box of the focus layout area in the root or [Rect.Zero] if the
* FocusModifier has not had a layout.
*/
-internal fun FocusModifier.focusRect(): Rect = coordinator?.let {
+@ExperimentalComposeUiApi
+internal fun FocusTargetModifierNode.focusRect(): Rect = coordinator?.let {
it.findRootCoordinates().localBoundingBoxOf(it, clipBounds = false)
} ?: Rect.Zero
+
/**
- * Returns all [FocusModifier] children that are not [FocusStateImpl.isDeactivated]. Any
+ * Returns all [FocusTargetModifierNode] children that are not Deactivated. Any
* child that is deactivated will add activated children instead, unless the deactivated
* node has a custom Enter specified.
*/
-internal fun FocusModifier.activatedChildren(): MutableVector<FocusModifier> {
- if (!children.any { it.focusState.isDeactivated }) {
- return children
- }
- val activated = mutableVectorOf<FocusModifier>()
- children.forEach { child ->
- if (!child.focusState.isDeactivated) {
- activated += child
- } else {
- // When we encounter a deactivated child, we add all its children,
- // unless a custom Enter is specified.
- @OptIn(ExperimentalComposeUiApi::class)
- when (val customEnter = child.focusProperties.enter(Enter)) {
- Cancel -> return mutableVectorOf()
- Default -> activated.addAll(child.activatedChildren())
- else -> customEnter.focusRequesterModifierLocals.forEach {
- it.findFocusNode()?.let { activated.add(it) }
- }
+@ExperimentalComposeUiApi
+internal fun DelegatableNode.collectAccessibleChildren(
+ accessibleChildren: MutableVector<FocusTargetModifierNode>
+) {
+ visitSubtreeIf(Nodes.FocusTarget) {
+
+ if (it.fetchFocusProperties().canFocus) {
+ accessibleChildren.add(it)
+ return@visitSubtreeIf false
+ }
+
+ // If we encounter a deactivated child, we mimic a moveFocus(Enter).
+ when (val customEnter = it.fetchFocusProperties().enter(Enter)) {
+ // If the user declined a custom enter, omit this part of the tree.
+ Cancel -> return@visitSubtreeIf false
+
+ // If there is no custom enter, we consider all the children.
+ Default -> return@visitSubtreeIf true
+
+ else -> customEnter.focusRequesterNodes.forEach { node ->
+ node.collectAccessibleChildren(accessibleChildren)
}
}
+ false
}
- return activated
-}
-
-/**
- * Returns the inner-most KeyInputModifier on the same LayoutNode as this FocusModifier.
- */
-@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
-internal fun FocusModifier.findLastKeyInputModifier(): KeyInputModifier? {
- val layoutNode = coordinator?.layoutNode ?: return null
- var best: KeyInputModifier? = null
- keyInputChildren.forEach { keyInputModifier ->
- if (keyInputModifier.layoutNode == layoutNode) {
- best = lastOf(keyInputModifier, best)
- }
- }
- if (best != null) {
- return best
- }
- // There isn't a KeyInputModifier after this, but there may be one before this.
- return keyInputModifier
}
/**
* Whether this node should be considered when searching for the next item during a traversal.
*/
-internal val FocusModifier.isEligibleForFocusSearch: Boolean
+@ExperimentalComposeUiApi
+internal val FocusTargetModifierNode.isEligibleForFocusSearch: Boolean
get() = coordinator?.layoutNode?.isPlaced == true &&
- coordinator?.layoutNode?.isAttached == true
+ coordinator?.layoutNode?.isAttached == true
-/**
- * Returns [one] if it comes after [two] in the modifier chain or [two] if it comes after [one].
- */
-@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
-private fun lastOf(one: KeyInputModifier, two: KeyInputModifier?): KeyInputModifier {
- var mod = two ?: return one
- val layoutNode = one.layoutNode
- while (mod != one) {
- val parent = mod.parent
- if (parent == null || parent.layoutNode != layoutNode) {
- return one
+@ExperimentalComposeUiApi
+internal val FocusTargetModifierNode.activeChild: FocusTargetModifierNode?
+ get() {
+ if (!node.isAttached) return null
+
+ visitChildren(Nodes.FocusTarget) {
+ when (it.focusStateImpl) {
+ Active, ActiveParent, Captured -> return it
+ Inactive -> return@visitChildren
+ }
}
- mod = parent
+ return null
}
- return two
-}
\ No newline at end of file
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 5b0c82b..70b237c 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
@@ -18,63 +18,69 @@
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.focus.FocusDirection.Companion.Next
import androidx.compose.ui.focus.FocusDirection.Companion.Previous
import androidx.compose.ui.focus.FocusStateImpl.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
-import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.nearestAncestor
+import androidx.compose.ui.node.visitChildren
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
private const val InvalidFocusDirection = "This function should only be used for 1-D focus search"
private const val NoActiveChild = "ActiveParent must have a focusedChild"
-internal fun FocusModifier.oneDimensionalFocusSearch(
+@ExperimentalComposeUiApi
+internal fun FocusTargetModifierNode.oneDimensionalFocusSearch(
direction: FocusDirection,
- onFound: (FocusModifier) -> Boolean
+ onFound: (FocusTargetModifierNode) -> Boolean
): Boolean = when (direction) {
Next -> forwardFocusSearch(onFound)
Previous -> backwardFocusSearch(onFound)
else -> error(InvalidFocusDirection)
}
-private fun FocusModifier.forwardFocusSearch(
- onFound: (FocusModifier) -> Boolean
-): Boolean = when (focusState) {
- ActiveParent, DeactivatedParent -> {
- val focusedChild = focusedChild ?: error(NoActiveChild)
+@ExperimentalComposeUiApi
+private fun FocusTargetModifierNode.forwardFocusSearch(
+ onFound: (FocusTargetModifierNode) -> Boolean
+): Boolean = when (focusStateImpl) {
+ ActiveParent -> {
+ val focusedChild = activeChild ?: error(NoActiveChild)
focusedChild.forwardFocusSearch(onFound) ||
generateAndSearchChildren(focusedChild, Next, onFound)
}
- Active, Captured, Deactivated -> pickChildForForwardSearch(onFound)
- Inactive -> onFound.invoke(this)
+ Active, Captured -> pickChildForForwardSearch(onFound)
+ Inactive -> if (fetchFocusProperties().canFocus) {
+ onFound.invoke(this)
+ } else {
+ pickChildForForwardSearch(onFound)
+ }
}
-private fun FocusModifier.backwardFocusSearch(
- onFound: (FocusModifier) -> Boolean
-): Boolean = when (focusState) {
- ActiveParent, DeactivatedParent -> {
- val focusedChild = focusedChild ?: error(NoActiveChild)
+@ExperimentalComposeUiApi
+private fun FocusTargetModifierNode.backwardFocusSearch(
+ onFound: (FocusTargetModifierNode) -> Boolean
+): Boolean = when (focusStateImpl) {
+ ActiveParent -> {
+ val focusedChild = activeChild ?: error(NoActiveChild)
// Unlike forwardFocusSearch, backwardFocusSearch visits the children before the parent.
- when (focusedChild.focusState) {
- ActiveParent -> focusedChild.backwardFocusSearch(onFound) ||
- // Don't forget to visit this item after visiting all its children.
- onFound.invoke(focusedChild)
-
- DeactivatedParent -> focusedChild.backwardFocusSearch(onFound) ||
- // Since this item is deactivated, just skip it and search among its siblings.
- generateAndSearchChildren(focusedChild, Previous, onFound)
+ when (focusedChild.focusStateImpl) {
+ ActiveParent ->
+ focusedChild.backwardFocusSearch(onFound) ||
+ generateAndSearchChildren(focusedChild, Previous, onFound) ||
+ (fetchFocusProperties().canFocus && onFound.invoke(focusedChild))
// Since this item "is focused", it means we already visited all its children.
// So just search among its siblings.
Active, Captured -> generateAndSearchChildren(focusedChild, Previous, onFound)
- Deactivated, Inactive -> error(NoActiveChild)
+ Inactive -> error(NoActiveChild)
}
}
// BackwardFocusSearch is invoked at the root, and so it searches among siblings of the
@@ -82,19 +88,21 @@
// ActiveParent) or a deactivated node (instead of a deactivated parent), it indicates
// that the hierarchy does not have focus. ie. this is the initial focus state.
// So we pick one of the children as the result.
- Active, Captured, Deactivated -> pickChildForBackwardSearch(onFound)
+ Active, Captured -> pickChildForBackwardSearch(onFound)
// If we encounter an inactive node, we attempt to pick one of its children before picking
// this node (backward search visits the children before the parent).
- Inactive -> pickChildForBackwardSearch(onFound) || onFound.invoke(this)
+ Inactive -> pickChildForBackwardSearch(onFound) ||
+ if (fetchFocusProperties().canFocus) onFound.invoke(this) else false
}
// Search among your children for the next child.
// If the next child is not found, generate more children by requesting a beyondBoundsLayout.
-private fun FocusModifier.generateAndSearchChildren(
- focusedItem: FocusModifier,
+@ExperimentalComposeUiApi
+private fun FocusTargetModifierNode.generateAndSearchChildren(
+ focusedItem: FocusTargetModifierNode,
direction: FocusDirection,
- onFound: (FocusModifier) -> Boolean
+ onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
// Search among the currently available children.
if (searchChildren(focusedItem, direction, onFound)) {
@@ -112,14 +120,18 @@
}
// Search for the next sibling that should be granted focus.
-private fun FocusModifier.searchChildren(
- focusedItem: FocusModifier,
+@ExperimentalComposeUiApi
+private fun FocusTargetModifierNode.searchChildren(
+ focusedItem: FocusTargetModifierNode,
direction: FocusDirection,
- onFound: (FocusModifier) -> Boolean
+ onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
- check(focusState == ActiveParent || focusState == DeactivatedParent) {
+ check(focusStateImpl == ActiveParent) {
"This function should only be used within a parent that has focus."
}
+ val children = MutableVector<FocusTargetModifierNode>().apply {
+ visitChildren(Nodes.FocusTarget) { add(it) }
+ }
children.sortWith(FocusableChildrenComparator)
when (direction) {
Next -> children.forEachItemAfter(focusedItem) { child ->
@@ -135,21 +147,29 @@
// backward search, we want to move focus to the parent unless the parent is deactivated.
// We also don't want to move focus to the root because from the user's perspective this would
// look like nothing is focused.
- if (direction == Next || focusState == DeactivatedParent || isRoot()) return false
+ if (direction == Next || !fetchFocusProperties().canFocus || isRoot()) return false
return onFound.invoke(this)
}
-private fun FocusModifier.pickChildForForwardSearch(
- onFound: (FocusModifier) -> Boolean
+@ExperimentalComposeUiApi
+private fun FocusTargetModifierNode.pickChildForForwardSearch(
+ onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
+ val children = MutableVector<FocusTargetModifierNode>().apply {
+ visitChildren(Nodes.FocusTarget) { add(it) }
+ }
children.sortWith(FocusableChildrenComparator)
return children.any { it.isEligibleForFocusSearch && it.forwardFocusSearch(onFound) }
}
-private fun FocusModifier.pickChildForBackwardSearch(
- onFound: (FocusModifier) -> Boolean
+@ExperimentalComposeUiApi
+private fun FocusTargetModifierNode.pickChildForBackwardSearch(
+ onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
+ val children = MutableVector<FocusTargetModifierNode>().apply {
+ visitChildren(Nodes.FocusTarget) { add(it) }
+ }
children.sortWith(FocusableChildrenComparator)
children.forEachReversed {
if (it.isEligibleForFocusSearch && it.backwardFocusSearch(onFound)) {
@@ -159,7 +179,8 @@
return false
}
-private fun FocusModifier.isRoot() = parent == null
+@OptIn(ExperimentalComposeUiApi::class)
+private fun FocusTargetModifierNode.isRoot() = nearestAncestor(Nodes.FocusTarget) == null
@Suppress("BanInlineOptIn")
@OptIn(ExperimentalContracts::class)
@@ -204,20 +225,24 @@
* order index. This would be more expensive than sorting the items. In addition to this, sorting
* the items makes the next focus search more efficient.
*/
-private object FocusableChildrenComparator : Comparator<FocusModifier> {
- override fun compare(focusModifier1: FocusModifier?, focusModifier2: FocusModifier?): Int {
- requireNotNull(focusModifier1)
- requireNotNull(focusModifier2)
+@OptIn(ExperimentalComposeUiApi::class)
+private object FocusableChildrenComparator : Comparator<FocusTargetModifierNode> {
+ override fun compare(
+ focusTarget1: FocusTargetModifierNode?,
+ focusTarget2: FocusTargetModifierNode?
+ ): Int {
+ requireNotNull(focusTarget1)
+ requireNotNull(focusTarget2)
// Ignore focus modifiers that won't be considered during focus search.
- if (!focusModifier1.isEligibleForFocusSearch || !focusModifier2.isEligibleForFocusSearch) {
- if (focusModifier1.isEligibleForFocusSearch) return -1
- if (focusModifier2.isEligibleForFocusSearch) return 1
+ if (!focusTarget1.isEligibleForFocusSearch || !focusTarget2.isEligibleForFocusSearch) {
+ if (focusTarget1.isEligibleForFocusSearch) return -1
+ if (focusTarget2.isEligibleForFocusSearch) return 1
return 0
}
- val layoutNode1 = checkNotNull(focusModifier1.coordinator?.layoutNode)
- val layoutNode2 = checkNotNull(focusModifier2.coordinator?.layoutNode)
+ val layoutNode1 = checkNotNull(focusTarget1.coordinator?.layoutNode)
+ val layoutNode2 = checkNotNull(focusTarget2.coordinator?.layoutNode)
// Use natural order for focus modifiers within the same layout node.
if (layoutNode1 == layoutNode2) return 0
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 5ba0114..55655cb 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
@@ -26,10 +26,10 @@
import androidx.compose.ui.focus.FocusStateImpl.Active
import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
-import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
import androidx.compose.ui.focus.FocusStateImpl.Inactive
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.visitChildren
import kotlin.math.absoluteValue
import kotlin.math.max
@@ -37,38 +37,38 @@
private const val NoActiveChild = "ActiveParent must have a focusedChild"
/**
- * Perform a search among the immediate children of this [node][FocusModifier] in the
+ * Perform a search among the immediate children of this [node][FocusTargetModifierNode] in the
* specified [direction][FocusDirection] and return the node that is to be focused next. If one
* of the children is currently focused, we start from that point and search in the specified
* [direction][FocusDirection]. If none of the children are currently focused, we pick the
* top-left or bottom right based on the specified [direction][FocusDirection].
*/
-internal fun FocusModifier.twoDimensionalFocusSearch(
+@ExperimentalComposeUiApi
+internal fun FocusTargetModifierNode.twoDimensionalFocusSearch(
direction: FocusDirection,
- onFound: (FocusModifier) -> Boolean
+ onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
- when (focusState) {
- Inactive -> return onFound.invoke(this)
- Deactivated -> return false
- ActiveParent, DeactivatedParent -> {
- val focusedChild = focusedChild ?: error(NoActiveChild)
+ when (focusStateImpl) {
+ Inactive -> return if (fetchFocusProperties().canFocus) onFound.invoke(this) else false
+ ActiveParent -> {
+ val focusedChild = activeChild ?: error(NoActiveChild)
// For 2D focus search we only search among siblings. You have to use DPad Center or
// call moveFocus(In) to move focus to a child. So twoDimensionalFocus Search delegates
// search to a child only if it "has focus". If this node "is focused", we just skip the
// children and search among the siblings of the focused item by calling
// "searchChildren" on this node.
- when (focusedChild.focusState) {
+ when (focusedChild.focusStateImpl) {
- ActiveParent, DeactivatedParent -> {
+ ActiveParent -> {
// If the focusedChild is an intermediate parent,
// we continue searching among its children.
if (focusedChild.twoDimensionalFocusSearch(direction, onFound)) return true
// If we don't find a match, we exit this Parent.
// First check if this node has a custom focus exit.
- @OptIn(ExperimentalComposeUiApi::class)
- focusedChild.focusProperties.exit(direction).performRequestFocus(onFound)
- ?.let { return it }
+ focusedChild
+ .fetchFocusProperties().exit(direction)
+ .findFocusTarget(onFound)?.let { return it }
// If we don't have a custom exit property,
// we search among the siblings of the parent.
@@ -77,7 +77,7 @@
// Search for the next eligible sibling.
Active, Captured ->
return generateAndSearchChildren(focusedChild, direction, onFound)
- Deactivated, Inactive -> error(NoActiveChild)
+ Inactive -> error(NoActiveChild)
}
}
Active, Captured -> {
@@ -97,16 +97,17 @@
* @param onFound the callback that is run when the child is found.
* @return true if we find a suitable child, false otherwise.
*/
-internal fun FocusModifier.findChildCorrespondingToFocusEnter(
+@ExperimentalComposeUiApi
+internal fun FocusTargetModifierNode.findChildCorrespondingToFocusEnter(
direction: FocusDirection,
- onFound: (FocusModifier) -> Boolean
+ onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
// Check if a custom FocusEnter is specified.
- @OptIn(ExperimentalComposeUiApi::class)
- focusProperties.enter(direction).performRequestFocus(onFound)?.let { return it }
+ fetchFocusProperties().enter(direction).findFocusTarget(onFound)?.let { return it }
- val focusableChildren = activatedChildren()
+ val focusableChildren = MutableVector<FocusTargetModifierNode>()
+ collectAccessibleChildren(focusableChildren)
// If there are aren't multiple children to choose from, return the first child.
if (focusableChildren.size <= 1) {
@@ -119,7 +120,7 @@
val requestedDirection = when (direction) {
// TODO(b/244528858) choose different items for moveFocus(Enter) based on LayoutDirection.
@OptIn(ExperimentalComposeUiApi::class)
- Enter -> Left
+ Enter -> Right
else -> direction
}
@@ -136,10 +137,11 @@
// Search among your children for the next child.
// If the next child is not found, generate more children by requesting a beyondBoundsLayout.
-private fun FocusModifier.generateAndSearchChildren(
- focusedItem: FocusModifier,
+@ExperimentalComposeUiApi
+private fun FocusTargetModifierNode.generateAndSearchChildren(
+ focusedItem: FocusTargetModifierNode,
direction: FocusDirection,
- onFound: (FocusModifier) -> Boolean
+ onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
// Search among the currently available children.
if (searchChildren(focusedItem, direction, onFound)) {
@@ -156,30 +158,33 @@
} ?: false
}
-private fun FocusModifier.searchChildren(
- focusedItem: FocusModifier,
+@ExperimentalComposeUiApi
+private fun FocusTargetModifierNode.searchChildren(
+ focusedItem: FocusTargetModifierNode,
direction: FocusDirection,
- onFound: (FocusModifier) -> Boolean
+ onFound: (FocusTargetModifierNode) -> Boolean
): Boolean {
- val childrenCopy = MutableVector<FocusModifier>(children.size)
- childrenCopy.addAll(children)
- while (childrenCopy.isNotEmpty()) {
- val nextItem = childrenCopy.findBestCandidate(focusedItem.focusRect(), direction)
+ val children = MutableVector<FocusTargetModifierNode>().apply {
+ visitChildren(Nodes.FocusTarget) {
+ this.add(it)
+ }
+ }
+ while (children.isNotEmpty()) {
+ val nextItem = children.findBestCandidate(focusedItem.focusRect(), direction)
?: return false
// If the result is not deactivated, this is a valid next item.
- if (!nextItem.focusState.isDeactivated) return onFound.invoke(nextItem)
+ if (nextItem.fetchFocusProperties().canFocus) return onFound.invoke(nextItem)
// If the result is deactivated, and the deactivated node has a custom Enter, we use it.
- @OptIn(ExperimentalComposeUiApi::class)
- nextItem.focusProperties.enter(direction).performRequestFocus(onFound)?.let { return it }
+ nextItem.fetchFocusProperties().enter(direction).findFocusTarget(onFound)?.let { return it }
- // If the result is deactivated, and there is no custom ehter, we search among its children.
+ // If the result is deactivated, and there is no custom enter, we search among its children.
if (nextItem.generateAndSearchChildren(focusedItem, direction, onFound)) return true
// If there are no results among the children of the deactivated node,
// repeat the search by excluding this deactivated node.
- childrenCopy.remove(nextItem)
+ children.remove(nextItem)
}
return false
}
@@ -188,11 +193,12 @@
// TODO(b/182319711): For Left/Right focus moves, Consider finding the first candidate in the beam
// and then only comparing candidates in the beam. If nothing is in the beam, then consider all
// valid candidates.
+@ExperimentalComposeUiApi
@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
-private fun MutableVector<FocusModifier>.findBestCandidate(
+private fun MutableVector<FocusTargetModifierNode>.findBestCandidate(
focusRect: Rect,
direction: FocusDirection
-): FocusModifier? {
+): FocusTargetModifierNode? {
// Pick an impossible rectangle as the initial best candidate Rect.
var bestCandidate = when (direction) {
Left -> focusRect.translate(focusRect.width + 1, 0f)
@@ -202,7 +208,7 @@
else -> error(InvalidFocusDirection)
}
- var searchResult: FocusModifier? = null
+ var searchResult: FocusTargetModifierNode? = null
forEach { candidateNode ->
if (candidateNode.isEligibleForFocusSearch) {
val candidateRect = candidateNode.focusRect()
@@ -363,8 +369,9 @@
private fun Rect.bottomRight() = Rect(right, bottom, right, bottom)
// Find the active descendant.
+@ExperimentalComposeUiApi
@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
-private fun FocusModifier.activeNode(): FocusModifier {
- check(focusState == ActiveParent || focusState == DeactivatedParent)
+private fun FocusTargetModifierNode.activeNode(): FocusTargetModifierNode {
+ check(focusState == ActiveParent)
return findActiveFocusNode() ?: error(NoActiveChild)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/focus/FocusAwareInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/focus/FocusAwareInputModifier.kt
deleted file mode 100644
index a559215..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/focus/FocusAwareInputModifier.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2021 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.input.focus
-
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ModifierLocalReadScope
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-
-internal interface FocusDirectedInputEvent
-
-/**
- * A modifier that routes [FocusDirectedInputEvent]s to the currently focused item.
- *
- * The event is routed to the focused item. Before reaching the focused item, [onPreEvent]() is
- * called for parents of the focused item. If the parents don't consume the event, [onPreEvent]()
- * is called for the focused item. If the event is still not consumed, [onEvent]() is called on the
- * focused item's parents.
- */
-internal open class FocusAwareInputModifier<T : FocusDirectedInputEvent>(
- val onEvent: ((FocusDirectedInputEvent) -> Boolean)?,
- val onPreEvent: ((FocusDirectedInputEvent) -> Boolean)?,
- override val key: ProvidableModifierLocal<FocusAwareInputModifier<T>?>
-) : ModifierLocalConsumer,
- ModifierLocalProvider<FocusAwareInputModifier<T>?> {
-
- // The focus-aware modifier that is a parent of this modifier.
- private var focusAwareEventParent: FocusAwareInputModifier<T>? = null
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
- focusAwareEventParent = with(scope) { key.current }
- }
- // Register this modifier as the FocusAwareParent for modifiers further down the hierarchy.
- override val value: FocusAwareInputModifier<T>
- get() = this
-
- fun propagateFocusAwareEvent(event: T) = propagatePreEvent(event) || propagateEvent(event)
-
- private fun propagatePreEvent(event: T): Boolean {
- // We first propagate the event to the parent.
- if (focusAwareEventParent?.propagatePreEvent(event) == true) return true
-
- // If none of the parents consume the event, we attempt to consume it.
- return onPreEvent?.invoke(event) ?: false
- }
-
- private fun propagateEvent(event: T): Boolean {
- // We attempt to consume the key event first.
- if (onEvent?.invoke(event) == true) return true
-
- // If the event is not consumed, we propagate it to the parent.
- return focusAwareEventParent?.propagateEvent(event) ?: false
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
index 542bf03..ef60df1 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
@@ -16,22 +16,47 @@
package androidx.compose.ui.input.key
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusModifier
-import androidx.compose.ui.focus.ModifierLocalParentFocusModifier
-import androidx.compose.ui.focus.findActiveFocusNode
-import androidx.compose.ui.focus.findLastKeyInputModifier
-import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.OnPlacedModifier
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ModifierLocalReadScope
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.modifier.modifierLocalOf
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.NodeCoordinator
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.platform.inspectable
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.modifierElementOf
+
+/**
+ * Implement this interface to create a [Modifier.Node] that can intercept hardware Key events.
+ *
+ * The event is routed to the focused item. Before reaching the focused item, [onPreKeyEvent]() is
+ * called for parents of the focused item. If the parents don't consume the event, [onPreKeyEvent]()
+ * is called for the focused item. If the event is still not consumed, [onKeyEvent]() is called on
+ * the focused item's parents.
+ */
+@ExperimentalComposeUiApi
+interface KeyInputModifierNode : DelegatableNode {
+
+ /**
+ * This function is called when a [KeyEvent] is received by this node during the upward
+ * pass. While implementing this callback, return true to stop propagation of this event. If you
+ * return false, the key event will be sent to this [KeyInputModifierNode]'s parent.
+ */
+ fun onKeyEvent(event: KeyEvent): Boolean
+
+ /**
+ * This function is called when a [KeyEvent] is received by this node during the
+ * downward pass. It gives ancestors of a focused component the chance to intercept an event.
+ * Return true to stop propagation of this event. If you return false, the event will be sent
+ * to this [KeyInputModifierNode]'s child. If none of the children consume the event,
+ * it will be sent back up to the root using the [onKeyEvent] function.
+ */
+ fun onPreKeyEvent(event: KeyEvent): Boolean
+}
+
+@ExperimentalComposeUiApi
+internal class KeyInputInputModifierNodeImpl(
+ var onEvent: ((KeyEvent) -> Boolean)?,
+ var onPreEvent: ((KeyEvent) -> Boolean)?
+) : KeyInputModifierNode, Modifier.Node() {
+ override fun onKeyEvent(event: KeyEvent): Boolean = this.onEvent?.invoke(event) ?: false
+ override fun onPreKeyEvent(event: KeyEvent): Boolean = this.onPreEvent?.invoke(event) ?: false
+}
/**
* Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -43,14 +68,19 @@
*
* @sample androidx.compose.ui.samples.KeyEventSample
*/
-fun Modifier.onKeyEvent(onKeyEvent: (KeyEvent) -> Boolean): Modifier = inspectable(
- inspectorInfo = debugInspectorInfo {
- name = "onKeyEvent"
- properties["onKeyEvent"] = onKeyEvent
- }
-) {
- KeyInputModifier( >
-}
+@OptIn(ExperimentalComposeUiApi::class)
+@Suppress("ModifierInspectorInfo") // b/251831790.
+fun Modifier.onKeyEvent(onKeyEvent: (KeyEvent) -> Boolean): Modifier = this.then(
+ modifierElementOf(
+ key = onKeyEvent,
+ create = { KeyInputInputModifierNodeImpl( },
+ update = { it. },
+ definitions = {
+ name = "onKeyEvent"
+ properties["onKeyEvent"] = onKeyEvent
+ }
+ )
+)
/**
* Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -60,75 +90,20 @@
* keyboard. It gives ancestors of a focused component the chance to intercept a [KeyEvent].
* Return true to stop propagation of this event. If you return false, the key event will be sent
* to this [onPreviewKeyEvent]'s child. If none of the children consume the event, it will be
- * sent back up to the root [KeyInputModifier] using the onKeyEvent callback.
+ * sent back up to the root [KeyInputModifierNode] using the onKeyEvent callback.
*
* @sample androidx.compose.ui.samples.KeyEventSample
*/
-fun Modifier.onPreviewKeyEvent(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier = inspectable(
- inspectorInfo = debugInspectorInfo {
- name = "onPreviewKeyEvent"
- properties["onPreviewKeyEvent"] = onPreviewKeyEvent
- }
-) {
- KeyInputModifier( >
-}
-
-/**
- * Used to build a tree of [KeyInputModifier]s. This contains the [KeyInputModifier] that is
- * higher in the layout tree.
- */
-internal val ModifierLocalKeyInput = modifierLocalOf<KeyInputModifier?> { null }
-
-internal class KeyInputModifier(
- val onKeyEvent: ((KeyEvent) -> Boolean)?,
- val onPreviewKeyEvent: ((KeyEvent) -> Boolean)?
-) : ModifierLocalConsumer, ModifierLocalProvider<KeyInputModifier?>, OnPlacedModifier {
- private var focusModifier: FocusModifier? = null
- var parent: KeyInputModifier? = null
- private set
- var layoutNode: LayoutNode? = null
- private set
-
- override val key: ProvidableModifierLocal<KeyInputModifier?>
- get() = ModifierLocalKeyInput
- override val value: KeyInputModifier
- get() = this
-
- fun processKeyInput(keyEvent: KeyEvent): Boolean {
- val activeKeyInputModifier = focusModifier
- ?.findActiveFocusNode()
- ?.findLastKeyInputModifier()
- ?: error("KeyEvent can't be processed because this key input node is not active.")
- val consumed = activeKeyInputModifier.propagatePreviewKeyEvent(keyEvent)
- return if (consumed) true else activeKeyInputModifier.propagateKeyEvent(keyEvent)
- }
-
- override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {
- focusModifier?.keyInputChildren?.remove(this@KeyInputModifier)
- focusModifier = ModifierLocalParentFocusModifier.current
- focusModifier?.keyInputChildren?.add(this@KeyInputModifier)
- parent = ModifierLocalKeyInput.current
- }
-
- fun propagatePreviewKeyEvent(keyEvent: KeyEvent): Boolean {
- // We first propagate the preview key event to the parent.
- val consumed = parent?.propagatePreviewKeyEvent(keyEvent)
- if (consumed == true) return consumed
-
- // If none of the parents consumed the event, we attempt to consume it.
- return onPreviewKeyEvent?.invoke(keyEvent) ?: false
- }
-
- fun propagateKeyEvent(keyEvent: KeyEvent): Boolean {
- // We attempt to consume the key event first.
- val consumed = onKeyEvent?.invoke(keyEvent)
- if (consumed == true) return consumed
-
- // If the event is not consumed, we propagate it to the parent.
- return parent?.propagateKeyEvent(keyEvent) ?: false
- }
-
- override fun onPlaced(coordinates: LayoutCoordinates) {
- layoutNode = (coordinates as NodeCoordinator).layoutNode
- }
-}
+@OptIn(ExperimentalComposeUiApi::class)
+@Suppress("ModifierInspectorInfo") // b/251831790.
+fun Modifier.onPreviewKeyEvent(onPreviewKeyEvent: (KeyEvent) -> Boolean): Modifier = this.then(
+ modifierElementOf(
+ key = onPreviewKeyEvent,
+ create = { KeyInputInputModifierNodeImpl( },
+ update = { it. },
+ definitions = {
+ name = "onPreviewKeyEvent"
+ properties["onPreviewKeyEvent"] = onPreviewKeyEvent
+ }
+ )
+)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
index a8a5189..c1d6b0e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
@@ -18,11 +18,48 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.focus.FocusDirectedInputEvent
-import androidx.compose.ui.input.focus.FocusAwareInputModifier
-import androidx.compose.ui.modifier.modifierLocalOf
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.platform.inspectable
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.modifierElementOf
+
+/**
+ * Implement this interface to create a [Modifier.Node] that can intercept rotary scroll events.
+ *
+ * The event is routed to the focused item. Before reaching the focused item,
+ * [onPreRotaryScrollEvent]() is called for parents of the focused item. If the parents don't
+ * consume the event, [onPreRotaryScrollEvent]() is called for the focused item. If the event is
+ * still not consumed, [onRotaryScrollEvent]() is called on the focused item's parents.
+ */
+@ExperimentalComposeUiApi
+interface RotaryInputModifierNode : DelegatableNode {
+ /**
+ * This function is called when a [RotaryScrollEvent] is received by this node during the upward
+ * pass. While implementing this callback, return true to stop propagation of this event. If you
+ * return false, the key event will be sent to this [RotaryInputModifierNode]'s parent.
+ */
+ fun onRotaryScrollEvent(event: RotaryScrollEvent): Boolean
+
+ /**
+ * This function is called when a [RotaryScrollEvent] is received by this node during the
+ * downward pass. It gives ancestors of a focused component the chance to intercept an event.
+ * Return true to stop propagation of this event. If you return false, the event will be sent
+ * to this [RotaryInputModifierNode]'s child. If none of the children consume the event,
+ * it will be sent back up to the root using the [onRotaryScrollEvent] function.
+ */
+ fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean
+}
+
+@ExperimentalComposeUiApi
+internal class RotaryInputModifierNodeImpl(
+ var onEvent: ((RotaryScrollEvent) -> Boolean)?,
+ var onPreEvent: ((RotaryScrollEvent) -> Boolean)?
+) : RotaryInputModifierNode, Modifier.Node() {
+ override fun onRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
+ return onEvent?.invoke(event) ?: false
+ }
+ override fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
+ return onPreEvent?.invoke(event) ?: false
+ }
+}
/**
* Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -43,21 +80,21 @@
* access to a [RotaryScrollEvent] when a child does not consume it:
* @sample androidx.compose.ui.samples.PreRotaryEventSample
*/
+@Suppress("ModifierInspectorInfo") // b/251831790.
@ExperimentalComposeUiApi
fun Modifier.onRotaryScrollEvent(
onRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
-): Modifier = inspectable(
- inspectorInfo = debugInspectorInfo {
- name = "onRotaryScrollEvent"
- properties["onRotaryScrollEvent"] = onRotaryScrollEvent
- }
-) {
- FocusAwareInputModifier(
- >
- >
- key = ModifierLocalRotaryScrollParent
+): Modifier = this.then(
+ modifierElementOf(
+ key = onRotaryScrollEvent,
+ create = { RotaryInputModifierNodeImpl( },
+ update = { it. },
+ definitions = {
+ name = "onRotaryScrollEvent"
+ properties["onRotaryScrollEvent"] = onRotaryScrollEvent
+ }
)
-}
+)
/**
* Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -80,30 +117,20 @@
*
* @sample androidx.compose.ui.samples.PreRotaryEventSample
*/
+@Suppress("ModifierInspectorInfo") // b/251831790.
@ExperimentalComposeUiApi
fun Modifier.onPreRotaryScrollEvent(
onPreRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
-): Modifier = inspectable(
- inspectorInfo = debugInspectorInfo {
- name = "onPreRotaryScrollEvent"
- properties["onPreRotaryScrollEvent"] = onPreRotaryScrollEvent
- }
-) {
- FocusAwareInputModifier(
- >
- >
- key = ModifierLocalRotaryScrollParent
+): Modifier = this.then(
+ modifierElementOf(
+ key = onPreRotaryScrollEvent,
+ create = {
+ RotaryInputModifierNodeImpl( >
+ },
+ update = { it. },
+ definitions = {
+ name = "onPreRotaryScrollEvent"
+ properties["onPreRotaryScrollEvent"] = onPreRotaryScrollEvent
+ }
)
-}
-
-@ExperimentalComposeUiApi
-internal val ModifierLocalRotaryScrollParent =
- modifierLocalOf<FocusAwareInputModifier<RotaryScrollEvent>?> { null }
-
-@ExperimentalComposeUiApi
-private fun ((RotaryScrollEvent) -> Boolean).focusAwareCallback() = { e: FocusDirectedInputEvent ->
- check(e is RotaryScrollEvent) {
- "FocusAwareEvent is dispatched to the wrong FocusAwareParent."
- }
- invoke(e)
-}
+)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.kt
index e79fdd4..732b277 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryScrollEvent.kt
@@ -17,7 +17,6 @@
package androidx.compose.ui.input.rotary
import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.input.focus.FocusDirectedInputEvent
/**
* This event represents a rotary input event.
@@ -44,14 +43,14 @@
* platform-dependent.
*/
val uptimeMillis: Long
-) : FocusDirectedInputEvent {
+) {
override fun equals(other: Any?): Boolean = other is RotaryScrollEvent &&
other.verticalScrollPixels == verticalScrollPixels &&
other.horizontalScrollPixels == horizontalScrollPixels &&
other.uptimeMillis == uptimeMillis
override fun hashCode(): Int = 0
- .let { 31 * it + verticalScrollPixels.hashCode() }
+ .let { verticalScrollPixels.hashCode() }
.let { 31 * it + horizontalScrollPixels.hashCode() }
.let { 31 * it + uptimeMillis.hashCode() }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
index 6fb14bb..49c7871 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
@@ -23,9 +23,15 @@
import androidx.compose.ui.draw.BuildDrawCacheParams
import androidx.compose.ui.draw.DrawCacheModifier
import androidx.compose.ui.draw.DrawModifier
+import androidx.compose.ui.focus.FocusEventModifier
+import androidx.compose.ui.focus.FocusEventModifierNode
import androidx.compose.ui.focus.FocusOrderModifier
import androidx.compose.ui.focus.FocusOrderModifierToProperties
-import androidx.compose.ui.focus.FocusPropertiesModifier
+import androidx.compose.ui.focus.FocusProperties
+import androidx.compose.ui.focus.FocusPropertiesModifierNode
+import androidx.compose.ui.focus.FocusRequesterModifier
+import androidx.compose.ui.focus.FocusRequesterModifierNode
+import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.input.pointer.PointerEvent
@@ -54,7 +60,6 @@
import androidx.compose.ui.modifier.ModifierLocalProvider
import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.modifier.modifierLocalMapOf
-import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsModifier
import androidx.compose.ui.unit.Constraints
@@ -83,6 +88,9 @@
ParentDataModifierNode,
LayoutAwareModifierNode,
GlobalPositionAwareModifierNode,
+ FocusEventModifierNode,
+ FocusPropertiesModifierNode,
+ FocusRequesterModifierNode,
OwnerScope,
BuildDrawCacheParams,
Modifier.Node() {
@@ -92,7 +100,7 @@
var element: Modifier.Element = element
set(value) {
- if (isAttached) uninitializeModifier()
+ if (isAttached) unInitializeModifier()
field = value
kindSet = calculateNodeKindSetFrom(value)
if (isAttached) initializeModifier(false)
@@ -103,10 +111,10 @@
}
override fun onDetach() {
- uninitializeModifier()
+ unInitializeModifier()
}
- private fun uninitializeModifier() {
+ private fun unInitializeModifier() {
check(isAttached)
val element = element
if (isKind(Nodes.Locals)) {
@@ -118,18 +126,13 @@
if (element is ModifierLocalConsumer) {
element.onModifierLocalsUpdated(DetachedModifierLocalReadScope)
}
- if (element is FocusOrderModifier) {
- val focusOrderElement = focusOrderElement
- if (focusOrderElement != null) {
- requireOwner()
- .modifierLocalManager
- .removedProvider(this, focusOrderElement.key)
- }
- }
}
if (isKind(Nodes.Semantics)) {
requireOwner().onSemanticsChange()
}
+ if (element is FocusRequesterModifier) {
+ element.focusRequester.focusRequesterNodes -= this
+ }
}
private fun initializeModifier(duringAttach: Boolean) {
@@ -145,24 +148,6 @@
else
sideEffect { updateModifierLocalConsumer() }
}
- // Special handling for FocusOrderModifier -- we have to use modifier local
- // consumers and providers for it.
- if (element is FocusOrderModifier) {
- // Have to create a new consumer/provider
- val scope = FocusOrderModifierToProperties(element)
- focusOrderElement = FocusPropertiesModifier(
- focusPropertiesScope = scope,
- inspectorInfo = debugInspectorInfo {
- name = "focusProperties"
- properties["scope"] = scope
- }
- )
- updateModifierLocalProvider(focusOrderElement!!)
- if (duringAttach)
- updateFocusOrderModifierLocalConsumer()
- else
- sideEffect { updateFocusOrderModifierLocalConsumer() }
- }
}
if (isKind(Nodes.Draw)) {
if (element is DrawCacheModifier) {
@@ -219,6 +204,9 @@
}
}
}
+ if (element is FocusRequesterModifier) {
+ element.focusRequester.focusRequesterNodes += this
+ }
if (isKind(Nodes.PointerInput)) {
if (element is PointerInputModifier) {
element.pointerInputFilter.layoutCoordinates = coordinator
@@ -262,7 +250,6 @@
invalidateDraw()
}
- private var focusOrderElement: FocusPropertiesModifier? = null
private var _providedValues: BackwardsCompatLocalMap? = null
var readValues = hashSetOf<ModifierLocal<*>>()
override val providedValues: ModifierLocalMap get() = _providedValues ?: modifierLocalMapOf()
@@ -292,18 +279,7 @@
}
}
- fun updateFocusOrderModifierLocalConsumer() {
- if (isAttached) {
- requireOwner().snapshotObserver.observeReads(
- this,
- updateFocusOrderModifierLocalConsumer
- ) {
- (focusOrderElement!! as ModifierLocalConsumer).onModifierLocalsUpdated(this)
- }
- }
- }
-
- fun updateModifierLocalProvider(element: ModifierLocalProvider<*>) {
+ private fun updateModifierLocalProvider(element: ModifierLocalProvider<*>) {
val providedValues = _providedValues
if (providedValues != null && providedValues.contains(element.key)) {
providedValues.element = element
@@ -314,7 +290,7 @@
_providedValues = BackwardsCompatLocalMap(element)
// we only need to notify the modifierLocalManager of an inserted provider
// in the cases where a provider was added to the chain where it was possible
- // that consumers below it could need to be invalidated. If this layoutnode
+ // that consumers below it could need to be invalidated. If this layout node
// is just now being created, then that is impossible. In this case, we can just
// do nothing and wait for the child consumers to read us. We infer this by
// checking to see if the tail node is attached or not. If it is not, then the node
@@ -448,6 +424,18 @@
}
}
+ override fun onFocusEvent(focusState: FocusState) {
+ val focusEventModifier = element
+ check(focusEventModifier is FocusEventModifier)
+ focusEventModifier.onFocusEvent(focusState)
+ }
+
+ override fun modifyFocusProperties(focusProperties: FocusProperties) {
+ val focusOrderModifier = element
+ check(focusOrderModifier is FocusOrderModifier)
+ focusProperties.apply(FocusOrderModifierToProperties(focusOrderModifier))
+ }
+
override fun toString(): String = element.toString()
}
@@ -463,7 +451,3 @@
private val updateModifierLocalConsumer = { it: BackwardsCompatNode ->
it.updateModifierLocalConsumer()
}
-
-private val updateFocusOrderModifierLocalConsumer = { it: BackwardsCompatNode ->
- it.updateFocusOrderModifierLocalConsumer()
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 2b4b7ac..ea2231a7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -94,6 +94,29 @@
}
@ExperimentalComposeUiApi
+internal fun DelegatableNode.ancestors(mask: Int): List<Modifier.Node>? {
+ check(node.isAttached)
+ var ancestors: MutableList<Modifier.Node>? = null
+ var node: Modifier.Node? = node.parent
+ var layout: LayoutNode? = requireLayoutNode()
+ while (layout != null) {
+ val head = layout.nodes.head
+ if (head.aggregateChildKindSet and mask != 0) {
+ while (node != null) {
+ if (node.kindSet and mask != 0) {
+ if (ancestors == null) ancestors = mutableListOf()
+ ancestors += node
+ }
+ node = node.parent
+ }
+ }
+ layout = layout.parent
+ node = layout?.nodes?.tail
+ }
+ return ancestors
+}
+
+@ExperimentalComposeUiApi
internal fun DelegatableNode.nearestAncestor(mask: Int): Modifier.Node? {
check(node.isAttached)
var node: Modifier.Node? = node.parent
@@ -115,6 +138,33 @@
}
@ExperimentalComposeUiApi
+internal fun DelegatableNode.firstChild(mask: Int): Modifier.Node? {
+ check(node.isAttached)
+ val branches = mutableVectorOf<Modifier.Node>()
+ val child = node.child
+ if (child == null)
+ branches.addLayoutNodeChildren(node)
+ else
+ branches.add(child)
+ while (branches.isNotEmpty()) {
+ val branch = branches.removeAt(branches.lastIndex)
+ if (branch.aggregateChildKindSet and mask == 0) {
+ branches.addLayoutNodeChildren(branch)
+ // none of these nodes match the mask, so don't bother traversing them
+ continue
+ }
+ var node: Modifier.Node? = branch
+ while (node != null) {
+ if (node.kindSet and mask != 0) {
+ return node
+ }
+ node = node.child
+ }
+ }
+ return null
+}
+
+@ExperimentalComposeUiApi
internal inline fun DelegatableNode.visitSubtree(mask: Int, block: (Modifier.Node) -> Unit) {
// TODO(lmr): we might want to add some safety wheels to prevent this from being called
// while one of the chains is being diffed / updated.
@@ -263,11 +313,21 @@
block: (T) -> Unit
) = visitAncestors(type.mask) { if (it is T) block(it) }
+@Suppress("UNCHECKED_CAST") // Type info lost due to erasure.
+@ExperimentalComposeUiApi
+internal inline fun <reified T> DelegatableNode.ancestors(
+ type: NodeKind<T>
+): List<T>? = ancestors(type.mask) as? List<T>
+
@ExperimentalComposeUiApi
internal inline fun <reified T : Any> DelegatableNode.nearestAncestor(type: NodeKind<T>): T? =
nearestAncestor(type.mask) as? T
@ExperimentalComposeUiApi
+internal inline fun <reified T : Any> DelegatableNode.firstChild(type: NodeKind<T>): T? =
+ firstChild(type.mask) as? T
+
+@ExperimentalComposeUiApi
internal inline fun <reified T> DelegatableNode.visitSubtree(
type: NodeKind<T>,
block: (T) -> Unit
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 a2f0c22..0674ea8 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
@@ -19,6 +19,7 @@
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusTargetModifierNode
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.input.pointer.PointerInputFilter
@@ -41,6 +42,9 @@
import androidx.compose.ui.node.LayoutNode.LayoutState.Measuring
import androidx.compose.ui.node.LayoutNode.LayoutState.LookaheadLayingOut
import androidx.compose.ui.node.LayoutNode.LayoutState.LookaheadMeasuring
+import androidx.compose.ui.node.Nodes.FocusEvent
+import androidx.compose.ui.node.Nodes.FocusProperties
+import androidx.compose.ui.node.Nodes.FocusTarget
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.simpleIdentityToString
import androidx.compose.ui.semantics.SemanticsModifierCore.Companion.generateSemanticsId
@@ -398,6 +402,8 @@
forEachCoordinatorIncludingInner { it.attach() }
onAttach?.invoke(owner)
+
+ invalidateFocusOnAttach()
}
/**
@@ -410,6 +416,7 @@
checkNotNull(owner) {
"Cannot detach node that is already detached! Tree: " + parent?.debugTreeToString()
}
+ invalidateFocusOnDetach()
val parent = this.parent
if (parent != null) {
parent.invalidateLayer()
@@ -1044,6 +1051,33 @@
}
}
+ @OptIn(ExperimentalComposeUiApi::class)
+ private fun invalidateFocusOnAttach() {
+ if (nodes.has(FocusTarget or FocusProperties or FocusEvent)) {
+ nodes.headToTail {
+ if (it.isKind(FocusTarget) or it.isKind(FocusProperties) or it.isKind(FocusEvent)) {
+ autoInvalidateInsertedNode(it)
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ private fun invalidateFocusOnDetach() {
+ if (nodes.has(FocusTarget)) {
+ nodes.tailToHead {
+ if (
+ it.isKind(FocusTarget) &&
+ it is FocusTargetModifierNode &&
+ it.focusState.isFocused
+ ) {
+ requireOwner().focusOwner.clearFocus(force = true, refreshFocusEvents = false)
+ it.scheduleInvalidationForFocusEvents()
+ }
+ }
+ }
+ }
+
internal inline fun ignoreRemeasureRequests(block: () -> Unit) {
ignoreRemeasureRequests = true
block()
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 c56b027..f3603db 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
@@ -468,12 +468,11 @@
element: Modifier.Element,
child: Modifier.Node,
): Modifier.Node {
- val node = if (element is ModifierNodeElement<*>) {
- element.create().also {
+ val node = when (element) {
+ is ModifierNodeElement<*> -> element.create().also {
it.kindSet = calculateNodeKindSetFrom(it)
}
- } else {
- BackwardsCompatNode(element)
+ else -> BackwardsCompatNode(element)
}
return insertParent(node, child)
}
@@ -506,29 +505,34 @@
next: Modifier.Element,
node: Modifier.Node,
): Modifier.Node {
- if (prev !is ModifierNodeElement<*> || next !is ModifierNodeElement<*>) {
- check(node is BackwardsCompatNode)
- node.element = next
- // we always autoInvalidate BackwardsCompatNode
- autoInvalidateUpdatedNode(node)
- return node
- }
- val updated = next.updateUnsafe(node)
- if (updated !== node) {
- // if a new instance is returned, we want to detach the old one
- autoInvalidateRemovedNode(node)
- node.detach()
- val result = replaceNode(node, updated)
- autoInvalidateInsertedNode(updated)
- return result
- } else {
- // the node was updated. we are done.
- if (next.autoInvalidate) {
- // the modifier element is labeled as "auto invalidate", which means that since the
- // node was updated, we need to invalidate everything relevant to it
- autoInvalidateUpdatedNode(updated)
+ when {
+ prev is ModifierNodeElement<*> && next is ModifierNodeElement<*> -> {
+ val updated = next.updateUnsafe(node)
+ if (updated !== node) {
+ // if a new instance is returned, we want to detach the old one
+ autoInvalidateRemovedNode(node)
+ node.detach()
+ val result = replaceNode(node, updated)
+ autoInvalidateInsertedNode(updated)
+ return result
+ } else {
+ // the node was updated. we are done.
+ if (next.autoInvalidate) {
+ // the modifier element is labeled as "auto invalidate", which means
+ // that since the node was updated, we need to invalidate everything
+ // relevant to it.
+ autoInvalidateUpdatedNode(updated)
+ }
+ return updated
+ }
}
- return updated
+ node is BackwardsCompatNode -> {
+ node.element = next
+ // We always autoInvalidate BackwardsCompatNode.
+ autoInvalidateUpdatedNode(node)
+ return node
+ }
+ else -> error("Unknown Modifier.Node type")
}
}
@@ -622,6 +626,8 @@
internal fun has(type: NodeKind<*>): Boolean = aggregateChildKindSet and type.mask != 0
+ internal fun has(mask: Int): Boolean = aggregateChildKindSet and mask != 0
+
override fun toString(): String = buildString {
append("[")
if (head === tail) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index 4b257cf..d0bb5e4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -21,8 +21,16 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.DrawModifier
+import androidx.compose.ui.focus.FocusEventModifier
+import androidx.compose.ui.focus.FocusEventModifierNode
import androidx.compose.ui.focus.FocusOrderModifier
+import androidx.compose.ui.focus.FocusProperties
+import androidx.compose.ui.focus.FocusPropertiesModifierNode
+import androidx.compose.ui.focus.FocusTargetModifierNode
+import androidx.compose.ui.focus.scheduleInvalidationOfAssociatedFocusTargets
+import androidx.compose.ui.input.key.KeyInputModifierNode
import androidx.compose.ui.input.pointer.PointerInputModifier
+import androidx.compose.ui.input.rotary.RotaryInputModifierNode
import androidx.compose.ui.layout.IntermediateLayoutModifier
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.LookaheadOnPlacedModifier
@@ -40,6 +48,7 @@
inline infix fun or(other: NodeKind<*>): Int = mask or other.mask
inline infix fun or(other: Int): Int = mask or other
}
+
internal inline infix fun Int.or(other: NodeKind<*>): Int = this or other.mask
// For a given NodeCoordinator, the "LayoutAware" nodes that it is concerned with should include
@@ -47,9 +56,8 @@
// implements any other node interfaces, such as draw, those should be visited by the coordinator
// below them.
@OptIn(ExperimentalComposeUiApi::class)
-internal val NodeKind<*>.includeSelfInTraversal: Boolean get() {
- return mask and Nodes.LayoutAware.mask != 0
-}
+internal val NodeKind<*>.includeSelfInTraversal: Boolean
+ get() = mask and Nodes.LayoutAware.mask != 0
// Note that these don't inherit from Modifier.Node to allow for a single Modifier.Node
// instance to implement multiple Node interfaces
@@ -76,6 +84,16 @@
inline val GlobalPositionAware get() = NodeKind<GlobalPositionAwareModifierNode>(0b1 shl 8)
@JvmStatic
inline val IntermediateMeasure get() = NodeKind<IntermediateLayoutModifierNode>(0b1 shl 9)
+ @JvmStatic
+ inline val FocusTarget get() = NodeKind<FocusTargetModifierNode>(0b1 shl 10)
+ @JvmStatic
+ inline val FocusProperties get() = NodeKind<FocusPropertiesModifierNode>(0b1 shl 11)
+ @JvmStatic
+ inline val FocusEvent get() = NodeKind<FocusEventModifierNode>(0b1 shl 12)
+ @JvmStatic
+ inline val KeyInput get() = NodeKind<KeyInputModifierNode>(0b1 shl 13)
+ @JvmStatic
+ inline val RotaryInput get() = NodeKind<RotaryInputModifierNode>(0b1 shl 14)
// ...
}
@@ -85,7 +103,6 @@
if (element is LayoutModifier) {
mask = mask or Nodes.Layout
}
- @OptIn(ExperimentalComposeUiApi::class)
if (element is IntermediateLayoutModifier) {
mask = mask or Nodes.IntermediateMeasure
}
@@ -100,13 +117,16 @@
}
if (
element is ModifierLocalConsumer ||
- element is ModifierLocalProvider<*> ||
- // Special handling for FocusOrderModifier -- we have to use modifier local
- // consumers and providers for it.
- element is FocusOrderModifier
+ element is ModifierLocalProvider<*>
) {
mask = mask or Nodes.Locals
}
+ if (element is FocusEventModifier) {
+ mask = mask or Nodes.FocusEvent
+ }
+ if (element is FocusOrderModifier) {
+ mask = mask or Nodes.FocusProperties
+ }
if (element is OnGloballyPositionedModifier) {
mask = mask or Nodes.GlobalPositionAware
}
@@ -153,6 +173,21 @@
if (node is IntermediateLayoutModifierNode) {
mask = mask or Nodes.IntermediateMeasure
}
+ if (node is FocusTargetModifierNode) {
+ mask = mask or Nodes.FocusTarget
+ }
+ if (node is FocusPropertiesModifierNode) {
+ mask = mask or Nodes.FocusProperties
+ }
+ if (node is FocusEventModifierNode) {
+ mask = mask or Nodes.FocusEvent
+ }
+ if (node is KeyInputModifierNode) {
+ mask = mask or Nodes.KeyInput
+ }
+ if (node is RotaryInputModifierNode) {
+ mask = mask or Nodes.RotaryInput
+ }
return mask
}
@@ -168,6 +203,7 @@
@OptIn(ExperimentalComposeUiApi::class)
internal fun autoInvalidateUpdatedNode(node: Modifier.Node) = autoInvalidateNode(node, Updated)
+
@OptIn(ExperimentalComposeUiApi::class)
private fun autoInvalidateNode(node: Modifier.Node, phase: Int) {
if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
@@ -189,4 +225,48 @@
if (node.isKind(Nodes.ParentData) && node is ParentDataModifierNode) {
node.invalidateParentData()
}
+ if (node.isKind(Nodes.FocusTarget) && node is FocusTargetModifierNode) {
+ when (phase) {
+ Removed -> node.onRemoved()
+ else -> node.requireOwner().focusOwner.scheduleInvalidation(node)
+ }
+ }
+ if (
+ node.isKind(Nodes.FocusProperties) &&
+ node is FocusPropertiesModifierNode &&
+ node.specifiesCanFocusProperty()
+ ) {
+ when (phase) {
+ Removed -> node.scheduleInvalidationOfAssociatedFocusTargets()
+ else -> node.requireOwner().focusOwner.scheduleInvalidation(node)
+ }
+ }
+ if (node.isKind(Nodes.FocusEvent) && node is FocusEventModifierNode && phase != Removed) {
+ node.requireOwner().focusOwner.scheduleInvalidation(node)
+ }
+}
+
+/**
+ * This function checks if the FocusProperties node has set the canFocus [FocusProperties.canFocus]
+ * property.
+ *
+ * We use a singleton CanFocusChecker to prevent extra allocations, and in doing so, we assume that
+ * there won't be multiple concurrent calls of this function. This is not an issue since this is
+ * called from the main thread, but if this changes in the future, replace the
+ * [CanFocusChecker.reset] call with a new [FocusProperties] object for every invocation.
+ */
+@ExperimentalComposeUiApi
+private fun FocusPropertiesModifierNode.specifiesCanFocusProperty(): Boolean {
+ CanFocusChecker.reset()
+ modifyFocusProperties(CanFocusChecker)
+ return CanFocusChecker.isCanFocusSet()
+}
+
+private object CanFocusChecker : FocusProperties {
+ private var canFocusValue: Boolean? = null
+ override var canFocus: Boolean
+ get() = checkNotNull(canFocusValue)
+ set(value) { canFocusValue = value }
+ fun isCanFocusSet(): Boolean = canFocusValue != null
+ fun reset() { canFocusValue = null }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 4b1abd0..9a7c79c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -20,7 +20,7 @@
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusOwner
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.hapticfeedback.HapticFeedback
@@ -112,9 +112,9 @@
val pointerIconService: PointerIconService
/**
- * Provide a focus manager that controls focus within Compose.
+ * Provide a focus owner that controls focus within Compose.
*/
- val focusManager: FocusManager
+ val focusOwner: FocusOwner
/**
* Provide information about the window that hosts this [Owner].
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
index 3fd7321..158a357 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt
@@ -180,7 +180,7 @@
LocalAutofillTree provides owner.autofillTree,
LocalClipboardManager provides owner.clipboardManager,
LocalDensity provides owner.density,
- LocalFocusManager provides owner.focusManager,
+ LocalFocusManager provides owner.focusOwner,
@Suppress("DEPRECATION") LocalFontLoader
providesDefault @Suppress("DEPRECATION") owner.fontLoader,
LocalFontFamilyResolver providesDefault owner.fontFamilyResolver,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
index 15586dd5..437c083 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
@@ -16,7 +16,7 @@
package androidx.compose.ui.platform
-import androidx.compose.ui.input.key.KeyInputModifier
+import androidx.compose.ui.focus.FocusOwner
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.type
@@ -26,7 +26,7 @@
internal actual fun sendKeyEvent(
platformInputService: PlatformInput,
- keyInputModifier: KeyInputModifier,
+ focusOwner: FocusOwner,
keyEvent: KeyEvent
): Boolean {
when {
@@ -35,8 +35,7 @@
keyEvent.type == KeyEventType.KeyUp ->
platformInputService.charKeyPressed = false
}
-
- return keyInputModifier.processKeyInput(keyEvent)
+ return focusOwner.dispatchKeyEvent(keyEvent)
}
private val defaultCursor = Cursor(Cursor.DEFAULT_CURSOR)
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
index 373368b..8114dfc 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
@@ -25,6 +25,7 @@
import androidx.compose.ui.DefaultPointerButtons
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
+import androidx.compose.ui.Modifier
import androidx.compose.ui.PrimaryPressedPointerButtons
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
@@ -33,8 +34,8 @@
import androidx.compose.ui.focus.FocusDirection.Companion.Next
import androidx.compose.ui.focus.FocusDirection.Companion.Out
import androidx.compose.ui.focus.FocusDirection.Companion.Previous
-import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.focus.FocusManagerImpl
+import androidx.compose.ui.focus.FocusOwner
+import androidx.compose.ui.focus.FocusOwnerImpl
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.asComposeCanvas
@@ -46,9 +47,10 @@
import androidx.compose.ui.input.key.Key.Companion.Tab
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
-import androidx.compose.ui.input.key.KeyInputModifier
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerIcon
@@ -120,12 +122,12 @@
properties = {}
)
- private val _focusManager: FocusManagerImpl = FocusManagerImpl().apply {
+ override val focusOwner: FocusOwner = FocusOwnerImpl {
+ registerOnEndApplyChangesListener(it)
+ }.apply {
// TODO(demin): support RTL [onRtlPropertiesChanged]
layoutDirection = LayoutDirection.Ltr
}
- override val focusManager: FocusManager
- get() = _focusManager
// TODO: Set the input mode. For now we don't support touch mode, (always in Key mode).
private val _inputModeManager = InputModeManagerImpl(
@@ -148,16 +150,13 @@
// TODO(b/177931787) : Consider creating a KeyInputManager like we have for FocusManager so
// that this common logic can be used by all owners.
- private val keyInputModifier: KeyInputModifier = KeyInputModifier(
- >
- val focusDirection = getFocusDirection(it)
- if (focusDirection == null || it.type != KeyDown) return@KeyInputModifier false
+ private val keyInputModifier = Modifier.onKeyEvent {
+ val focusDirection = getFocusDirection(it)
+ if (focusDirection == null || it.type != KeyDown) return@onKeyEvent false
- // Consume the key event if we moved focus.
- focusManager.moveFocus(focusDirection)
- },
- >
- )
+ // Consume the key event if we moved focus.
+ focusOwner.moveFocus(focusDirection)
+ }
@Suppress("unused") // to be used in JB fork (not all prerequisite changes added yet)
internal fun setCurrentKeyboardModifiers(modifiers: PointerKeyboardModifiers) {
@@ -179,14 +178,10 @@
override val root = LayoutNode().also {
it.measurePolicy = RootMeasurePolicy
it.modifier = semanticsModifier
- .then(_focusManager.modifier)
+ .then(focusOwner.modifier)
.then(keyInputModifier)
- .then(
- KeyInputModifier(
- >
- >
- )
- )
+ .onPreviewKeyEvent(onPreviewKeyEvent)
+ .onKeyEvent(onKeyEvent)
}
override val rootForTest = this
@@ -202,7 +197,7 @@
init {
snapshotObserver.startObserving()
root.attach(this)
- _focusManager.takeFocus()
+ focusOwner.takeFocus()
}
fun dispose() {
@@ -237,7 +232,7 @@
override val viewConfiguration: ViewConfiguration = DefaultViewConfiguration(density)
override fun sendKeyEvent(keyEvent: KeyEvent): Boolean =
- sendKeyEvent(platformInputService, keyInputModifier, keyEvent)
+ sendKeyEvent(platformInputService, focusOwner, keyEvent)
override var showLayoutBounds = false
@@ -507,7 +502,7 @@
internal expect fun sendKeyEvent(
platformInputService: PlatformInput,
- keyInputModifier: KeyInputModifier,
+ focusOwner: FocusOwner,
keyEvent: KeyEvent
): Boolean
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
deleted file mode 100644
index 88e33de..0000000
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/focus/FocusManagerTest.kt
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright 2020 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.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Deactivated
-import androidx.compose.ui.focus.FocusStateImpl.DeactivatedParent
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
-import androidx.compose.ui.node.InnerNodeCoordinator
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.add
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@RunWith(Parameterized::class)
-class FocusManagerTest(private val initialFocusState: FocusState) {
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "rootInitialFocus = {0}")
- fun initParameters(): List<FocusState> = FocusStateImpl.values().asList()
- }
-
- private val focusModifier = FocusModifier(Inactive)
- private val focusManager = FocusManagerImpl(focusModifier)
-
- @Before
- fun setup() {
- val innerPlaceable = InnerNodeCoordinator(LayoutNode())
- focusModifier.coordinator = innerPlaceable
- }
-
- @Test
- fun defaultFocusState() {
- assertThat(focusModifier.focusState).isEqualTo(Inactive)
- }
-
- @Test
- fun takeFocus_onlyInactiveChangesState() {
- // Arrange.
- focusModifier.focusState = initialFocusState as FocusStateImpl
-
- // Act.
- focusManager.takeFocus()
-
- // Assert.
- assertThat(focusModifier.focusState).isEqualTo(
- when (initialFocusState) {
- Inactive -> Active
- Active, ActiveParent, Captured, Deactivated, DeactivatedParent -> initialFocusState
- }
- )
- }
-
- @Test
- fun releaseFocus_changesStateToInactive() {
- // Arrange.
- focusModifier.focusState = initialFocusState as FocusStateImpl
- if (initialFocusState == ActiveParent || initialFocusState == DeactivatedParent) {
- val childLayoutNode = LayoutNode()
- val child = FocusModifier(Active).apply {
- coordinator = InnerNodeCoordinator(childLayoutNode)
- }
- focusModifier.coordinator!!.layoutNode.add(childLayoutNode)
- focusModifier.focusedChild = child
- }
-
- // Act.
- focusManager.releaseFocus()
-
- // Assert.
- assertThat(focusModifier.focusState).isEqualTo(
- when (initialFocusState) {
- Active, ActiveParent, Captured, Inactive -> Inactive
- Deactivated, DeactivatedParent -> Deactivated
- }
- )
- }
-
- @Test
- fun clearFocus_forced() {
- // Arrange.
- focusModifier.focusState = initialFocusState as FocusStateImpl
- if (initialFocusState == ActiveParent || initialFocusState == DeactivatedParent) {
- val childLayoutNode = LayoutNode()
- val child = FocusModifier(Active).apply {
- coordinator = InnerNodeCoordinator(childLayoutNode)
- }
- focusModifier.coordinator!!.layoutNode.add(childLayoutNode)
- focusModifier.focusedChild = child
- }
-
- // Act.
- focusManager.clearFocus(force = true)
-
- // Assert.
- assertThat(focusModifier.focusState).isEqualTo(
- when (initialFocusState) {
- // If the initial state was focused, assert that after clearing the hierarchy,
- // the root is set to Active.
- Active, ActiveParent, Captured -> Active
- Deactivated, DeactivatedParent -> Deactivated
- Inactive -> Inactive
- }
- )
- }
-
- @Test
- fun clearFocus_notForced() {
- // Arrange.
- focusModifier.focusState = initialFocusState as FocusStateImpl
- if (initialFocusState == ActiveParent || initialFocusState == DeactivatedParent) {
- val childLayoutNode = LayoutNode()
- val child = FocusModifier(Active).apply {
- coordinator = InnerNodeCoordinator(childLayoutNode)
- }
- focusModifier.coordinator!!.layoutNode.add(childLayoutNode)
- focusModifier.focusedChild = child
- }
-
- // Act.
- focusManager.clearFocus(force = false)
-
- // Assert.
- assertThat(focusModifier.focusState).isEqualTo(
- when (initialFocusState) {
- // If the initial state was focused, assert that after clearing the hierarchy,
- // the root is set to Active.
- Active, ActiveParent -> Active
- Deactivated, DeactivatedParent -> Deactivated
- Captured -> Captured
- Inactive -> Inactive
- }
- )
- }
-
- @Test
- fun clearFocus_childIsCaptured() {
- if (initialFocusState == ActiveParent || initialFocusState == DeactivatedParent) {
- // Arrange.
- focusModifier.focusState = initialFocusState as FocusStateImpl
- val childLayoutNode = LayoutNode()
- val child = FocusModifier(Captured).apply {
- coordinator = InnerNodeCoordinator(childLayoutNode)
- }
- focusModifier.coordinator!!.layoutNode.add(childLayoutNode)
- focusModifier.focusedChild = child
-
- // Act.
- focusManager.clearFocus()
-
- // Assert.
- assertThat(focusModifier.focusState).isEqualTo(initialFocusState)
- }
- }
-}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 0d2d1c1..c957ee8 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -23,7 +23,7 @@
import androidx.compose.ui.draw.DrawModifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusOwner
import androidx.compose.ui.geometry.MutableRect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
@@ -2504,7 +2504,7 @@
get() = TODO("Not yet implemented")
override val pointerIconService: PointerIconService
get() = TODO("Not yet implemented")
- override val focusManager: FocusManager
+ override val focusOwner: FocusOwner
get() = TODO("Not yet implemented")
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index 0358c58..84732ff 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -25,7 +25,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.focus.FocusOwner
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.hapticfeedback.HapticFeedback
@@ -340,7 +340,7 @@
get() = TODO("Not yet implemented")
override val pointerIconService: PointerIconService
get() = TODO("Not yet implemented")
- override val focusManager: FocusManager
+ override val focusOwner: FocusOwner
get() = TODO("Not yet implemented")
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")