Merge "Revert "Fix several focus issues"" into androidx-main
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 9631cd1..a12b66a 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -135,12 +135,10 @@
property public final boolean NewNestedScrollFlingDispatchingEnabled;
property public final boolean isRectTrackingEnabled;
property public final boolean isSemanticAutofillEnabled;
- property public final boolean isViewFocusFixEnabled;
field public static final androidx.compose.ui.ComposeUiFlags INSTANCE;
field public static boolean NewNestedScrollFlingDispatchingEnabled;
field public static boolean isRectTrackingEnabled;
field public static boolean isSemanticAutofillEnabled;
- field public static boolean isViewFocusFixEnabled;
}
public final class ComposedModifierKt {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index f614763c..258aa94 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -135,12 +135,10 @@
property public final boolean NewNestedScrollFlingDispatchingEnabled;
property public final boolean isRectTrackingEnabled;
property public final boolean isSemanticAutofillEnabled;
- property public final boolean isViewFocusFixEnabled;
field public static final androidx.compose.ui.ComposeUiFlags INSTANCE;
field public static boolean NewNestedScrollFlingDispatchingEnabled;
field public static boolean isRectTrackingEnabled;
field public static boolean isSemanticAutofillEnabled;
- field public static boolean isViewFocusFixEnabled;
}
public final class ComposedModifierKt {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
index baafca3..8d7bed4 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusViewInteropTest.kt
@@ -19,9 +19,7 @@
import android.content.Context
import android.graphics.Rect as AndroidRect
import android.view.View
-import android.widget.Button
import android.widget.EditText
-import android.widget.LinearLayout
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
@@ -35,8 +33,6 @@
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.material.Button
-import androidx.compose.material.Text
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -45,24 +41,14 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection.Companion.Down
-import androidx.compose.ui.focus.FocusDirection.Companion.Left
-import androidx.compose.ui.focus.FocusDirection.Companion.Next
-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.geometry.Offset
-import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.requestFocus
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.dp
@@ -225,268 +211,6 @@
assertThat(thirdFocused).isTrue()
}
- @Test
- fun moveFocusThroughUnfocusableComposeViewNext() {
- lateinit var topEditText: EditText
- lateinit var composeView: ComposeView
- lateinit var bottomEditText: EditText
- lateinit var focusManager: FocusManager
-
- rule.setContent {
- focusManager = LocalFocusManager.current
- AndroidView(
- modifier = Modifier.fillMaxSize(),
- factory = { context ->
- LinearLayout(context).also { linearLayout ->
- linearLayout.orientation = LinearLayout.VERTICAL
- EditText(context).also {
- linearLayout.addView(it)
- topEditText = it
- }
- ComposeView(context).also {
- it.setContent { Box(Modifier.size(10.dp)) }
- linearLayout.addView(it)
- composeView = it
- }
- EditText(context).also {
- linearLayout.addView(it)
- bottomEditText = it
- }
- }
- }
- )
- }
-
- rule.runOnIdle { topEditText.requestFocus() }
-
- rule.runOnIdle { focusManager.moveFocus(Next) }
-
- rule.runOnIdle {
- assertThat(topEditText.isFocused).isFalse()
- assertThat(composeView.isFocused).isFalse()
- assertThat(bottomEditText.isFocused).isTrue()
- }
- }
-
- @Test
- fun moveFocusThroughUnfocusableComposeViewDown() {
- lateinit var topEditText: EditText
- lateinit var composeView: ComposeView
- lateinit var bottomEditText: EditText
- lateinit var focusManager: FocusManager
-
- rule.setContent {
- focusManager = LocalFocusManager.current
- AndroidView(
- modifier = Modifier.fillMaxSize(),
- factory = { context ->
- LinearLayout(context).also { linearLayout ->
- linearLayout.orientation = LinearLayout.VERTICAL
- EditText(context).also {
- linearLayout.addView(it)
- topEditText = it
- }
- ComposeView(context).also {
- it.setContent { Box(Modifier.size(10.dp)) }
- linearLayout.addView(it)
- composeView = it
- }
- EditText(context).also {
- linearLayout.addView(it)
- bottomEditText = it
- }
- }
- }
- )
- }
-
- rule.runOnIdle { topEditText.requestFocus() }
-
- rule.runOnIdle { focusManager.moveFocus(Down) }
-
- rule.runOnIdle {
- assertThat(topEditText.isFocused).isFalse()
- assertThat(composeView.isFocused).isFalse()
- assertThat(bottomEditText.isFocused).isTrue()
- }
- }
-
- @Test
- fun focusBetweenComposeViews_NextPrevious() {
- lateinit var focusManager: FocusManager
-
- rule.setContent {
- focusManager = LocalFocusManager.current
- AndroidView(
- modifier = Modifier.fillMaxSize(),
- factory = { context ->
- LinearLayout(context).also { linearLayout ->
- linearLayout.orientation = LinearLayout.VERTICAL
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button1")
- )
- }
- linearLayout.addView(it)
- }
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button2")
- )
- }
- linearLayout.addView(it)
- }
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button3")
- )
- }
- linearLayout.addView(it)
- }
- }
- }
- )
- }
- rule.onNodeWithTag("button1").requestFocus()
- rule.onNodeWithTag("button1").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Next) }
- rule.onNodeWithTag("button2").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Next) }
- rule.onNodeWithTag("button3").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Previous) }
- rule.onNodeWithTag("button2").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Previous) }
- rule.onNodeWithTag("button1").assertIsFocused()
- }
-
- @Test
- fun focusBetweenComposeViews_DownUp() {
- lateinit var focusManager: FocusManager
-
- rule.setContent {
- focusManager = LocalFocusManager.current
- AndroidView(
- modifier = Modifier.fillMaxSize(),
- factory = { context ->
- LinearLayout(context).also { linearLayout ->
- linearLayout.orientation = LinearLayout.VERTICAL
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button1")
- )
- }
- linearLayout.addView(it)
- }
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button2")
- )
- }
- linearLayout.addView(it)
- }
- ComposeView(context).also {
- it.setContent {
- Box(
- Modifier.size(10.dp)
- .focusProperties { canFocus = true }
- .focusable()
- .testTag("button3")
- )
- }
- linearLayout.addView(it)
- }
- }
- }
- )
- }
- rule.onNodeWithTag("button1").requestFocus()
- rule.onNodeWithTag("button1").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Down) }
- rule.onNodeWithTag("button2").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Down) }
- rule.onNodeWithTag("button3").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Up) }
- rule.onNodeWithTag("button2").assertIsFocused()
- rule.runOnIdle { focusManager.moveFocus(Up) }
- rule.onNodeWithTag("button1").assertIsFocused()
- }
-
- @Test
- fun requestFocusFromViewMovesToComposeView() {
- lateinit var androidButton1: Button
- lateinit var composeView: View
- val composeButton = FocusRequester()
- rule.setContent {
- composeView = LocalView.current
- Column(Modifier.fillMaxSize()) {
- Button(
- onClick = {},
- Modifier.testTag("button")
- .focusProperties { canFocus = true }
- .focusRequester(composeButton)
- ) {
- Text("Compose Button")
- }
- AndroidView(
- factory = { context ->
- LinearLayout(context).also { linearLayout ->
- linearLayout.orientation = LinearLayout.VERTICAL
- linearLayout.addView(
- Button(context).apply {
- setText("Android Button")
- isFocusableInTouchMode = true
- androidButton1 = this
- }
- )
- linearLayout.addView(
- Button(context).apply {
- setText("Android Button 2")
- isFocusableInTouchMode = true
- }
- )
- }
- }
- )
- }
- }
-
- for (direction in arrayOf(Left, Up, Right, Down, Next, Previous)) {
- rule.runOnIdle { androidButton1.requestFocus() }
-
- rule.runOnIdle {
- assertThat(androidButton1.isFocused).isTrue()
- composeButton.requestFocus(direction)
- }
-
- rule.onNodeWithTag("button").assertIsFocused()
-
- rule.runOnIdle {
- assertThat(composeView.isFocused).isTrue()
- assertThat(androidButton1.isFocused).isFalse()
- }
- }
- }
-
private fun View.getFocusedRect() =
AndroidRect().run {
rule.runOnIdle { getFocusedRect(this) }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
index 334d095..71f1d7e 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
@@ -248,8 +248,7 @@
// Assert.
rule.runOnIdle {
- // b/369256395 we must return true when we advertise that we're focusable
- assertThat(success).isTrue()
+ assertThat(success).isFalse()
assertThat(ownerView.isFocused).isFalse()
}
}
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 ad5617d..5b17e1c3 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
@@ -107,6 +107,7 @@
import androidx.compose.ui.focus.focusRect
import androidx.compose.ui.focus.is1dFocusSearch
import androidx.compose.ui.focus.isBetterCandidate
+import androidx.compose.ui.focus.requestFocus
import androidx.compose.ui.focus.requestInteropFocus
import androidx.compose.ui.focus.toAndroidFocusDirection
import androidx.compose.ui.focus.toFocusDirection
@@ -325,90 +326,28 @@
override val windowInfo: WindowInfo
get() = _windowInfo
- /**
- * Because AndroidComposeView always accepts focus, we have to divert focus to another View if
- * there is nothing focusable within. However, if there are only nonfocusable ComposeViews, then
- * the redirection can recurse infinitely. This makes sure that if that happens, then it can
- * bail when it is detected
- */
- private var processingRequestFocusForNextNonChildView = false
-
// When move focus is triggered by a key event, and move focus does not cause any focus change,
// we return the key event to the view system if focus search finds a suitable view which is not
// a compose sub-view. However if move focus is triggered programmatically, we have to manually
// implement this behavior because the view system does not have a moveFocus API.
private fun onMoveFocusInChildren(focusDirection: FocusDirection): Boolean {
- @OptIn(ExperimentalComposeUiApi::class)
- if (!ComposeUiFlags.isViewFocusFixEnabled) {
- // The view system does not have an API corresponding to Enter/Exit.
- if (focusDirection == Enter || focusDirection == Exit) return false
- val direction =
- checkNotNull(focusDirection.toAndroidFocusDirection()) { "Invalid focus direction" }
- val focusedRect = onFetchFocusRect()?.toAndroidRect()
-
- val nextView =
- FocusFinder.getInstance().let {
- if (focusedRect == null) {
- it.findNextFocus(this, findFocus(), direction)
- } else {
- it.findNextFocusFromRect(this, focusedRect, direction)
- }
- }
- return nextView?.requestInteropFocus(direction, focusedRect) ?: false
- }
// The view system does not have an API corresponding to Enter/Exit.
- if (focusDirection == Enter || focusDirection == Exit || !hasFocus()) return false
-
- val androidViewsHandler = _androidViewsHandler ?: return false
+ if (focusDirection == Enter || focusDirection == Exit) return false
val direction =
checkNotNull(focusDirection.toAndroidFocusDirection()) { "Invalid focus direction" }
+ val focusedRect = onFetchFocusRect()?.toAndroidRect()
- val root = rootView as ViewGroup
-
- val currentFocus = root.findFocus() ?: error("view hasFocus but root can't find it")
-
- val focusFinder = FocusFinder.getInstance()
- val nextView: View?
- val focusedRect: Rect?
- if (focusDirection.is1dFocusSearch() && androidViewsHandler.hasFocus()) {
- focusedRect = null
- if (SDK_INT >= O) {
- // On newer devices, the focus is normal and we can expect forward/next to work
- nextView = focusFinder.findNextFocus(root, currentFocus, direction)
- } else {
- // On older devices, FocusFinder doesn't properly order Views, so we have to use
- // a copy of the focus finder the corrects the order
- nextView = FocusFinderCompat.instance.findNextFocus1d(root, currentFocus, direction)
+ val nextView =
+ FocusFinder.getInstance().let {
+ if (focusedRect == null) {
+ it.findNextFocus(this, findFocus(), direction)
+ } else {
+ it.findNextFocusFromRect(this, focusedRect, direction)
+ }
}
- } else {
- focusedRect = onFetchFocusRect()?.toAndroidRect()
- nextView = focusFinder.findNextFocusFromRect(root, focusedRect, direction)
- nextView?.getLocationInWindow(tmpPositionArray)
- val nextPositionX = tmpPositionArray[0]
- val nextPositionY = tmpPositionArray[1]
- getLocationInWindow(tmpPositionArray)
- focusedRect?.offset(
- tmpPositionArray[0] - nextPositionX,
- tmpPositionArray[1] - nextPositionY
- )
- }
-
- // is it part of the compose hierarchy?
- if (nextView == null || nextView === currentFocus) {
- return false
- }
-
- val focusedChild = androidViewsHandler.focusedChild
- var nextParent = nextView.parent
- while (nextParent != null && nextParent !== focusedChild) {
- nextParent = nextParent.parent
- }
- if (nextParent == null) {
- return false // not a part of the compose hierarchy
- }
- return nextView.requestInteropFocus(direction, focusedRect)
+ return nextView?.requestInteropFocus(direction, focusedRect) ?: false
}
// If this root view is focused, we can get the focus rect from focusOwner. But if a sub-view
@@ -429,16 +368,6 @@
val focusDirection = getFocusDirection(keyEvent)
if (focusDirection == null || keyEvent.type != KeyDown) return@onKeyEvent false
- val androidDirection = focusDirection.toAndroidFocusDirection()
-
- @OptIn(ExperimentalComposeUiApi::class)
- if (ComposeUiFlags.isViewFocusFixEnabled) {
- if (hasFocus() && androidDirection != null) {
- // A child AndroidView is focused. See if the view has a child that should be
- // focused next.
- if (onMoveFocusInChildren(focusDirection)) return@onKeyEvent true
- }
- }
val focusedRect = onFetchFocusRect()
// Consume the key event if we moved focus or if focus search or requestFocus is
@@ -459,22 +388,13 @@
// this view. We don't return false because we don't want to re-visit sub-views. They
// will
// instead be visited when the AndroidView around them gets a moveFocus(Enter)).
+ val androidDirection =
+ checkNotNull(focusDirection.toAndroidFocusDirection()) { "Invalid focus direction" }
+ val androidRect = checkNotNull(focusedRect?.toAndroidRect()) { "Invalid rect" }
- if (androidDirection != null) {
- val nextView = findNextNonChildView(androidDirection).takeIf { it != this }
- if (nextView != null) {
- val androidRect = checkNotNull(focusedRect?.toAndroidRect()) { "Invalid rect" }
- nextView.getLocationInWindow(tmpPositionArray)
- val nextX = tmpPositionArray[0]
- val nextY = tmpPositionArray[1]
- getLocationInWindow(tmpPositionArray)
- val currentX = tmpPositionArray[0]
- val currentY = tmpPositionArray[1]
- androidRect.offset(currentX - nextX, currentY - nextY)
- if (nextView.requestInteropFocus(androidDirection, androidRect)) {
- return@onKeyEvent true
- }
- }
+ val nextView = findNextNonChildView(androidDirection).takeIf { it != this }
+ if (nextView != null && nextView.requestInteropFocus(androidDirection, androidRect)) {
+ return@onKeyEvent true
}
// Focus finder couldn't find another view. We manually wrap around since focus remained
@@ -498,9 +418,10 @@
private fun findNextNonChildView(direction: Int): View? {
var currentView: View? = this
- val focusFinder = FocusFinder.getInstance()
while (currentView != null) {
- currentView = focusFinder.findNextFocus(rootView as ViewGroup, currentView, direction)
+ currentView =
+ FocusFinder.getInstance()
+ .findNextFocus(rootView as ViewGroup, currentView, direction)
if (currentView != null && !containsDescendant(currentView)) return currentView
}
return null
@@ -1014,73 +935,22 @@
}
override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean {
- @OptIn(ExperimentalComposeUiApi::class)
- if (!ComposeUiFlags.isViewFocusFixEnabled) {
- // This view is already focused.
- if (isFocused) return true
-
- // If the root has focus, it means a sub-view is focused,
- // and is trying to move focus within itself.
- if (focusOwner.rootState.hasFocus) {
- return super.requestFocus(direction, previouslyFocusedRect)
- }
-
- val focusDirection = toFocusDirection(direction) ?: Enter
- return focusOwner.focusSearch(
- focusDirection = focusDirection,
- focusedRect = previouslyFocusedRect?.toComposeRect()
- ) {
- it.requestFocus(focusDirection)
- } ?: false
- }
// This view is already focused.
if (isFocused) return true
- // There is nothing focusable and we've looped around all Views back to this one, so
- // we just return false to indicate that nothing can be focused.
- if (processingRequestFocusForNextNonChildView) return false
-
- val focusDirection = toFocusDirection(direction) ?: Enter
-
// If the root has focus, it means a sub-view is focused,
// and is trying to move focus within itself.
- if (hasFocus() && onMoveFocusInChildren(focusDirection)) return true
-
- var foundFocusable = false
- val focusSearchResult =
- focusOwner.focusSearch(
- focusDirection = focusDirection,
- focusedRect = previouslyFocusedRect?.toComposeRect()
- ) {
- foundFocusable = true
- it.requestFocus(focusDirection)
- }
- if (focusSearchResult == null) {
- return false // The focus search was canceled
- }
- if (focusSearchResult) {
- return true // We found something to focus on
- }
- if (foundFocusable) {
- return false // The requestFocus() from within the focusSearch was canceled
+ if (focusOwner.rootState.hasFocus) {
+ return super.requestFocus(direction, previouslyFocusedRect)
}
- // We advertised ourselves as focusable, but we aren't. Try to just move the focus to the
- // next item.
- val nextFocusedView = findNextNonChildView(direction)
-
- // Can crash if we return false when we've advertised ourselves as focusable and we aren't
- // b/369256395
- if (nextFocusedView == null || nextFocusedView === this) {
- // There is no next View, so just return true so we don't cause a crash
- return true
- }
-
- // Try to focus on the next focusable View
- processingRequestFocusForNextNonChildView = true
- val requestFocusResult = nextFocusedView.requestFocus(direction)
- processingRequestFocusForNextNonChildView = false
- return requestFocusResult
+ val focusDirection = toFocusDirection(direction) ?: Enter
+ return focusOwner.focusSearch(
+ focusDirection = focusDirection,
+ focusedRect = previouslyFocusedRect?.toComposeRect()
+ ) {
+ it.requestFocus(focusDirection)
+ } ?: false
}
private fun onRequestFocusForOwner(
@@ -1098,12 +968,7 @@
}
private fun onClearFocusForOwner() {
- @OptIn(ExperimentalComposeUiApi::class)
- if (isFocused || (!ComposeUiFlags.isViewFocusFixEnabled && hasFocus())) {
- super.clearFocus()
- } else if (hasFocus()) {
- // Call clearFocus() on the child that has focus
- findFocus()?.clearFocus()
+ if (isFocused || hasFocus()) {
super.clearFocus()
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/FocusFinderCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/FocusFinderCompat.android.kt
deleted file mode 100644
index 9a42b02..0000000
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/FocusFinderCompat.android.kt
+++ /dev/null
@@ -1,462 +0,0 @@
-/*
- * Copyright 2024 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.platform
-
-import android.annotation.SuppressLint
-import android.content.pm.PackageManager
-import android.graphics.Rect
-import android.os.Build
-import android.view.View
-import android.view.View.FOCUS_BACKWARD
-import android.view.View.FOCUS_FORWARD
-import android.view.ViewGroup
-import androidx.collection.MutableObjectList
-import androidx.collection.ObjectList
-import androidx.collection.mutableObjectIntMapOf
-import androidx.collection.mutableScatterMapOf
-import androidx.collection.mutableScatterSetOf
-import androidx.core.view.isVisible
-import java.util.Collections
-
-/**
- * On devices before [Build.VERSION_CODES.O], [FocusFinder] orders Views incorrectly. This
- * implementation uses the current [FocusFinder] behavior, ordering Views correctly for
- * one-dimensional focus searches.
- *
- * This is copied and simplified from FocusFinder's source. There may be some code that doesn't look
- * quite right in Kotlin as it was copy/pasted with auto-translation.
- */
-internal class FocusFinderCompat {
- companion object {
- private val FocusFinderThreadLocal =
- object : ThreadLocal<FocusFinderCompat>() {
- override fun initialValue(): FocusFinderCompat {
- return FocusFinderCompat()
- }
- }
-
- /** Get the focus finder for this thread. */
- val instance: FocusFinderCompat
- get() = FocusFinderThreadLocal.get()!!
- }
-
- private val focusedRect: Rect = Rect()
-
- private val userSpecifiedFocusComparator =
- UserSpecifiedFocusComparator({ r, v ->
- if (isValidId(v.nextFocusForwardId)) v.findUserSetNextFocus(r, FOCUS_FORWARD) else null
- })
-
- private val tmpList = MutableObjectList<View>()
-
- // enforce thread local access
- private fun FocusFinder() {}
-
- /**
- * Find the next view to take focus in root's descendants, starting from the view that currently
- * is focused.
- *
- * @param root Contains focused. Cannot be null.
- * @param focused Has focus now.
- * @param direction Direction to look.
- * @return The next focusable view, or null if none exists.
- */
- fun findNextFocus1d(root: ViewGroup, focused: View, direction: Int): View? {
- val effectiveRoot = getEffectiveRoot(root, focused)
- var next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction)
- if (next != null) {
- return next
- }
- val focusables = tmpList
- try {
- focusables.clear()
- effectiveRoot.addFocusableViews(focusables, direction)
- if (!focusables.isEmpty()) {
- next = findNextFocus(effectiveRoot, focused, direction, focusables)
- }
- } finally {
- focusables.clear()
- }
- return next
- }
-
- /**
- * Returns the "effective" root of a view. The "effective" root is the closest ancestor
- * within-which focus should cycle.
- *
- * For example: normal focus navigation would stay within a ViewGroup marked as
- * touchscreenBlocksFocus and keyboardNavigationCluster until a cluster-jump out.
- *
- * @return the "effective" root of {@param focused}
- */
- private fun getEffectiveRoot(root: ViewGroup, focused: View?): ViewGroup {
- if (focused == null || focused === root) {
- return root
- }
- var effective: ViewGroup? = null
- var nextParent = focused.parent
- while (nextParent is ViewGroup) {
- if (nextParent === root) {
- return effective ?: root
- }
- val vg = nextParent
- if (
- vg.touchscreenBlocksFocus &&
- focused.context.packageManager.hasSystemFeature(
- PackageManager.FEATURE_TOUCHSCREEN
- )
- ) {
- // Don't stop and return here because the cluster could be nested and we only
- // care about the top-most one.
- effective = vg
- }
- nextParent = nextParent.parent
- }
- return root
- }
-
- private fun findNextUserSpecifiedFocus(root: ViewGroup, focused: View, direction: Int): View? {
- // check for user specified next focus
- var userSetNextFocus: View? = focused.findUserSetNextFocus(root, direction)
- var cycleCheck = userSetNextFocus
- var cycleStep = true // we want the first toggle to yield false
- while (userSetNextFocus != null) {
- if (
- userSetNextFocus.isFocusable &&
- userSetNextFocus.visibility == View.VISIBLE &&
- (!userSetNextFocus.isInTouchMode || userSetNextFocus.isFocusableInTouchMode)
- ) {
- return userSetNextFocus
- }
- userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction)
- if ((!cycleStep).also { cycleStep = it }) {
- cycleCheck = cycleCheck?.findUserSetNextFocus(root, direction)
- if (cycleCheck === userSetNextFocus) {
- // found a cycle, user-specified focus forms a loop and none of the views
- // are currently focusable.
- break
- }
- }
- }
- return null
- }
-
- private fun findNextFocus(
- root: ViewGroup,
- focused: View,
- direction: Int,
- focusables: MutableObjectList<View>
- ): View? {
- val focusedRect = focusedRect
- // fill in interesting rect from focused
- focused.getFocusedRect(focusedRect)
- root.offsetDescendantRectToMyCoords(focused, focusedRect)
-
- return findNextFocusInRelativeDirection(focusables, root, focused, direction)
- }
-
- @SuppressLint("AsCollectionCall")
- private fun findNextFocusInRelativeDirection(
- focusables: MutableObjectList<View>,
- root: ViewGroup?,
- focused: View,
- direction: Int
- ): View? {
- try {
- // Note: This sort is stable.
- userSpecifiedFocusComparator.setFocusables(focusables, root!!)
- Collections.sort(focusables.asMutableList(), userSpecifiedFocusComparator)
- } finally {
- userSpecifiedFocusComparator.recycle()
- }
-
- val count = focusables.size
- if (count < 2) {
- return null
- }
- var next: View? = null
- val looped = BooleanArray(1)
- when (direction) {
- FOCUS_FORWARD -> next = getNextFocusable(focused, focusables, count, looped)
- FOCUS_BACKWARD -> next = getPreviousFocusable(focused, focusables, count, looped)
- }
- return next ?: focusables[count - 1]
- }
-
- private fun getNextFocusable(
- focused: View,
- focusables: ObjectList<View>,
- count: Int,
- outLooped: BooleanArray
- ): View? {
- if (count < 2) {
- return null
- }
- val position = focusables.lastIndexOf(focused)
- if (position >= 0 && position + 1 < count) {
- return focusables[position + 1]
- }
- outLooped[0] = true
- return focusables[0]
- }
-
- private fun getPreviousFocusable(
- focused: View?,
- focusables: ObjectList<View>,
- count: Int,
- outLooped: BooleanArray
- ): View? {
- if (count < 2) {
- return null
- }
- if (focused != null) {
- val position = focusables.indexOf(focused)
- if (position > 0) {
- return focusables[position - 1]
- }
- }
- outLooped[0] = true
- return focusables[count - 1]
- }
-
- private fun isValidId(id: Int): Boolean {
- return id != 0 && id != View.NO_ID
- }
-
- /**
- * Sorts views according to any explicitly-specified focus-chains. If there are no explicitly
- * specified focus chains (eg. no nextFocusForward attributes defined), this should be a no-op.
- */
- private class UserSpecifiedFocusComparator(private val mNextFocusGetter: NextFocusGetter) :
- Comparator<View?> {
- private val nextFoci = mutableScatterMapOf<View, View>()
- private val isConnectedTo = mutableScatterSetOf<View>()
- private val headsOfChains = mutableScatterMapOf<View, View>()
- private val originalOrdinal = mutableObjectIntMapOf<View>()
- private var root: View? = null
-
- fun interface NextFocusGetter {
- fun get(root: View, view: View): View?
- }
-
- fun recycle() {
- root = null
- headsOfChains.clear()
- isConnectedTo.clear()
- originalOrdinal.clear()
- nextFoci.clear()
- }
-
- fun setFocusables(focusables: ObjectList<View>, root: View) {
- this.root = root
- focusables.forEachIndexed { index, view -> originalOrdinal[view] = index }
-
- for (i in focusables.indices.reversed()) {
- val view = focusables[i]
- val next = mNextFocusGetter.get(root, view)
- if (next != null && originalOrdinal.containsKey(next)) {
- nextFoci[view] = next
- isConnectedTo.add(next)
- }
- }
-
- for (i in focusables.indices.reversed()) {
- val view = focusables[i]
- val next = nextFoci[view]
- if (next != null && !isConnectedTo.contains(view)) {
- setHeadOfChain(view)
- }
- }
- }
-
- fun setHeadOfChain(head: View) {
- var newHead = head
- var view: View? = newHead
- while (view != null) {
- val otherHead = headsOfChains[view]
- if (otherHead != null) {
- if (otherHead === newHead) {
- return // This view has already had its head set properly
- }
- // A hydra -- multi-headed focus chain (e.g. A->C and B->C)
- // Use the one we've already chosen instead and reset this chain.
- view = newHead
- newHead = otherHead
- }
- headsOfChains[view] = newHead
- view = nextFoci[view]
- }
- }
-
- override fun compare(first: View?, second: View?): Int {
- if (first === second) {
- return 0
- }
- if (first == null) {
- return -1
- }
- if (second == null) {
- return 1
- }
- // Order between views within a chain is immaterial -- next/previous is
- // within a chain is handled elsewhere.
- val firstHead = headsOfChains[first]
- val secondHead = headsOfChains[second]
- if (firstHead === secondHead && firstHead != null) {
- return if (first === firstHead) {
- -1 // first is the head, it should be first
- } else if (second === firstHead) {
- 1 // second is the head, it should be first
- } else if (nextFoci[first] != null) {
- -1 // first is not the end of the chain
- } else {
- 1 // first is end of chain
- }
- }
- val chainedFirst = firstHead ?: first
- val chainedSecond = secondHead ?: second
-
- return if (firstHead != null || secondHead != null) {
- // keep original order between chains
- if (originalOrdinal[chainedFirst] < originalOrdinal[chainedSecond]) -1 else 1
- } else {
- 0
- }
- }
- }
-}
-
-/**
- * If a user manually specified the next view id for a particular direction, use the root to look up
- * the view.
- *
- * @param root The root view of the hierarchy containing this view.
- * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, or
- * FOCUS_BACKWARD.
- * @return The user specified next view, or null if there is none.
- */
-private fun View.findUserSetNextFocus(root: View, direction: Int): View? {
- when (direction) {
- FOCUS_FORWARD -> {
- val next = nextFocusForwardId
- if (next == View.NO_ID) return null
- return findViewInsideOutShouldExist(root, this, next)
- }
- FOCUS_BACKWARD -> {
- if (id == View.NO_ID) return null
- val startView: View = this
- // Since we have forward links but no backward links, we need to find the view that
- // forward links to this view. We can't just find the view with the specified ID
- // because view IDs need not be unique throughout the tree.
- return root.findViewByPredicateInsideOut(startView) { t ->
- (findViewInsideOutShouldExist(root, t, t.nextFocusForwardId) === startView)
- }
- }
- }
- return null
-}
-
-private fun findViewInsideOutShouldExist(root: View, start: View, id: Int): View? {
- return root.findViewByPredicateInsideOut(start) { it.id == id }
-}
-
-/**
- * Look for a child view that matches the specified predicate, starting with the specified view and
- * its descendants and then recursively searching the ancestors and siblings of that view until this
- * view is reached.
- *
- * This method is useful in cases where the predicate does not match a single unique view (perhaps
- * multiple views use the same id) and we are trying to find the view that is "closest" in scope to
- * the starting view.
- *
- * @param start The view to start from.
- * @param predicate The predicate to evaluate.
- * @return The first view that matches the predicate or null.
- */
-private fun View.findViewByPredicateInsideOut(start: View, predicate: (View) -> Boolean): View? {
- var next = start
- var childToSkip: View? = null
- while (true) {
- val view = next.findViewByPredicateTraversal(predicate, childToSkip)
- if (view != null || next === this) {
- return view
- }
-
- val parent = next.parent
- if (parent == null || parent !is View) {
- return null
- }
-
- childToSkip = next
- next = parent
- }
-}
-
-/**
- * @param predicate The predicate to evaluate.
- * @param childToSkip If not null, ignores this child during the recursive traversal.
- * @return The first view that matches the predicate or null.
- */
-private fun View.findViewByPredicateTraversal(
- predicate: (View) -> Boolean,
- childToSkip: View?
-): View? {
- if (predicate(this)) {
- return this
- }
- if (this is ViewGroup) {
- for (i in 0 until childCount) {
- val child = getChildAt(i)
- if (child !== childToSkip) {
- val v = child.findViewByPredicateTraversal(predicate, childToSkip)
- if (v != null) {
- return v
- }
- }
- }
- }
- return null
-}
-
-private fun View.addFocusableViews(views: MutableObjectList<View>, direction: Int) {
- addFocusableViews(views, direction, isInTouchMode)
-}
-
-/**
- * Older versions of View don't add focusable Views in order. This is a corrected version that adds
- * them in the right order.
- */
-private fun View.addFocusableViews(
- views: MutableObjectList<View>,
- direction: Int,
- inTouchMode: Boolean
-) {
- if (
- isVisible &&
- isFocusable &&
- isEnabled &&
- width > 0 &&
- height > 0 &&
- (!inTouchMode || isFocusableInTouchMode)
- ) {
- views += this
- }
- if (this is ViewGroup) {
- for (i in 0 until childCount) {
- getChildAt(i).addFocusableViews(views, direction, inTouchMode)
- }
- }
-}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index 9c9835e..8b1d1dc9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -25,8 +25,6 @@
import android.view.ViewParent
import androidx.compose.runtime.ComposeNodeLifecycleCallback
import androidx.compose.runtime.CompositionContext
-import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@@ -420,10 +418,6 @@
if (view.parent !== this) addView(view)
}
layoutNode.onDetach = { owner ->
- @OptIn(ExperimentalComposeUiApi::class)
- if (ComposeUiFlags.isViewFocusFixEnabled && hasFocus()) {
- owner.focusOwner.clearFocus(true)
- }
(owner as? AndroidComposeView)?.removeAndroidView(this)
removeAllViewsInLayout()
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt
index 8082b9a..0d25269 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/FocusGroupNode.android.kt
@@ -22,8 +22,6 @@
import android.view.ViewGroup
import android.view.ViewGroup.FOCUS_DOWN
import android.view.ViewTreeObserver
-import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection.Companion.Exit
@@ -88,12 +86,7 @@
val onExit: FocusEnterExitScope.() -> Unit = {
val embeddedView = getEmbeddedView()
- @OptIn(ExperimentalComposeUiApi::class)
- if (ComposeUiFlags.isViewFocusFixEnabled) {
- if (embeddedView.hasFocus() || embeddedView.isFocused) {
- embeddedView.clearFocus()
- }
- } else if (embeddedView.hasFocus()) {
+ if (embeddedView.hasFocus()) {
val focusOwner = requireOwner().focusOwner
val hostView = requireView()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
index d518673..728a1bb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
@@ -80,10 +80,4 @@
* the new Autofill APIs and features introduced.
*/
@Suppress("MutableBareField") @JvmField var isSemanticAutofillEnabled: Boolean = false
-
- /**
- * This enables fixes for View focus. The changes are large enough to require a flag to allow
- * disabling them.
- */
- @Suppress("MutableBareField") @JvmField var isViewFocusFixEnabled: Boolean = true
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
index d86a6bc..8741918 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
@@ -17,8 +17,6 @@
package androidx.compose.ui.focus
import androidx.collection.MutableLongSet
-import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.CustomDestinationResult.Cancelled
import androidx.compose.ui.focus.CustomDestinationResult.None
@@ -204,11 +202,6 @@
* @return true if focus was moved successfully. false if the focused item is unchanged.
*/
override fun moveFocus(focusDirection: FocusDirection): Boolean {
- // First check to see if the focus should move within child Views
- @OptIn(ExperimentalComposeUiApi::class)
- if (ComposeUiFlags.isViewFocusFixEnabled && onMoveFocusInterop(focusDirection)) {
- return true
- }
var requestFocusSuccess: Boolean? = false
val generationBefore = focusTransactionManager.generation
val focusSearchSuccess =
@@ -244,8 +237,7 @@
// If we couldn't move focus within compose, we attempt to move focus within embedded views.
// We don't need this for 1D focus search because the wrap-around logic triggers a
// focus exit which will perform a focus search among the subviews.
- @OptIn(ExperimentalComposeUiApi::class)
- return !ComposeUiFlags.isViewFocusFixEnabled && onMoveFocusInterop(focusDirection)
+ return onMoveFocusInterop(focusDirection)
}
override fun focusSearch(
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 fe97805..7d1b2d6 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
@@ -16,9 +16,6 @@
package androidx.compose.ui.focus
-import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.focus.CustomDestinationResult.Cancelled
import androidx.compose.ui.focus.CustomDestinationResult.None
import androidx.compose.ui.focus.CustomDestinationResult.RedirectCancelled
@@ -32,7 +29,6 @@
import androidx.compose.ui.node.Nodes.FocusTarget
import androidx.compose.ui.node.nearestAncestor
import androidx.compose.ui.node.observeReads
-import androidx.compose.ui.node.requireLayoutNode
import androidx.compose.ui.node.requireOwner
/**
@@ -62,14 +58,7 @@
}
}
}
- if (success) {
- @OptIn(ExperimentalComposeUiApi::class, InternalComposeUiApi::class)
- if (ComposeUiFlags.isViewFocusFixEnabled && requireLayoutNode().getInteropView() == null) {
- // This isn't an AndroidView, so we should be focused on this ComposeView
- requireOwner().focusOwner.requestFocusForOwner(FocusDirection.Next, null)
- }
- dispatchFocusCallbacks()
- }
+ if (success) dispatchFocusCallbacks()
return success
}