1 /*
<lambda>null2 * Copyright 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.ui.focus
18
19 import androidx.collection.MutableLongSet
20 import androidx.collection.MutableObjectList
21 import androidx.compose.ui.ComposeUiFlags
22 import androidx.compose.ui.ExperimentalComposeUiApi
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.focus.CustomDestinationResult.Cancelled
25 import androidx.compose.ui.focus.CustomDestinationResult.None
26 import androidx.compose.ui.focus.CustomDestinationResult.RedirectCancelled
27 import androidx.compose.ui.focus.CustomDestinationResult.Redirected
28 import androidx.compose.ui.focus.FocusDirection.Companion.Exit
29 import androidx.compose.ui.focus.FocusDirection.Companion.Next
30 import androidx.compose.ui.focus.FocusDirection.Companion.Previous
31 import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
32 import androidx.compose.ui.focus.FocusRequester.Companion.Default
33 import androidx.compose.ui.focus.FocusRequester.Companion.Redirect
34 import androidx.compose.ui.focus.FocusStateImpl.Active
35 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
36 import androidx.compose.ui.focus.FocusStateImpl.Captured
37 import androidx.compose.ui.focus.FocusStateImpl.Inactive
38 import androidx.compose.ui.geometry.Rect
39 import androidx.compose.ui.input.indirect.IndirectTouchEvent
40 import androidx.compose.ui.input.key.KeyEvent
41 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
42 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
43 import androidx.compose.ui.input.key.key
44 import androidx.compose.ui.input.key.type
45 import androidx.compose.ui.input.rotary.RotaryScrollEvent
46 import androidx.compose.ui.internal.requirePrecondition
47 import androidx.compose.ui.node.DelegatableNode
48 import androidx.compose.ui.node.ModifierNodeElement
49 import androidx.compose.ui.node.NodeKind
50 import androidx.compose.ui.node.Nodes
51 import androidx.compose.ui.node.ancestors
52 import androidx.compose.ui.node.dispatchForKind
53 import androidx.compose.ui.node.nearestAncestor
54 import androidx.compose.ui.node.visitAncestors
55 import androidx.compose.ui.node.visitLocalDescendants
56 import androidx.compose.ui.platform.InspectorInfo
57 import androidx.compose.ui.unit.LayoutDirection
58 import androidx.compose.ui.util.fastForEach
59 import androidx.compose.ui.util.fastForEachReversed
60 import androidx.compose.ui.util.trace
61
62 /**
63 * The focus manager is used by different [Owner][androidx.compose.ui.node.Owner] implementations to
64 * control focus.
65 */
66 internal class FocusOwnerImpl(
67 onRequestApplyChangesListener: (() -> Unit) -> Unit,
68 private val onRequestFocusForOwner:
69 (focusDirection: FocusDirection?, previouslyFocusedRect: Rect?) -> Boolean,
70 private val onMoveFocusInterop: (focusDirection: FocusDirection) -> Boolean,
71 private val onClearFocusForOwner: () -> Unit,
72 private val onFocusRectInterop: () -> Rect?,
73 private val onLayoutDirection: (() -> LayoutDirection)
74 ) : FocusOwner {
75
76 // The root focus target is not focusable, and acts like a focus group.
77 internal var rootFocusNode = FocusTargetNode(focusability = Focusability.Never)
78
79 private val focusInvalidationManager =
80 FocusInvalidationManager(
81 onRequestApplyChangesListener,
82 ::invalidateOwnerFocusState,
83 ::rootState,
84 ::activeFocusTargetNode
85 )
86
87 override val focusTransactionManager: FocusTransactionManager = FocusTransactionManager()
88
89 /**
90 * A [Modifier] that can be added to the [Owners][androidx.compose.ui.node.Owner] modifier list
91 * that contains the modifiers required by the focus system. (Eg, a root focus modifier).
92 */
93 // TODO(b/168831247): return an empty Modifier when there are no focusable children.
94 override val modifier: Modifier =
95 object : ModifierNodeElement<FocusTargetNode>() {
96 override fun create() = rootFocusNode
97
98 override fun update(node: FocusTargetNode) {}
99
100 override fun InspectorInfo.inspectableProperties() {
101 name = "RootFocusTarget"
102 }
103
104 override fun hashCode(): Int = rootFocusNode.hashCode()
105
106 override fun equals(other: Any?) = other === this
107 }
108
109 /**
110 * This function is called to ask the owner to request focus from the framework. eg. If a
111 * composable calls requestFocus and the root view does not have focus, this function can be
112 * used to request focus for the view.
113 *
114 * @param focusDirection If this focus request was triggered by a call to moveFocus or using the
115 * keyboard, provide the owner with the direction of focus change.
116 * @param previouslyFocusedRect The bounds of the currently focused item.
117 * @return true if the owner successfully requested focus from the framework. False otherwise.
118 */
119 override fun requestFocusForOwner(
120 focusDirection: FocusDirection?,
121 previouslyFocusedRect: Rect?
122 ): Boolean = onRequestFocusForOwner(focusDirection, previouslyFocusedRect)
123
124 /**
125 * Keeps track of which keys have received DOWN events without UP events – i.e. which keys are
126 * currently down. This is used to detect UP events for keys that aren't down and ignore them.
127 *
128 * This set is lazily initialized the first time a DOWN event is received for a key.
129 */
130 // TODO(b/307580000) Factor this state out into a class to manage key inputs.
131 private var keysCurrentlyDown: MutableLongSet? = null
132
133 /**
134 * The [Owner][androidx.compose.ui.node.Owner] calls this function when it gains focus. This
135 * informs the [focus manager][FocusOwnerImpl] that the [Owner][androidx.compose.ui.node.Owner]
136 * gained focus, and that it should propagate this focus to one of the focus modifiers in the
137 * component hierarchy.
138 *
139 * @param focusDirection the direction to search for the focus target.
140 * @param previouslyFocusedRect the bounds of the currently focused item.
141 * @return true, if a suitable [FocusTargetNode] was found and it took focus, false if no
142 * [FocusTargetNode] was found or if the focus search was cancelled.
143 */
144 override fun takeFocus(focusDirection: FocusDirection, previouslyFocusedRect: Rect?): Boolean {
145 return focusSearch(focusDirection, previouslyFocusedRect) {
146 it.requestFocus(focusDirection)
147 } ?: false
148 }
149
150 /**
151 * The [Owner][androidx.compose.ui.node.Owner] calls this function when it loses focus. This
152 * informs the [focus manager][FocusOwnerImpl] that the [Owner][androidx.compose.ui.node.Owner]
153 * lost focus, and that it should clear focus from all the focus modifiers in the component
154 * hierarchy.
155 */
156 override fun releaseFocus() {
157 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
158 rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
159 } else {
160 focusTransactionManager.withExistingTransaction {
161 rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
162 }
163 }
164 }
165
166 /**
167 * Call this function to set the focus to the root focus modifier.
168 *
169 * @param force: Whether we should forcefully clear focus regardless of whether we have any
170 * components that have captured focus.
171 *
172 * This could be used to clear focus when a user clicks on empty space outside a focusable
173 * component.
174 */
175 override fun clearFocus(force: Boolean) {
176 clearFocus(force, refreshFocusEvents = true, clearOwnerFocus = true, focusDirection = Exit)
177 }
178
179 override fun clearFocus(
180 force: Boolean,
181 refreshFocusEvents: Boolean,
182 clearOwnerFocus: Boolean,
183 focusDirection: FocusDirection
184 ): Boolean {
185 val clearedFocusSuccessfully =
186 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
187 if (!force) {
188 // Don't clear focus if an item on the focused path has a custom exit specified.
189 when (rootFocusNode.performCustomClearFocus(focusDirection)) {
190 Redirected,
191 Cancelled,
192 RedirectCancelled -> false
193 None -> clearFocus(force, refreshFocusEvents)
194 }
195 } else {
196 clearFocus(force, refreshFocusEvents)
197 }
198 } else {
199 focusTransactionManager.withNewTransaction(
200 onCancelled = {
201 return@withNewTransaction
202 }
203 ) {
204 if (!force) {
205 // Don't clear focus if an item on the focused path has a custom exit
206 // specified.
207 when (rootFocusNode.performCustomClearFocus(focusDirection)) {
208 Redirected,
209 Cancelled,
210 RedirectCancelled -> return@withNewTransaction false
211 None -> {
212 /* Do nothing. */
213 }
214 }
215 }
216 return@withNewTransaction rootFocusNode.clearFocus(force, refreshFocusEvents)
217 }
218 }
219
220 if (clearedFocusSuccessfully && clearOwnerFocus) {
221 onClearFocusForOwner.invoke()
222 }
223 return clearedFocusSuccessfully
224 }
225
226 private fun clearFocus(forced: Boolean = false, refreshFocusEvents: Boolean): Boolean {
227 if (activeFocusTargetNode == null) return true
228 if (isFocusCaptured && !forced) {
229 return false // Cannot clear focus if it's captured unless forced
230 }
231 val previousActiveFocusTargetNode = activeFocusTargetNode
232 activeFocusTargetNode = null
233 if (refreshFocusEvents && previousActiveFocusTargetNode != null) {
234 previousActiveFocusTargetNode.dispatchFocusCallbacks(
235 if (isFocusCaptured) Captured else Active,
236 Inactive
237 )
238 previousActiveFocusTargetNode.visitAncestors(Nodes.FocusTarget) {
239 it.dispatchFocusCallbacks(ActiveParent, Inactive)
240 }
241 }
242 return true
243 }
244
245 /**
246 * Moves focus in the specified direction.
247 *
248 * @return true if focus was moved successfully. false if the focused item is unchanged.
249 */
250 override fun moveFocus(focusDirection: FocusDirection): Boolean {
251 // First check to see if the focus should move within child Views
252 @OptIn(ExperimentalComposeUiApi::class)
253 if (ComposeUiFlags.isViewFocusFixEnabled && onMoveFocusInterop(focusDirection)) {
254 return true
255 }
256 var requestFocusSuccess: Boolean? = false
257 val generationBefore = focusTransactionManager.generation
258 val activeNodeBefore = activeFocusTargetNode
259 val focusSearchSuccess =
260 focusSearch(focusDirection, onFocusRectInterop()) {
261 requestFocusSuccess = it.requestFocus(focusDirection)
262 requestFocusSuccess ?: false
263 }
264 val generationAfter = focusTransactionManager.generation
265 if (
266 focusSearchSuccess == true &&
267 (generationBefore != generationAfter ||
268 (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled &&
269 activeNodeBefore !== activeFocusTargetNode))
270 ) {
271 // There was a successful requestFocus() during the focusSearch
272 return true
273 }
274
275 // If focus search was cancelled, or if focus search succeeded but request focus was
276 // cancelled, it implies that moveFocus() failed.
277 if (focusSearchSuccess == null || requestFocusSuccess == null) return false
278
279 // If focus search and request focus succeeded, move focus succeeded.
280 if (focusSearchSuccess == true && requestFocusSuccess == true) return true
281
282 // To wrap focus around, we clear focus and request initial focus.
283 if (focusDirection.is1dFocusSearch()) {
284 val clearFocus =
285 clearFocus(
286 force = false,
287 refreshFocusEvents = true,
288 clearOwnerFocus = false,
289 focusDirection = focusDirection
290 )
291 return clearFocus && takeFocus(focusDirection, previouslyFocusedRect = null)
292 }
293
294 // If we couldn't move focus within compose, we attempt to move focus within embedded views.
295 // We don't need this for 1D focus search because the wrap-around logic triggers a
296 // focus exit which will perform a focus search among the subviews.
297 @OptIn(ExperimentalComposeUiApi::class)
298 return !ComposeUiFlags.isViewFocusFixEnabled && onMoveFocusInterop(focusDirection)
299 }
300
301 override fun focusSearch(
302 focusDirection: FocusDirection,
303 focusedRect: Rect?,
304 onFound: (FocusTargetNode) -> Boolean
305 ): Boolean? {
306 val source =
307 findFocusTargetNode()?.also {
308 // Check if a custom focus traversal order is specified.
309 when (val customDest = it.customFocusSearch(focusDirection, onLayoutDirection())) {
310 Cancel -> return null
311 Redirect -> return findFocusTargetNode()?.let(onFound)
312 Default -> {
313 /* Do Nothing */
314 }
315 else -> return customDest.findFocusTargetNode(onFound)
316 }
317 }
318
319 return rootFocusNode.focusSearch(focusDirection, onLayoutDirection(), focusedRect) {
320 when (it) {
321 source -> false
322 rootFocusNode -> error("Focus search landed at the root.")
323 else -> onFound(it)
324 }
325 }
326 }
327
328 /** Dispatches a key event through the compose hierarchy. */
329 override fun dispatchKeyEvent(keyEvent: KeyEvent, onFocusedItem: () -> Boolean): Boolean {
330 trace("FocusOwnerImpl:dispatchKeyEvent") {
331 if (focusInvalidationManager.hasPendingInvalidation()) {
332 // Ignoring this to unblock b/346370327.
333 println("$FocusWarning: Dispatching key event while focus system is invalidated.")
334 return false
335 }
336 if (!validateKeyEvent(keyEvent)) return false
337
338 val activeFocusTarget = findFocusTargetNode()
339 val focusedKeyInputNode =
340 activeFocusTarget?.lastLocalKeyInputNode()
341 ?: activeFocusTarget?.nearestAncestorIncludingSelf(Nodes.KeyInput)?.node
342 ?: rootFocusNode.nearestAncestor(Nodes.KeyInput)?.node
343
344 focusedKeyInputNode?.traverseAncestorsIncludingSelf(
345 type = Nodes.KeyInput,
346 onPreVisit = { if (it.onPreKeyEvent(keyEvent)) return true },
347 onVisit = { if (onFocusedItem.invoke()) return true },
348 onPostVisit = { if (it.onKeyEvent(keyEvent)) return true }
349 )
350 return false
351 }
352 }
353
354 override fun dispatchInterceptedSoftKeyboardEvent(keyEvent: KeyEvent): Boolean {
355 if (focusInvalidationManager.hasPendingInvalidation()) {
356 // Ignoring this to unblock b/346370327.
357 println(
358 "$FocusWarning: Dispatching intercepted soft keyboard event while the focus system" +
359 " is invalidated."
360 )
361 return false
362 }
363
364 val focusedSoftKeyboardInterceptionNode =
365 rootFocusNode
366 .findActiveFocusNode()
367 ?.nearestAncestorIncludingSelf(Nodes.SoftKeyboardKeyInput)
368
369 focusedSoftKeyboardInterceptionNode?.traverseAncestorsIncludingSelf(
370 type = Nodes.SoftKeyboardKeyInput,
371 onPreVisit = { if (it.onPreInterceptKeyBeforeSoftKeyboard(keyEvent)) return true },
372 onVisit = { /* TODO(b/320510084): dispatch soft keyboard events to embedded views. */ },
373 onPostVisit = { if (it.onInterceptKeyBeforeSoftKeyboard(keyEvent)) return true }
374 )
375 return false
376 }
377
378 /** Dispatches a rotary scroll event through the compose hierarchy. */
379 override fun dispatchRotaryEvent(
380 event: RotaryScrollEvent,
381 onFocusedItem: () -> Boolean
382 ): Boolean {
383 if (focusInvalidationManager.hasPendingInvalidation()) {
384 // Ignoring this to unblock b/379289347.
385 println(
386 "$FocusWarning: Dispatching rotary event while the focus system is invalidated."
387 )
388 return false
389 }
390
391 val focusedRotaryInputNode =
392 findFocusTargetNode()?.nearestAncestorIncludingSelf(Nodes.RotaryInput)
393
394 focusedRotaryInputNode?.traverseAncestorsIncludingSelf(
395 type = Nodes.RotaryInput,
396 onPreVisit = { if (it.onPreRotaryScrollEvent(event)) return true },
397 onVisit = { if (onFocusedItem()) return true },
398 onPostVisit = { if (it.onRotaryScrollEvent(event)) return true }
399 )
400
401 return false
402 }
403
404 @OptIn(ExperimentalComposeUiApi::class)
405 override fun dispatchIndirectTouchEvent(
406 event: IndirectTouchEvent,
407 onFocusedItem: () -> Boolean
408 ): Boolean {
409 if (focusInvalidationManager.hasPendingInvalidation()) {
410 // Ignoring this to unblock b/379289347.
411 println(
412 "$FocusWarning: Dispatching indirect touch event while the focus system is invalidated."
413 )
414 return false
415 }
416
417 val focusedIndirectTouchInputNode =
418 findFocusTargetNode()?.nearestAncestorIncludingSelf(Nodes.IndirectTouchInput)
419 focusedIndirectTouchInputNode?.traverseAncestorsIncludingSelf(
420 type = Nodes.IndirectTouchInput,
421 onPreVisit = { if (it.onPreIndirectTouchEvent(event)) return true },
422 onVisit = { if (onFocusedItem()) return true },
423 onPostVisit = { if (it.onIndirectTouchEvent(event)) return true }
424 )
425
426 return false
427 }
428
429 override fun scheduleInvalidation(node: FocusTargetNode) {
430 focusInvalidationManager.scheduleInvalidation(node)
431 }
432
433 override fun scheduleInvalidation(node: FocusEventModifierNode) {
434 focusInvalidationManager.scheduleInvalidation(node)
435 }
436
437 override fun scheduleInvalidation(node: FocusPropertiesModifierNode) {
438 focusInvalidationManager.scheduleInvalidation(node)
439 }
440
441 override fun scheduleInvalidationForOwner() {
442 focusInvalidationManager.scheduleInvalidationForOwner()
443 }
444
445 /**
446 * At the end of the invalidations, we need to ensure that the focus system is in a valid state.
447 */
448 private fun invalidateOwnerFocusState() {
449 // If an active item is removed, we currently clear focus from the hierarchy. We don't
450 // clear focus from the root because that could cause initial focus logic to be re-run.
451 // Now that all the invalidations are complete, we run owner.clearFocus() if needed.
452 if (
453 (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled &&
454 activeFocusTargetNode == null) || rootFocusNode.focusState == Inactive
455 ) {
456 onClearFocusForOwner()
457 }
458 }
459
460 private inline fun <reified T : DelegatableNode> DelegatableNode.traverseAncestorsIncludingSelf(
461 type: NodeKind<T>,
462 onPreVisit: (T) -> Unit,
463 onVisit: () -> Unit,
464 onPostVisit: (T) -> Unit
465 ) {
466 val ancestors = ancestors(type)
467 ancestors?.fastForEachReversed(onPreVisit)
468 node.dispatchForKind(type, onPreVisit)
469 onVisit.invoke()
470 node.dispatchForKind(type, onPostVisit)
471 ancestors?.fastForEach(onPostVisit)
472 }
473
474 private inline fun <reified T : Any> DelegatableNode.nearestAncestorIncludingSelf(
475 type: NodeKind<T>
476 ): T? {
477 visitAncestors(type, includeSelf = true) {
478 return it
479 }
480 return null
481 }
482
483 /** Searches for the currently focused item, and returns its coordinates as a rect. */
484 override fun getFocusRect(): Rect? {
485 return findFocusTargetNode()?.focusRect()
486 }
487
488 private fun findFocusTargetNode(): FocusTargetNode? {
489 return rootFocusNode.findActiveFocusNode()
490 }
491
492 override val rootState: FocusState
493 get() = rootFocusNode.focusState
494
495 override val listeners: MutableObjectList<FocusListener> = MutableObjectList(1)
496
497 override var activeFocusTargetNode: FocusTargetNode? = null
498 set(value) {
499 val previousValue = field
500 field = value
501 if (value == null || previousValue !== value) isFocusCaptured = false
502 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
503 listeners.forEach { it.onFocusChanged(previousValue, value) }
504 }
505 }
506
507 override var isFocusCaptured: Boolean = false
508 set(value) {
509 requirePrecondition(!value || activeFocusTargetNode != null) {
510 "Cannot capture focus when the active focus target node is unset"
511 }
512 field = value
513 }
514
515 private fun DelegatableNode.lastLocalKeyInputNode(): Modifier.Node? {
516 var focusedKeyInputNode: Modifier.Node? = null
517 visitLocalDescendants(Nodes.FocusTarget or Nodes.KeyInput) { modifierNode ->
518 if (modifierNode.isKind(Nodes.FocusTarget)) return focusedKeyInputNode
519
520 focusedKeyInputNode = modifierNode
521 }
522 return focusedKeyInputNode
523 }
524
525 // TODO(b/307580000) Factor this out into a class to manage key inputs.
526 private fun validateKeyEvent(keyEvent: KeyEvent): Boolean {
527 val keyCode = keyEvent.key.keyCode
528 when (keyEvent.type) {
529 KeyDown -> {
530 // It's probably rare for more than 3 hardware keys to be pressed simultaneously.
531 val keysCurrentlyDown =
532 keysCurrentlyDown
533 ?: MutableLongSet(initialCapacity = 3).also { keysCurrentlyDown = it }
534 keysCurrentlyDown += keyCode
535 }
536 KeyUp -> {
537 if (keysCurrentlyDown?.contains(keyCode) != true) {
538 // An UP event for a key that was never DOWN is invalid, ignore it.
539 return false
540 }
541 keysCurrentlyDown?.remove(keyCode)
542 }
543 // Always process Unknown event types.
544 }
545 return true
546 }
547 }
548
549 /**
550 * focus search in the Android framework wraps around for 1D focus search, but not for 2D focus
551 * search. This is a helper function that can be used to determine whether we should wrap around or
552 * not.
553 */
is1dFocusSearchnull554 internal fun FocusDirection.is1dFocusSearch(): Boolean =
555 when (this) {
556 Next,
557 Previous -> true
558 else -> false
559 }
560