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