[go: nahoru, domu]

blob: f7574c4beebfdd83e7ba814384d3a5018261c355 [file] [log] [blame]
/*
* 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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection.Companion.Next
import androidx.compose.ui.focus.FocusDirection.Companion.Previous
import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
import androidx.compose.ui.focus.FocusRequester.Companion.Default
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.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 {
/**
* Call this function to clear focus from the currently focused component, and 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.
*
* @sample androidx.compose.ui.samples.ClearFocusSample
*/
fun clearFocus(force: Boolean = false)
/**
* Moves focus in the specified [direction][FocusDirection].
*
* If you are not satisfied with the default focus order, consider setting a custom order using
* [Modifier.focusProperties()][focusProperties].
*
* @return true if focus was moved successfully. false if the focused item is unchanged.
*
* @sample androidx.compose.ui.samples.MoveFocusSample
*/
fun moveFocus(focusDirection: FocusDirection): Boolean
}
/**
* The focus manager is used by different [Owner][androidx.compose.ui.node.Owner] implementations
* to control focus.
*/
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.
@Suppress("ModifierInspectorInfo") // b/251831790.
@OptIn(ExperimentalComposeUiApi::class)
override val modifier: Modifier = modifierElementOf(
create = { rootFocusNode },
definitions = { name = "RootFocusTarget" }
)
override lateinit 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.
*/
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.
@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][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.
*/
override fun releaseFocus() {
@OptIn(ExperimentalComposeUiApi::class)
rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
}
/**
* 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.
*
* This could be used to clear focus when a user clicks on empty space outside a focusable
* 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 = rootFocusNode.focusStateImpl
if (rootFocusNode.clearFocus(force, refreshFocusEvents)) {
rootFocusNode.focusStateImpl = when (rootInitialState) {
Active, ActiveParent, Captured -> Active
Inactive -> Inactive
}
}
}
/**
* Moves focus in the specified direction.
*
* @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 = rootFocusNode.findActiveFocusNode() ?: return false
// Check if a custom focus traversal order is specified.
return when (val next = source.customFocusSearch(focusDirection, layoutDirection)) {
@OptIn(ExperimentalComposeUiApi::class)
Cancel -> false
Default -> {
val foundNextItem =
rootFocusNode.focusSearch(focusDirection, layoutDirection) { destination ->
if (destination == source) return@focusSearch false
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
}
// If we didn't find a potential next item, try to wrap around.
foundNextItem || wrapAroundFocus(focusDirection)
}
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).
next.requestFocus()
true
}
}
}
/**
* Dispatches a key event through the compose hierarchy.
*/
@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,
onPreVisit = { if (it.onPreKeyEvent(keyEvent)) return true },
onVisit = { if (it.onKeyEvent(keyEvent)) return true }
)
return false
}
/**
* Dispatches a rotary scroll event through the compose hierarchy.
*/
@OptIn(ExperimentalComposeUiApi::class)
override fun dispatchRotaryEvent(event: RotaryScrollEvent): Boolean {
val focusedRotaryInputNode = rootFocusNode.findActiveFocusNode()
?.nearestAncestor(Nodes.RotaryInput)
focusedRotaryInputNode?.traverseAncestors(
type = Nodes.RotaryInput,
onPreVisit = { if (it.onPreRotaryScrollEvent(event)) return true },
onVisit = { 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
// replaced by code that sends the move request back to the view system. The view system
// 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.
@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)
@OptIn(ExperimentalComposeUiApi::class)
if (!rootFocusNode.focusState.isFocused) return false
// Wrap around by calling moveFocus after the root gains focus.
return moveFocus(focusDirection)
}
// We only wrap-around for 1D Focus search.
else -> return false
}
}
}