1 /*
2 * Copyright 2024 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 com.android.photopicker.util
18
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.CompositionLocalProvider
21 import androidx.compose.runtime.DisposableEffect
22 import androidx.compose.runtime.LaunchedEffect
23 import androidx.compose.runtime.State
24 import androidx.compose.runtime.compositionLocalOf
25 import androidx.compose.runtime.derivedStateOf
26 import androidx.compose.runtime.getValue
27 import androidx.compose.runtime.mutableStateListOf
28 import androidx.compose.runtime.remember
29 import androidx.compose.runtime.rememberUpdatedState
30 import androidx.compose.runtime.snapshots.SnapshotStateList
31 import androidx.compose.ui.focus.FocusRequester
32 import androidx.compose.ui.platform.LocalFocusManager
33 import kotlinx.coroutines.CoroutineScope
34
35 /**
36 * Coordinates focus for any composables in [content] and determines which composable will get
37 * focus. [HierarchicalFocusCoordinator]s can be nested, and form a tree, with an implicit root.
38 * Focus-requiring components (i.e. components using [ActiveFocusListener] or
39 * [ActiveFocusRequester]) should only be in the leaf [HierarchicalFocusCoordinator]s, and there
40 * should be at most one per [HierarchicalFocusCoordinator]. For [HierarchicalFocusCoordinator]
41 * elements sharing a parent (or at the top level, sharing the implicit root parent), only one
42 * should have focus enabled. The selected [HierarchicalFocusCoordinator] is the one that has focus
43 * enabled for itself and all ancestors, it will pass focus to its focus-requiring component if it
44 * has one, or call FocusManager#clearFocus() otherwise. If no [HierarchicalFocusCoordinator] is
45 * selected, there will be no change on the focus state.
46 *
47 * Example usage:
48 *
49 * @param requiresFocus a function should return true when the [content] subtree of the composition
50 * is active and may requires the focus (and false when it's not). For example, a pager can
51 * enclose each page's content with a call to [HierarchicalFocusCoordinator], marking only the
52 * current page as requiring focus.
53 * @param content The content of this component.
54 */
55 @Composable
HierarchicalFocusCoordinatornull56 public fun HierarchicalFocusCoordinator(
57 requiresFocus: () -> Boolean,
58 content: @Composable () -> Unit,
59 ) {
60 val focusManager = LocalFocusManager.current
61 FocusComposableImpl(
62 requiresFocus,
63 onFocusChanged = { if (it) focusManager.clearFocus() },
64 content = content,
65 )
66 }
67
68 /**
69 * Use as part of a focus-requiring component to register a callback to be notified when the focus
70 * state changes.
71 *
72 * @param onFocusChanged callback to be invoked when the focus state changes, the parameter is the
73 * new state (if true, we are becoming active and should request focus).
74 */
75 @Composable
ActiveFocusListenernull76 public fun ActiveFocusListener(onFocusChanged: CoroutineScope.(Boolean) -> Unit) {
77 FocusComposableImpl(focusEnabled = { true }, onFocusChanged = onFocusChanged, content = {})
78 }
79
80 /**
81 * Use as part of a focus-requiring component to register a callback to automatically request focus
82 * when this component is active. Note that this may call requestFocus in the provided
83 * FocusRequester, so that focusRequester should be used in a .focusRequester modifier on a
84 * Composable that is part of the composition.
85 *
86 * @param focusRequester The associated [FocusRequester] to request focus on.
87 */
88 @Composable
ActiveFocusRequesternull89 public fun ActiveFocusRequester(focusRequester: FocusRequester) {
90 ActiveFocusListener { if (it) focusRequester.requestFocus() }
91 }
92
93 /**
94 * Creates, remembers and returns a new [FocusRequester], that will have .requestFocus called when
95 * the enclosing [HierarchicalFocusCoordinator] becomes active. Note that the location you call this
96 * is important, in particular, which [HierarchicalFocusCoordinator] is enclosing it. Also, this may
97 * call requestFocus in the returned FocusRequester, so that focusRequester should be used in a
98 * .focusRequester modifier on a Composable that is part of the composition.
99 */
100 @Composable
rememberActiveFocusRequesternull101 public fun rememberActiveFocusRequester() =
102 remember { FocusRequester() }.also { ActiveFocusRequester(it) }
103
104 /**
105 * Implements a node in the Focus control tree (either a [HierarchicalFocusCoordinator] or
106 * [ActiveFocusListener]). Each [FocusComposableImpl] maps to a [FocusNode] in our internal
107 * representation, this is used to:
108 * 1) Check that our parent is focused (or we have no explicit parent), to see if we can be focused.
109 * 2) See if we have children. If not, we are a leaf node and will forward focus status updates to
110 * the onFocusChanged callback.
111 */
112 @Composable
FocusComposableImplnull113 internal fun FocusComposableImpl(
114 focusEnabled: () -> Boolean,
115 onFocusChanged: CoroutineScope.(Boolean) -> Unit,
116 content: @Composable () -> Unit,
117 ) {
118 val updatedFocusEnabled by rememberUpdatedState(focusEnabled)
119 val parent by rememberUpdatedState(LocalFocusNodeParent.current)
120
121 // Node in our internal tree representation of the FocusComposableImpl
122 val node = remember {
123 FocusNode(
124 focused = derivedStateOf { (parent?.focused?.value ?: true) && updatedFocusEnabled() }
125 )
126 }
127
128 // Attach our node to our parent's (and remove if we leave the composition).
129 parent?.let {
130 DisposableEffect(it) {
131 it.children.add(node)
132
133 onDispose { it.children.remove(node) }
134 }
135 }
136
137 CompositionLocalProvider(LocalFocusNodeParent provides node, content = content)
138
139 // If we are a leaf node, forward events to the onFocusChanged callback
140 LaunchedEffect(node.focused.value) {
141 if (node.children.isEmpty()) {
142 onFocusChanged(node.focused.value)
143 }
144 }
145 }
146
147 // Internal class used to represent a node in our tree of focus-aware components.
148 internal class FocusNode(
149 val focused: State<Boolean>,
150 var children: SnapshotStateList<FocusNode> = mutableStateListOf(),
151 )
152
153 // Composition Local used to keep a tree of focus-aware nodes (either controller nodes or
154 // focus requesting nodes).
155 // Nodes will register into their parent (unless they are the top ones) when they enter the
156 // composition and are removed when they leave it.
<lambda>null157 internal val LocalFocusNodeParent = compositionLocalOf<FocusNode?> { null }
158