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.foundation
18
19 import androidx.compose.foundation.interaction.FocusInteraction
20 import androidx.compose.foundation.interaction.Interaction
21 import androidx.compose.foundation.interaction.MutableInteractionSource
22 import androidx.compose.runtime.Stable
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.focus.FocusState
25 import androidx.compose.ui.focus.FocusTargetModifierNode
26 import androidx.compose.ui.focus.Focusability
27 import androidx.compose.ui.layout.LayoutCoordinates
28 import androidx.compose.ui.layout.LocalPinnableContainer
29 import androidx.compose.ui.layout.PinnableContainer
30 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
31 import androidx.compose.ui.node.DelegatingNode
32 import androidx.compose.ui.node.GlobalPositionAwareModifierNode
33 import androidx.compose.ui.node.ModifierNodeElement
34 import androidx.compose.ui.node.ObserverModifierNode
35 import androidx.compose.ui.node.SemanticsModifierNode
36 import androidx.compose.ui.node.TraversableNode
37 import androidx.compose.ui.node.currentValueOf
38 import androidx.compose.ui.node.findNearestAncestor
39 import androidx.compose.ui.node.invalidateSemantics
40 import androidx.compose.ui.node.observeReads
41 import androidx.compose.ui.platform.InspectorInfo
42 import androidx.compose.ui.relocation.bringIntoView
43 import androidx.compose.ui.semantics.SemanticsPropertyReceiver
44 import androidx.compose.ui.semantics.focused
45 import androidx.compose.ui.semantics.requestFocus
46 import kotlinx.coroutines.Job
47 import kotlinx.coroutines.launch
48
49 /**
50 * Configure component to be focusable via focus system or accessibility "focus" event.
51 *
52 * Add this modifier to the element to make it focusable within its bounds.
53 *
54 * @sample androidx.compose.foundation.samples.FocusableSample
55 * @param enabled Controls the enabled state. When `false`, element won't participate in the focus
56 * @param interactionSource [MutableInteractionSource] that will be used to emit
57 * [FocusInteraction.Focus] when this element is being focused.
58 */
59 @Stable
60 fun Modifier.focusable(
61 enabled: Boolean = true,
62 interactionSource: MutableInteractionSource? = null,
63 ) =
64 this.then(
65 if (enabled) {
66 FocusableElement(interactionSource)
67 } else {
68 Modifier
69 }
70 )
71
72 /**
73 * Creates a focus group or marks this component as a focus group. This means that when we move
74 * focus using the keyboard or programmatically using
75 * [FocusManager.moveFocus()][androidx.compose.ui.focus.FocusManager.moveFocus], the items within
76 * the focus group will be given a higher priority before focus moves to items outside the focus
77 * group.
78 *
79 * In the sample below, each column is a focus group, so pressing the tab key will move focus to all
80 * the buttons in column 1 before visiting column 2.
81 *
82 * @sample androidx.compose.foundation.samples.FocusGroupSample
83 *
84 * Note: The focusable children of a focusable parent automatically form a focus group. This
85 * modifier is to be used when you want to create a focus group where the parent is not focusable.
86 * If you encounter a component that uses a [focusGroup] internally, you can make it focusable by
87 * using a [focusable] modifier. In the second sample here, the
88 * [LazyRow][androidx.compose.foundation.lazy.LazyRow] is a focus group that is not itself
89 * focusable. But you can make it focusable by adding a [focusable] modifier.
90 *
91 * @sample androidx.compose.foundation.samples.FocusableFocusGroupSample
92 */
93 @Stable
focusGroupnull94 fun Modifier.focusGroup(): Modifier {
95 return this.then(FocusGroupElement)
96 }
97
98 private object FocusGroupElement : ModifierNodeElement<FocusGroupNode>() {
createnull99 override fun create() = FocusGroupNode()
100
101 override fun update(node: FocusGroupNode) {}
102
inspectablePropertiesnull103 override fun InspectorInfo.inspectableProperties() {
104 name = "focusGroup"
105 }
106
hashCodenull107 override fun hashCode() = "focusGroup".hashCode()
108
109 override fun equals(other: Any?) = other === this
110 }
111
112 private class FocusGroupNode : DelegatingNode() {
113 init {
114 delegate(FocusTargetModifierNode(focusability = Focusability.Never))
115 }
116 }
117
118 private class FocusableElement(private val interactionSource: MutableInteractionSource?) :
119 ModifierNodeElement<FocusableNode>() {
120
createnull121 override fun create(): FocusableNode = FocusableNode(interactionSource)
122
123 override fun update(node: FocusableNode) {
124 node.update(interactionSource)
125 }
126
equalsnull127 override fun equals(other: Any?): Boolean {
128 if (this === other) return true
129 if (other !is FocusableElement) return false
130
131 if (interactionSource != other.interactionSource) return false
132 return true
133 }
134
hashCodenull135 override fun hashCode(): Int {
136 return interactionSource.hashCode()
137 }
138
inspectablePropertiesnull139 override fun InspectorInfo.inspectableProperties() {
140 name = "focusable"
141 properties["enabled"] = true
142 properties["interactionSource"] = interactionSource
143 }
144 }
145
146 internal class FocusableNode(
147 private var interactionSource: MutableInteractionSource?,
148 focusability: Focusability = Focusability.Always,
149 private val onFocusChange: ((Boolean) -> Unit)? = null
150 ) :
151 DelegatingNode(),
152 SemanticsModifierNode,
153 GlobalPositionAwareModifierNode,
154 CompositionLocalConsumerModifierNode,
155 ObserverModifierNode,
156 TraversableNode {
157 override val shouldAutoInvalidate: Boolean = false
158
159 private companion object TraverseKey
160
161 override val traverseKey: Any
162 get() = TraverseKey
163
164 private var focusedInteraction: FocusInteraction.Focus? = null
165 private var pinnedHandle: PinnableContainer.PinnedHandle? = null
166 private var globalLayoutCoordinates: LayoutCoordinates? = null
167
168 private val focusTargetNode =
169 delegate(
170 FocusTargetModifierNode(
171 focusability = focusability,
172 onFocusChange = ::onFocusStateChange
173 )
174 )
175
176 private var requestFocus: (() -> Boolean)? = null
177
178 private val focusedBoundsObserver: FocusedBoundsObserverNode?
179 get() =
180 if (isAttached) {
181 findNearestAncestor(FocusedBoundsObserverNode.TraverseKey)
182 as? FocusedBoundsObserverNode
183 } else {
184 null
185 }
186
187 // Focusables have a few different cases where they need to make sure they stay visible:
188 //
189 // 1. Focusable node newly receives focus – always bring entire node into view. That's what this
190 // BringIntoViewRequester does.
191 // 2. Scrollable parent resizes and the currently-focused item is now hidden – bring entire node
192 // into view if it was also in view before the resize. This handles the case of
193 // `softInputMode=ADJUST_RESIZE`. See b/216842427.
194 // 3. Entire window is panned due to `softInputMode=ADJUST_PAN` – report the correct focused
195 // rect to the view system, and the view system itself will keep the focused area in view.
196 // See aosp/1964580.
197
updatenull198 fun update(interactionSource: MutableInteractionSource?) {
199 if (this.interactionSource != interactionSource) {
200 disposeInteractionSource()
201 this.interactionSource = interactionSource
202 }
203 }
204
onFocusStateChangenull205 private fun onFocusStateChange(previousState: FocusState, currentState: FocusState) {
206 if (!isAttached) return
207 val isFocused = currentState.isFocused
208 val wasFocused = previousState.isFocused
209 // Ignore cases where we are initialized as unfocused, or moving between different unfocused
210 // states, such as Inactive -> ActiveParent.
211 if (isFocused == wasFocused) return
212 onFocusChange?.invoke(isFocused)
213 if (isFocused) {
214 coroutineScope.launch { bringIntoView() }
215 val pinnableContainer = retrievePinnableContainer()
216 pinnedHandle = pinnableContainer?.pin()
217 notifyObserverWhenAttached()
218 } else {
219 pinnedHandle?.release()
220 pinnedHandle = null
221 focusedBoundsObserver?.onFocusBoundsChanged(null)
222 }
223 invalidateSemantics()
224 emitInteraction(isFocused)
225 }
226
applySemanticsnull227 override fun SemanticsPropertyReceiver.applySemantics() {
228 focused = focusTargetNode.focusState.isFocused
229 if (requestFocus == null) {
230 requestFocus = { focusTargetNode.requestFocus() }
231 }
232 requestFocus(action = requestFocus)
233 }
234
onResetnull235 override fun onReset() {
236 pinnedHandle?.release()
237 pinnedHandle = null
238 }
239
onObservedReadsChangednull240 override fun onObservedReadsChanged() {
241 val pinnableContainer = retrievePinnableContainer()
242 if (focusTargetNode.focusState.isFocused) {
243 pinnedHandle?.release()
244 pinnedHandle = pinnableContainer?.pin()
245 }
246 }
247
248 // TODO: b/276790428 move this to be lazily delegated when we are focused, we don't need to
249 // be notified of global position changes if we aren't focused.
onGloballyPositionednull250 override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
251 globalLayoutCoordinates = coordinates
252 if (!focusTargetNode.focusState.isFocused) return
253 if (coordinates.isAttached) {
254 notifyObserverWhenAttached()
255 } else {
256 focusedBoundsObserver?.onFocusBoundsChanged(null)
257 }
258 }
259
retrievePinnableContainernull260 private fun retrievePinnableContainer(): PinnableContainer? {
261 var container: PinnableContainer? = null
262 observeReads { container = currentValueOf(LocalPinnableContainer) }
263 return container
264 }
265
notifyObserverWhenAttachednull266 private fun notifyObserverWhenAttached() {
267 if (globalLayoutCoordinates != null && globalLayoutCoordinates!!.isAttached) {
268 focusedBoundsObserver?.onFocusBoundsChanged(globalLayoutCoordinates)
269 }
270 }
271
emitInteractionnull272 private fun emitInteraction(isFocused: Boolean) {
273 interactionSource?.let { interactionSource ->
274 if (isFocused) {
275 focusedInteraction?.let { oldValue ->
276 val interaction = FocusInteraction.Unfocus(oldValue)
277 interactionSource.emitWithFallback(interaction)
278 focusedInteraction = null
279 }
280 val interaction = FocusInteraction.Focus()
281 interactionSource.emitWithFallback(interaction)
282 focusedInteraction = interaction
283 } else {
284 focusedInteraction?.let { oldValue ->
285 val interaction = FocusInteraction.Unfocus(oldValue)
286 interactionSource.emitWithFallback(interaction)
287 focusedInteraction = null
288 }
289 }
290 }
291 }
292
disposeInteractionSourcenull293 private fun disposeInteractionSource() {
294 interactionSource?.let { interactionSource ->
295 focusedInteraction?.let { oldValue ->
296 val interaction = FocusInteraction.Unfocus(oldValue)
297 interactionSource.tryEmit(interaction)
298 }
299 }
300 focusedInteraction = null
301 }
302
MutableInteractionSourcenull303 private fun MutableInteractionSource.emitWithFallback(interaction: Interaction) {
304 if (isAttached) {
305 // If this is being called from inside FocusTargetNode's onDetach(), we are still
306 // attached, but the scope will be cancelled soon after - so the launch {} might not
307 // even start before it is cancelled. We don't want to use CoroutineStart.UNDISPATCHED,
308 // or always call tryEmit() as this will break other timing / cause some events to be
309 // missed for other cases. Instead just make sure we call tryEmit if we cancel the
310 // scope, before we finish emitting.
311 val handler =
312 coroutineScope.coroutineContext[Job]?.invokeOnCompletion { tryEmit(interaction) }
313 coroutineScope.launch {
314 emit(interaction)
315 handler?.dispose()
316 }
317 } else {
318 tryEmit(interaction)
319 }
320 }
321 }
322