1 /* <lambda>null2 * Copyright 2022 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.MutableScatterSet 20 import androidx.collection.mutableScatterSetOf 21 import androidx.compose.ui.ComposeUiFlags 22 import androidx.compose.ui.ExperimentalComposeUiApi 23 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent 24 import androidx.compose.ui.focus.FocusStateImpl.Inactive 25 import androidx.compose.ui.internal.checkPrecondition 26 import androidx.compose.ui.node.Nodes 27 import androidx.compose.ui.node.visitAncestors 28 import androidx.compose.ui.node.visitSelfAndChildren 29 import androidx.compose.ui.util.fastForEach 30 31 /** 32 * The [FocusInvalidationManager] allows us to schedule focus related nodes for invalidation. These 33 * nodes are invalidated after onApplyChanges. It does this by registering an onApplyChangesListener 34 * when nodes are scheduled for invalidation. 35 */ 36 internal class FocusInvalidationManager( 37 private val onRequestApplyChangesListener: (() -> Unit) -> Unit, 38 private val invalidateOwnerFocusState: () -> Unit, 39 private val rootFocusStateFetcher: () -> FocusState, 40 private val activeFocusTargetNodeFetcher: () -> FocusTargetNode? 41 ) { 42 private val focusTargetNodes = mutableScatterSetOf<FocusTargetNode>() 43 private val focusEventNodes = mutableScatterSetOf<FocusEventModifierNode>() 44 private val focusTargetNodesLegacy = mutableListOf<FocusTargetNode>() 45 private val focusEventNodesLegacy = mutableListOf<FocusEventModifierNode>() 46 private val focusPropertiesNodesLegacy = mutableListOf<FocusPropertiesModifierNode>() 47 private val focusTargetsWithInvalidatedFocusEventsLegacy = mutableListOf<FocusTargetNode>() 48 49 private var isInvalidationScheduled = false 50 51 fun scheduleInvalidation(node: FocusTargetNode) { 52 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) { 53 focusTargetNodes.scheduleInvalidation(node) 54 } else { 55 focusTargetNodesLegacy.scheduleInvalidationLegacy(node) 56 } 57 } 58 59 fun scheduleInvalidation(node: FocusEventModifierNode) { 60 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) { 61 focusEventNodes.scheduleInvalidation(node) 62 } else { 63 focusEventNodesLegacy.scheduleInvalidationLegacy(node) 64 } 65 } 66 67 fun scheduleInvalidation(node: FocusPropertiesModifierNode) { 68 focusPropertiesNodesLegacy.scheduleInvalidationLegacy(node) 69 } 70 71 fun scheduleInvalidationForOwner() { 72 setUpOnRequestApplyChangesListener() 73 } 74 75 fun hasPendingInvalidation(): Boolean { 76 return if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) { 77 isInvalidationScheduled 78 } else { 79 focusTargetNodesLegacy.isNotEmpty() || 80 focusPropertiesNodesLegacy.isNotEmpty() || 81 focusEventNodesLegacy.isNotEmpty() 82 } 83 } 84 85 private fun <T> MutableScatterSet<T>.scheduleInvalidation(node: T) { 86 if (add(node)) { 87 setUpOnRequestApplyChangesListener() 88 } 89 } 90 91 private fun setUpOnRequestApplyChangesListener() { 92 if (!isInvalidationScheduled) { 93 onRequestApplyChangesListener.invoke(::invalidateNodes) 94 isInvalidationScheduled = true 95 } 96 } 97 98 private fun <T> MutableList<T>.scheduleInvalidationLegacy(node: T) { 99 if (add(node)) { 100 // If this is the first node scheduled for invalidation, 101 // we set up a listener that runs after onApplyChanges. 102 if ( 103 focusTargetNodesLegacy.size + 104 focusEventNodesLegacy.size + 105 focusPropertiesNodesLegacy.size == 1 106 ) { 107 onRequestApplyChangesListener.invoke(::invalidateNodes) 108 } 109 } 110 } 111 112 private fun invalidateNodes() { 113 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isTrackFocusEnabled) { 114 invalidateNodesOptimized() 115 } else { 116 invalidateNodesLegacy() 117 } 118 } 119 120 private fun invalidateNodesOptimized() { 121 val activeFocusTargetNode = activeFocusTargetNodeFetcher() 122 if (activeFocusTargetNode == null) { 123 // If there is no active focus node, dispatch the Inactive state to event nodes. 124 focusEventNodes.forEach { it.onFocusEvent(Inactive) } 125 } else if (activeFocusTargetNode.isAttached) { 126 if (focusTargetNodes.contains(activeFocusTargetNode)) { 127 activeFocusTargetNode.invalidateFocus() 128 } 129 130 val activeFocusTargetNodeState = activeFocusTargetNode.focusState 131 var traversedFocusTargetCount = 0 132 activeFocusTargetNode.visitAncestors( 133 Nodes.FocusTarget or Nodes.FocusEvent, 134 includeSelf = true 135 ) { 136 // Keep track of whether we traversed past the first target node ancestor of the 137 // active focus target node, so that all the subsequent event nodes are sent the 138 // ActiveParent state rather than Active/Captured. 139 if (it.isKind(Nodes.FocusTarget)) traversedFocusTargetCount++ 140 141 // Don't send events to event nodes that were not invalidated. 142 if (it !is FocusEventModifierNode || !focusEventNodes.contains(it)) { 143 return@visitAncestors 144 } 145 146 // Event nodes that are between the active focus target and the first ancestor 147 // target receive the Active/Captured state, while the event nodes further up 148 // receive the ActiveParent state. 149 if (traversedFocusTargetCount <= 1) { 150 it.onFocusEvent(activeFocusTargetNodeState) 151 } else { 152 it.onFocusEvent(ActiveParent) 153 } 154 155 // Remove the event node from the list of invalidated nodes, so that we only send a 156 // single event per node. 157 focusEventNodes.remove(it) 158 } 159 160 // Send the Inactive state to the event nodes that are not in the active node ancestors. 161 focusEventNodes.forEach { it.onFocusEvent(Inactive) } 162 } 163 164 invalidateOwnerFocusState() 165 focusTargetNodes.clear() 166 focusEventNodes.clear() 167 isInvalidationScheduled = false 168 } 169 170 private fun invalidateNodesLegacy() { 171 if (!rootFocusStateFetcher().hasFocus) { 172 // If root doesn't have focus, skip full invalidation and default to the Inactive state. 173 focusEventNodesLegacy.fastForEach { it.onFocusEvent(Inactive) } 174 focusTargetNodesLegacy.fastForEach { node -> 175 if (node.isAttached && !node.isInitialized()) { 176 node.initializeFocusState(Inactive) 177 } 178 } 179 focusTargetNodesLegacy.clear() 180 focusEventNodesLegacy.clear() 181 focusPropertiesNodesLegacy.clear() 182 focusTargetsWithInvalidatedFocusEventsLegacy.clear() 183 invalidateOwnerFocusState() 184 return 185 } 186 187 // Process all the invalidated FocusProperties nodes. 188 focusPropertiesNodesLegacy.fastForEach { 189 // We don't need to invalidate a focus properties node if it was scheduled for 190 // invalidation earlier in the composition but was then removed. 191 if (!it.node.isAttached) return@fastForEach 192 193 it.visitSelfAndChildren(Nodes.FocusTarget) { focusTarget -> 194 focusTargetNodesLegacy.add(focusTarget) 195 } 196 } 197 focusPropertiesNodesLegacy.clear() 198 199 // Process all the focus events nodes. 200 focusEventNodesLegacy.fastForEach { focusEventNode -> 201 // When focus nodes are removed, the corresponding focus events are scheduled for 202 // invalidation. If the focus event was also removed, we don't need to invalidate it. 203 // We call onFocusEvent with the default value, just to make it easier for the user, 204 // so that they don't have to keep track of whether they caused a focused item to be 205 // removed (Which would cause it to lose focus). 206 if (!focusEventNode.node.isAttached) { 207 focusEventNode.onFocusEvent(Inactive) 208 return@fastForEach 209 } 210 211 var requiresUpdate = true 212 var aggregatedNode = false 213 var focusTarget: FocusTargetNode? = null 214 focusEventNode.visitSelfAndChildren(Nodes.FocusTarget) { 215 216 // If there are multiple focus targets associated with this focus event node, 217 // we need to calculate the aggregated state. 218 if (focusTarget != null) { 219 aggregatedNode = true 220 } 221 222 focusTarget = it 223 224 // If the associated focus node is already scheduled for invalidation, it will 225 // send an onFocusEvent if the invalidation causes a focus state change. 226 // However this onFocusEvent was invalidated, so we have to ensure that we call 227 // onFocusEvent even if the focus state didn't change. 228 if (it in focusTargetNodesLegacy) { 229 requiresUpdate = false 230 focusTargetsWithInvalidatedFocusEventsLegacy.add(it) 231 return@visitSelfAndChildren 232 } 233 } 234 235 if (requiresUpdate) { 236 focusEventNode.onFocusEvent( 237 if (aggregatedNode) { 238 focusEventNode.getFocusState() 239 } else { 240 focusTarget?.focusState ?: Inactive 241 } 242 ) 243 } 244 } 245 focusEventNodesLegacy.clear() 246 247 // Process all the focus target nodes. 248 focusTargetNodesLegacy.fastForEach { 249 // We don't need to invalidate the focus target if it was scheduled for invalidation 250 // earlier in the composition but was then removed. 251 if (!it.isAttached) return@fastForEach 252 253 val preInvalidationState = it.focusState 254 it.invalidateFocus() 255 if ( 256 preInvalidationState != it.focusState || 257 it in focusTargetsWithInvalidatedFocusEventsLegacy 258 ) { 259 it.dispatchFocusCallbacks() 260 } 261 } 262 focusTargetNodesLegacy.clear() 263 // Clear the set so we can reuse it 264 focusTargetsWithInvalidatedFocusEventsLegacy.clear() 265 266 invalidateOwnerFocusState() 267 268 checkPrecondition(focusPropertiesNodesLegacy.isEmpty()) { 269 "Unprocessed FocusProperties nodes" 270 } 271 checkPrecondition(focusEventNodesLegacy.isEmpty()) { "Unprocessed FocusEvent nodes" } 272 checkPrecondition(focusTargetNodesLegacy.isEmpty()) { "Unprocessed FocusTarget nodes" } 273 } 274 } 275