1 /*
<lambda>null2 * Copyright 2020 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.ui.ComposeUiFlags
20 import androidx.compose.ui.ExperimentalComposeUiApi
21 import androidx.compose.ui.focus.FocusDirection.Companion.Down
22 import androidx.compose.ui.focus.FocusDirection.Companion.Enter
23 import androidx.compose.ui.focus.FocusDirection.Companion.Exit
24 import androidx.compose.ui.focus.FocusDirection.Companion.Left
25 import androidx.compose.ui.focus.FocusDirection.Companion.Next
26 import androidx.compose.ui.focus.FocusDirection.Companion.Previous
27 import androidx.compose.ui.focus.FocusDirection.Companion.Right
28 import androidx.compose.ui.focus.FocusDirection.Companion.Up
29 import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
30 import androidx.compose.ui.focus.FocusRequester.Companion.Default
31 import androidx.compose.ui.focus.FocusRequester.Companion.Redirect
32 import androidx.compose.ui.focus.FocusStateImpl.Active
33 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
34 import androidx.compose.ui.focus.FocusStateImpl.Captured
35 import androidx.compose.ui.focus.FocusStateImpl.Inactive
36 import androidx.compose.ui.geometry.Rect
37 import androidx.compose.ui.layout.findRootCoordinates
38 import androidx.compose.ui.node.Nodes
39 import androidx.compose.ui.node.requireOwner
40 import androidx.compose.ui.node.visitAncestors
41 import androidx.compose.ui.node.visitChildren
42 import androidx.compose.ui.unit.LayoutDirection
43 import androidx.compose.ui.unit.LayoutDirection.Ltr
44 import androidx.compose.ui.unit.LayoutDirection.Rtl
45
46 /**
47 * Search up the component tree for any parent/parents that have specified a custom focus order.
48 * Allowing parents higher up the hierarchy to overwrite the focus order specified by their
49 * children.
50 *
51 * @param focusDirection the focus direction passed to [FocusManager.moveFocus] that triggered this
52 * focus search.
53 * @param layoutDirection the current system [LayoutDirection].
54 */
55 internal fun FocusTargetNode.customFocusSearch(
56 focusDirection: FocusDirection,
57 layoutDirection: LayoutDirection
58 ): FocusRequester {
59 val focusProperties = fetchFocusProperties()
60 return when (focusDirection) {
61 Next -> focusProperties.next
62 Previous -> focusProperties.previous
63 Up -> focusProperties.up
64 Down -> focusProperties.down
65 Left ->
66 when (layoutDirection) {
67 Ltr -> focusProperties.start
68 Rtl -> focusProperties.end
69 }.takeUnless { it === Default } ?: focusProperties.left
70 Right ->
71 when (layoutDirection) {
72 Ltr -> focusProperties.end
73 Rtl -> focusProperties.start
74 }.takeUnless { it === Default } ?: focusProperties.right
75 // TODO(b/183746982): add focus order API for "In" and "Out".
76 // Developers can to specify a custom "In" to specify which child should be visited when
77 // the user presses dPad center. (They can also redirect the "In" to some other item).
78 // Developers can specify a custom "Out" to specify which composable should take focus
79 // when the user presses the back button.
80 Enter,
81 Exit -> {
82 val scope = CancelIndicatingFocusBoundaryScope(focusDirection)
83 with(focusProperties) {
84 val focusTransactionManager = focusTransactionManager
85 val generationBefore = focusTransactionManager?.generation ?: 0
86 val focusOwner = requireOwner().focusOwner
87 val activeNodeBefore = focusOwner.activeFocusTargetNode
88 if (focusDirection == Enter) {
89 scope.onEnter()
90 } else {
91 scope.onExit()
92 }
93 val generationAfter = focusTransactionManager?.generation ?: 0
94 if (scope.isCanceled) {
95 Cancel
96 } else if (
97 generationBefore != generationAfter ||
98 (@OptIn(ExperimentalComposeUiApi::class)
99 ComposeUiFlags.isTrackFocusEnabled &&
100 activeNodeBefore !== focusOwner.activeFocusTargetNode)
101 ) {
102 Redirect
103 } else {
104 Default
105 }
106 }
107 }
108 else -> error("invalid FocusDirection")
109 }
110 }
111
112 /**
113 * Moves focus based on the requested focus direction.
114 *
115 * @param focusDirection The requested direction to move focus.
116 * @param layoutDirection Whether the layout is RTL or LTR.
117 * @param previouslyFocusedRect The bounds of the previously focused item.
118 * @param onFound This lambda is invoked if focus search finds the next focus node.
119 * @return if no focus node is found, we return false. If we receive a cancel, we return null
120 * otherwise we return the result of [onFound].
121 */
focusSearchnull122 internal fun FocusTargetNode.focusSearch(
123 focusDirection: FocusDirection,
124 layoutDirection: LayoutDirection,
125 previouslyFocusedRect: Rect?,
126 onFound: (FocusTargetNode) -> Boolean
127 ): Boolean? {
128 return when (focusDirection) {
129 Next,
130 Previous -> oneDimensionalFocusSearch(focusDirection, onFound)
131 Left,
132 Right,
133 Up,
134 Down -> twoDimensionalFocusSearch(focusDirection, previouslyFocusedRect, onFound)
135 Enter -> {
136 // we search among the children of the active item.
137 val direction =
138 when (layoutDirection) {
139 Rtl -> Left
140 Ltr -> Right
141 }
142 findActiveFocusNode()
143 ?.twoDimensionalFocusSearch(direction, previouslyFocusedRect, onFound)
144 }
145 Exit ->
146 findActiveFocusNode()?.findNonDeactivatedParent().let {
147 if (it == null || it == this) false else onFound.invoke(it)
148 }
149 else -> error("Focus search invoked with invalid FocusDirection $focusDirection")
150 }
151 }
152
153 /**
154 * Returns the bounding box of the focus layout area in the root or [Rect.Zero] if the FocusModifier
155 * has not had a layout.
156 */
focusRectnull157 internal fun FocusTargetNode.focusRect(): Rect =
158 coordinator?.let { it.findRootCoordinates().localBoundingBoxOf(it, clipBounds = false) }
159 ?: Rect.Zero
160
161 /** Whether this node should be considered when searching for the next item during a traversal. */
162 internal val FocusTargetNode.isEligibleForFocusSearch: Boolean
163 get() = coordinator?.layoutNode?.isPlaced == true && coordinator?.layoutNode?.isAttached == true
164
165 internal val FocusTargetNode.activeChild: FocusTargetNode?
166 get() {
167 if (!node.isAttached) return null
<lambda>null168 visitChildren(Nodes.FocusTarget) {
169 if (!it.node.isAttached) return@visitChildren
170 when (it.focusState) {
171 Active,
172 ActiveParent,
173 Captured -> return it
174 Inactive -> return@visitChildren
175 }
176 }
177 return null
178 }
179
findActiveFocusNodenull180 internal fun FocusTargetNode.findActiveFocusNode(): FocusTargetNode? {
181 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) {
182 val activeNode = requireOwner().focusOwner.activeFocusTargetNode
183 return if (activeNode != null && activeNode.isAttached) activeNode else null
184 } else {
185 when (focusState) {
186 Active,
187 Captured -> return this
188 ActiveParent -> {
189 visitChildren(Nodes.FocusTarget) { node ->
190 node.findActiveFocusNode()?.let {
191 return it
192 }
193 }
194 return null
195 }
196 Inactive -> return null
197 }
198 }
199 }
200
201 @Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
findNonDeactivatedParentnull202 private fun FocusTargetNode.findNonDeactivatedParent(): FocusTargetNode? {
203 visitAncestors(Nodes.FocusTarget) { if (it.fetchFocusProperties().canFocus) return it }
204 return null
205 }
206