FocusModifier.kt
/*
* 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.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.LayoutNodeWrapper
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
/**
* Used to build a tree of [FocusModifier] elements. This contains the parent.
*/
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 layoutNodeWrapper: LayoutNodeWrapper? = null
var focusRequestedOnPlaced = false
/**
* The KeyInputModifier that this FocusModifier comes after.
*/
var keyInputModifier: KeyInputModifier? = null
private set
/**
* All KeyInputModifiers that read this [FocusModifier] in the
* [ModifierLocalParentFocusModifier].
*/
val keyInputChildren = mutableVectorOf<KeyInputModifier>()
// Reading the FocusProperties ModifierLocal.
override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
modifierLocalReadScope = scope
with(scope) {
parent = ModifierLocalParentFocusModifier.current.also { newParent ->
if (newParent != parent) {
if (newParent == null) {
when (focusState) {
Active, Captured -> layoutNodeWrapper?.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
// Update the focus node with the current focus properties.
refreshFocusProperties()
}
}
@ExperimentalComposeUiApi
fun propagateRotaryEvent(event: RotaryScrollEvent): Boolean {
return rotaryScrollParent?.propagateFocusAwareEvent(event) ?: false
}
// 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
companion object {
val RefreshFocusProperties: (FocusModifier) -> Unit = { focusModifier ->
focusModifier.refreshFocusProperties()
}
}
override val key: ProvidableModifierLocal<FocusModifier?>
get() = ModifierLocalParentFocusModifier
override val value: FocusModifier
get() = this
override fun onPlaced(coordinates: LayoutCoordinates) {
val wasNull = layoutNodeWrapper == null
layoutNodeWrapper = coordinates as LayoutNodeWrapper
if (wasNull) {
refreshFocusProperties()
}
if (focusRequestedOnPlaced) {
focusRequestedOnPlaced = false
requestFocus()
}
}
}
/**
* Add this modifier to a component to make it focusable.
*
* Focus state is stored within this modifier. The bounds of this modifier reflect the bounds of
* the focus box.
*
* Note: This is a low level modifier. Before using this consider using
* [Modifier.focusable()][androidx.compose.foundation.focusable]. It uses a [focusTarget] in
* its implementation. [Modifier.focusable()][androidx.compose.foundation.focusable] adds semantics
* that are needed for accessibility.
*
* @sample androidx.compose.ui.samples.FocusableSampleUsingLowerLevelFocusTarget
*/
fun Modifier.focusTarget(): Modifier = composed(debugInspectorInfo { name = "focusTarget" }) {
val focusModifier = remember { FocusModifier(Inactive) }
SideEffect {
focusModifier.sendOnFocusEvent()
}
focusTarget(focusModifier)
}
/**
* Add this modifier to a component to make it focusable.
*/
@Deprecated(
"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
}
)