1 /*
<lambda>null2 * Copyright 2021 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.compose.runtime.collection.MutableVector
20 import androidx.compose.runtime.collection.mutableVectorOf
21 import androidx.compose.ui.ComposeUiFlags
22 import androidx.compose.ui.ExperimentalComposeUiApi
23 import androidx.compose.ui.InternalComposeUiApi
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.FocusRequester.Companion.Cancel
29 import androidx.compose.ui.focus.FocusRequester.Companion.Redirect
30 import androidx.compose.ui.focus.FocusStateImpl.Active
31 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
32 import androidx.compose.ui.focus.FocusStateImpl.Captured
33 import androidx.compose.ui.focus.FocusStateImpl.Inactive
34 import androidx.compose.ui.node.Nodes
35 import androidx.compose.ui.node.nearestAncestor
36 import androidx.compose.ui.node.observeReads
37 import androidx.compose.ui.node.requireLayoutNode
38 import androidx.compose.ui.node.requireOwner
39 import androidx.compose.ui.node.visitAncestors
40
41 /**
42 * This function performs the request focus action.
43 *
44 * Note: Do not call this directly, consider using [requestFocus], which will check if any custom
45 * focus [enter][FocusProperties.onEnter] and [exit][FocusProperties.onExit]
46 * [properties][FocusProperties] have been specified.
47 */
48 internal fun FocusTargetNode.performRequestFocus(): Boolean {
49 return if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
50 performRequestFocusOptimized()
51 } else {
52 performRequestFocusLegacy()
53 }
54 }
55
FocusTargetNodenull56 private fun FocusTargetNode.performRequestFocusOptimized(): Boolean {
57 val focusOwner = requireOwner().focusOwner
58 val previousActiveNode = focusOwner.activeFocusTargetNode
59 val previousFocusState = focusState
60 if (previousActiveNode === this) {
61 // Focus events should be sent again if focus is requested for an already focused node
62 dispatchFocusCallbacks(previousFocusState, previousFocusState)
63 return true
64 }
65
66 // Request owner focus if it doesn't already have focus
67 if (previousActiveNode == null && !requestFocusForOwner()) {
68 return false // Don't grant focus if requesting owner focus failed
69 }
70
71 // Find ancestor target and event nodes of the previous active target node
72 var previousAncestorTargetNodes: MutableVector<FocusTargetNode>? = null
73 if (previousActiveNode != null) {
74 previousAncestorTargetNodes = mutableVectorOf()
75 previousActiveNode.visitAncestors(Nodes.FocusTarget) { previousAncestorTargetNodes.add(it) }
76 }
77
78 // Diff the previous ancestor nodes with the ancestors of the new active target node.
79 // We also check if the previous active node is an ancestor of the new active node, in which
80 // case we don't need to clear focus from it.
81 var shouldClearFocusFromPreviousActiveNode = true
82 val ancestorTargetNodes = mutableVectorOf<FocusTargetNode>()
83 visitAncestors(Nodes.FocusTarget) {
84 val removed = previousAncestorTargetNodes?.remove(it)
85 if (removed == null || !removed) {
86 ancestorTargetNodes.add(it)
87 }
88 if (it === previousActiveNode) shouldClearFocusFromPreviousActiveNode = false
89 }
90
91 if (shouldClearFocusFromPreviousActiveNode) {
92 if (previousActiveNode?.clearFocus(refreshFocusEvents = true) == false) {
93 return false // Don't grant focus if clearing focus from the previous node was rejected
94 }
95 }
96
97 grantFocus()
98
99 // Notify ancestor target nodes of the previous active node that are no longer ActiveParent
100 // The ancestors are traversed in the reversed order to dispatch events top->down
101 previousAncestorTargetNodes?.forEachReversed {
102 // Check if focus was cleared or redirected in a previous focus change callback
103 if (focusOwner.activeFocusTargetNode !== this) {
104 // The focus request was redirected or cancelled in a previous focus change callback
105 return false
106 }
107 it.dispatchFocusCallbacks(ActiveParent, Inactive)
108 }
109
110 // Notify ancestor target nodes of the new active node that become ActiveParent
111 // The ancestors are traversed in the reversed order to dispatch events top->down
112 ancestorTargetNodes.forEachReversed {
113 // Check if focus was cleared or redirected in a previous focus change callback
114 if (focusOwner.activeFocusTargetNode !== this) {
115 // The focus request was redirected or cancelled in a previous focus change callback
116 return false
117 }
118 it.dispatchFocusCallbacks(
119 previousState = if (it === previousActiveNode) Active else Inactive,
120 newState = ActiveParent
121 )
122 }
123
124 // Check if focus was cleared or redirected in a previous focus change callback
125 if (focusOwner.activeFocusTargetNode !== this) {
126 // The focus request was redirected or cancelled in a previous focus change callback
127 return false
128 }
129
130 // Send events to the new active node
131 dispatchFocusCallbacks(previousFocusState, Active)
132
133 // Check if focus was cleared or redirected in a previous focus change callback
134 if (focusOwner.activeFocusTargetNode !== this) {
135 // The focus request was redirected or cancelled in a previous focus change callback
136 return false
137 }
138
139 @OptIn(ExperimentalComposeUiApi::class, InternalComposeUiApi::class)
140 if (ComposeUiFlags.isViewFocusFixEnabled && requireLayoutNode().getInteropView() == null) {
141 // This isn't an AndroidView, so we should be focused on this ComposeView
142 requireOwner().focusOwner.requestFocusForOwner(FocusDirection.Next, null)
143 }
144
145 return true
146 }
147
FocusTargetNodenull148 private fun FocusTargetNode.performRequestFocusLegacy(): Boolean {
149 val success =
150 when (focusState) {
151 Active,
152 Captured -> true
153 ActiveParent -> clearChildFocus() && grantFocus()
154 Inactive -> {
155 val parent = nearestAncestor(Nodes.FocusTarget)
156 if (parent != null) {
157 val prevState = parent.focusState
158 val success = parent.requestFocusForChild(this)
159 if (success && prevState !== parent.focusState) {
160 parent.dispatchFocusCallbacks()
161 }
162 success
163 } else {
164 requestFocusForOwner() && grantFocus()
165 }
166 }
167 }
168 if (success) {
169 @OptIn(ExperimentalComposeUiApi::class, InternalComposeUiApi::class)
170 if (ComposeUiFlags.isViewFocusFixEnabled && requireLayoutNode().getInteropView() == null) {
171 // This isn't an AndroidView, so we should be focused on this ComposeView
172 requireOwner().focusOwner.requestFocusForOwner(FocusDirection.Next, null)
173 }
174 dispatchFocusCallbacks()
175 }
176 return success
177 }
178
179 /**
180 * Deny requests to clear focus.
181 *
182 * This is used when a component wants to hold onto focus (eg. A phone number field with an invalid
183 * number.
184 *
185 * @return true if the focus was successfully captured. False otherwise.
186 */
captureFocusnull187 internal fun FocusTargetNode.captureFocus() =
188 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
189 when (focusState) {
190 Active -> {
191 requireOwner().focusOwner.isFocusCaptured = true
192 dispatchFocusCallbacks(Active, Captured)
193 true
194 }
195 Captured -> true
196 ActiveParent,
197 Inactive -> false
198 }
199 } else {
<lambda>null200 requireTransactionManager().withNewTransaction {
201 when (focusState) {
202 Active -> {
203 focusState = Captured
204 dispatchFocusCallbacks()
205 true
206 }
207 Captured -> true
208 ActiveParent,
209 Inactive -> false
210 }
211 }
212 }
213
214 /**
215 * When the node is in the [Captured] state, it rejects all requests to clear focus. Calling
216 * [freeFocus] puts the node in the [Active] state, where it is no longer preventing other nodes
217 * from requesting focus.
218 *
219 * @return true if the captured focus was released. False Otherwise.
220 */
freeFocusnull221 internal fun FocusTargetNode.freeFocus() =
222 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
223 when (focusState) {
224 Captured -> {
225 requireOwner().focusOwner.isFocusCaptured = false
226 dispatchFocusCallbacks(previousState = Captured, newState = Active)
227 true
228 }
229 Active -> true
230 ActiveParent,
231 Inactive -> false
232 }
233 } else {
<lambda>null234 requireTransactionManager().withNewTransaction {
235 when (focusState) {
236 Captured -> {
237 focusState = Active
238 dispatchFocusCallbacks()
239 true
240 }
241 Active -> true
242 ActiveParent,
243 Inactive -> false
244 }
245 }
246 }
247
248 /**
249 * This function clears focus from this node.
250 *
251 * Note: This function should only be called by a parent [focus node][FocusTargetNode] to clear
252 * focus from one of its child [focus node][FocusTargetNode]s. It does not change the state of the
253 * parent.
254 */
clearFocusnull255 internal fun FocusTargetNode.clearFocus(
256 forced: Boolean = false,
257 refreshFocusEvents: Boolean
258 ): Boolean =
259 when (focusState) {
260 Active -> {
261 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
262 requireOwner().focusOwner.activeFocusTargetNode = null
263 if (refreshFocusEvents)
264 dispatchFocusCallbacks(previousState = Active, newState = Inactive)
265 } else {
266 focusState = Inactive
267 if (refreshFocusEvents) dispatchFocusCallbacks()
268 }
269 true
270 }
271 /**
272 * If the node is [ActiveParent], we need to clear focus from the [Active] descendant first,
273 * before clearing focus from this node.
274 */
275 ActiveParent ->
276 if (clearChildFocus(forced, refreshFocusEvents)) {
277 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
278 if (refreshFocusEvents)
279 dispatchFocusCallbacks(previousState = ActiveParent, newState = Inactive)
280 } else {
281 focusState = Inactive
282 if (refreshFocusEvents) dispatchFocusCallbacks()
283 }
284 true
285 } else {
286 false
287 }
288
289 /** If the node is [Captured], deny requests to clear focus, except for a forced clear. */
290 Captured -> {
291 if (forced) {
292 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
293 requireOwner().focusOwner.activeFocusTargetNode = null
294 if (refreshFocusEvents)
295 dispatchFocusCallbacks(previousState = Captured, newState = Inactive)
296 } else {
297 focusState = Inactive
298 if (refreshFocusEvents) dispatchFocusCallbacks()
299 }
300 }
301 forced
302 }
303 /** Nothing to do if the node is not focused. */
304 Inactive -> true
305 }
306
307 /**
308 * This function grants focus to this node. Note: This is a private function that just changes the
309 * state of this node and does not affect any other nodes in the hierarchy.
310 */
FocusTargetNodenull311 private fun FocusTargetNode.grantFocus(): Boolean {
312 // When we grant focus to this node, we need to observe changes to the canFocus property.
313 // If canFocus is set to false, we need to clear focus.
314 observeReads { fetchFocusProperties() }
315 // No Focused Children, or we don't want to propagate focus to children.
316 when (focusState) {
317 Inactive,
318 ActiveParent -> {
319 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled)
320 requireOwner().focusOwner.activeFocusTargetNode = this
321 else focusState = Active
322 }
323 Active,
324 Captured -> {
325 /* Already focused. */
326 }
327 }
328 return true
329 }
330
331 /** This function clears any focus from the focused child. */
clearChildFocusnull332 private fun FocusTargetNode.clearChildFocus(
333 forced: Boolean = false,
334 refreshFocusEvents: Boolean = true
335 ): Boolean = activeChild?.clearFocus(forced, refreshFocusEvents) ?: true
336
337 /**
338 * Focusable children of this [focus node][FocusTargetNode] can use this function to request focus.
339 *
340 * @param childNode: The node that is requesting focus.
341 * @return true if focus was granted, false otherwise.
342 */
343 private fun FocusTargetNode.requestFocusForChild(childNode: FocusTargetNode): Boolean {
344
345 // Only this node's children can ask for focus.
346 if (childNode.nearestAncestor(Nodes.FocusTarget) != this) {
347 error("Non child node cannot request focus.")
348 }
349
350 return when (focusState) {
351 // If this node is [Active], it can give focus to the requesting child.
352 Active -> childNode.grantFocus().also { success -> if (success) focusState = ActiveParent }
353 // If this node is [ActiveParent] ie, one of the parent's descendants is [Active],
354 // remove focus from the currently focused child and grant it to the requesting child.
355 ActiveParent -> {
356 requireActiveChild()
357 clearChildFocus() && childNode.grantFocus()
358 }
359 // If this node is not [Active], we must gain focus first before granting it
360 // to the requesting child.
361 Inactive -> {
362 val focusParent = nearestAncestor(Nodes.FocusTarget)
363 when {
364 // If this node is the root, request focus from the compose owner.
365 focusParent == null && requestFocusForOwner() -> {
366 childNode.grantFocus().also { success ->
367 if (success) focusState = ActiveParent
368 }
369 }
370 // For non-root nodes, request focus for this node before the child.
371 // We request focus even if this is a deactivated node, as we will end up taking
372 // focus away and granting it to the child.
373 focusParent != null && focusParent.requestFocusForChild(this) -> {
374 requestFocusForChild(childNode).also { success ->
375 // Verify that focus state was granted to the child.
376 // If this child didn't take focus then we can end up in a situation where
377 // a deactivated parent is focused.
378 check(this.focusState == ActiveParent) { "Deactivated node is focused" }
379 if (success) focusParent.dispatchFocusCallbacks()
380 }
381 }
382
383 // Could not gain focus, so have no focus to give.
384 else -> false
385 }
386 }
387 // If this node is [Captured], decline requests from the children.
388 Captured -> false
389 }
390 }
391
requestFocusForOwnernull392 private fun FocusTargetNode.requestFocusForOwner(): Boolean {
393 return requireOwner().focusOwner.requestFocusForOwner(null, null)
394 }
395
FocusTargetNodenull396 private fun FocusTargetNode.requireActiveChild(): FocusTargetNode {
397 return requireNotNull(activeChild) { "ActiveParent with no focused child" }
398 }
399
400 internal enum class CustomDestinationResult {
401 None,
402 Cancelled,
403 Redirected,
404 RedirectCancelled
405 }
406
performCustomRequestFocusnull407 internal fun FocusTargetNode.performCustomRequestFocus(
408 focusDirection: FocusDirection
409 ): CustomDestinationResult {
410 when (focusState) {
411 Active,
412 Captured -> return None
413 ActiveParent -> return requireActiveChild().performCustomClearFocus(focusDirection)
414 Inactive -> {
415 val focusParent = nearestAncestor(Nodes.FocusTarget) ?: return None
416 return when (focusParent.focusState) {
417 Captured -> Cancelled
418 ActiveParent -> focusParent.performCustomRequestFocus(focusDirection)
419 Active -> focusParent.performCustomEnter(focusDirection)
420 Inactive ->
421 focusParent.performCustomRequestFocus(focusDirection).takeUnless { it == None }
422 ?: focusParent.performCustomEnter(focusDirection)
423 }
424 }
425 }
426 }
427
performCustomClearFocusnull428 internal fun FocusTargetNode.performCustomClearFocus(
429 focusDirection: FocusDirection
430 ): CustomDestinationResult =
431 when (focusState) {
432 Active,
433 Inactive -> None
434 Captured -> Cancelled
435 ActiveParent ->
436 requireActiveChild().performCustomClearFocus(focusDirection).takeUnless { it == None }
437 ?: performCustomExit(focusDirection)
438 }
439
performCustomEnternull440 private fun FocusTargetNode.performCustomEnter(
441 focusDirection: FocusDirection
442 ): CustomDestinationResult {
443 fetchCustomEnter(focusDirection) {
444 if (it === Cancel) return Cancelled else if (it === Redirect) return Redirected
445 return if (it.requestFocus()) Redirected else RedirectCancelled
446 }
447 return None
448 }
449
performCustomExitnull450 private fun FocusTargetNode.performCustomExit(
451 focusDirection: FocusDirection
452 ): CustomDestinationResult {
453 fetchCustomExit(focusDirection) {
454 if (it === Cancel) return Cancelled else if (it === Redirect) return Redirected
455 return if (it.requestFocus()) Redirected else RedirectCancelled
456 }
457 return None
458 }
459