1 /*
<lambda>null2 * 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 androidx.compose.material3
18
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.animation.core.VectorConverter
22 import androidx.compose.animation.core.animate
23 import androidx.compose.material3.internal.AnchoredDraggableState
24 import androidx.compose.material3.internal.snapTo
25 import androidx.compose.material3.tokens.MotionSchemeKeyTokens
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.Stable
28 import androidx.compose.runtime.derivedStateOf
29 import androidx.compose.runtime.getValue
30 import androidx.compose.runtime.mutableStateOf
31 import androidx.compose.runtime.remember
32 import androidx.compose.runtime.saveable.Saver
33 import androidx.compose.runtime.saveable.rememberSaveable
34 import androidx.compose.runtime.setValue
35 import androidx.compose.ui.unit.Density
36 import androidx.compose.ui.unit.dp
37
38 @ExperimentalMaterial3ExpressiveApi
39 /** Possible values of [WideNavigationRailState]. */
40 enum class WideNavigationRailValue {
41 /** The state of the rail when it is collapsed. */
42 Collapsed,
43
44 /** The state of the rail when it is expanded. */
45 Expanded
46 }
47
48 /**
49 * A state object that can be hoisted to observe the wide navigation rail state. It allows for
50 * setting to the rail to be collapsed or expanded.
51 *
52 * @see rememberWideNavigationRailState to construct the default implementation.
53 */
54 @ExperimentalMaterial3ExpressiveApi
55 interface WideNavigationRailState {
56 /** Whether the state is currently animating */
57 val isAnimating: Boolean
58
59 /** Whether the rail is going to be expanded or not. */
60 val targetValue: WideNavigationRailValue
61
62 /** Whether the rail is currently expanded or not. */
63 val currentValue: WideNavigationRailValue
64
65 /** Expand the rail with animation and suspend until it fully expands. */
expandnull66 suspend fun expand()
67
68 /** Collapse the rail with animation and suspend until it fully collapses. */
69 suspend fun collapse()
70
71 /**
72 * Collapse the rail with animation if it's expanded, or expand it if it's collapsed, and
73 * suspend until it's set to its new state.
74 */
75 suspend fun toggle()
76
77 /**
78 * Set the state without any animation and suspend until it's set.
79 *
80 * @param targetValue the expanded boolean to set to
81 */
82 suspend fun snapTo(targetValue: WideNavigationRailValue)
83 }
84
85 /** Create and [remember] a [WideNavigationRailState]. */
86 @ExperimentalMaterial3ExpressiveApi
87 @Composable
88 fun rememberWideNavigationRailState(
89 initialValue: WideNavigationRailValue = WideNavigationRailValue.Collapsed
90 ): WideNavigationRailState {
91 // TODO: Load the motionScheme tokens from the component tokens file.
92 val animationSpec = MotionSchemeKeyTokens.DefaultSpatial.value<Float>()
93 return rememberSaveable(saver = WideNavigationRailStateImpl.Saver(animationSpec)) {
94 WideNavigationRailStateImpl(
95 initialValue = initialValue,
96 animationSpec = animationSpec,
97 )
98 }
99 }
100
101 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
102 internal val WideNavigationRailValue.isExpanded
103 get() = this == WideNavigationRailValue.Expanded
104
105 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
notnull106 internal operator fun WideNavigationRailValue.not(): WideNavigationRailValue {
107 return if (this == WideNavigationRailValue.Collapsed) {
108 WideNavigationRailValue.Expanded
109 } else {
110 WideNavigationRailValue.Collapsed
111 }
112 }
113
114 @ExperimentalMaterial3ExpressiveApi
115 internal class WideNavigationRailStateImpl(
116 var initialValue: WideNavigationRailValue,
117 private val animationSpec: AnimationSpec<Float>,
118 ) : WideNavigationRailState {
119
120 private val internalValue = if (initialValue.isExpanded) Expanded else Collapsed
121 private val internalState = Animatable(internalValue, Float.VectorConverter)
<lambda>null122 private val _currentVal = derivedStateOf {
123 if (internalState.value == Expanded) {
124 WideNavigationRailValue.Expanded
125 } else {
126 WideNavigationRailValue.Collapsed
127 }
128 }
129
130 override val isAnimating: Boolean
131 get() = internalState.isRunning
132
133 override val targetValue: WideNavigationRailValue
134 get() =
135 if (internalState.targetValue == Expanded) {
136 WideNavigationRailValue.Expanded
137 } else {
138 WideNavigationRailValue.Collapsed
139 }
140
141 override val currentValue: WideNavigationRailValue
142 get() = _currentVal.value
143
expandnull144 override suspend fun expand() {
145 internalState.animateTo(targetValue = Expanded, animationSpec = animationSpec)
146 }
147
collapsenull148 override suspend fun collapse() {
149 internalState.animateTo(targetValue = Collapsed, animationSpec = animationSpec)
150 }
151
togglenull152 override suspend fun toggle() {
153 internalState.animateTo(
154 targetValue = if (targetValue.isExpanded) Collapsed else Expanded,
155 animationSpec = animationSpec
156 )
157 }
158
snapTonull159 override suspend fun snapTo(targetValue: WideNavigationRailValue) {
160 val target = if (targetValue.isExpanded) Expanded else Collapsed
161 internalState.snapTo(target)
162 }
163
164 companion object {
165 private const val Collapsed = 0f
166 private const val Expanded = 1f
167
168 /** The default [Saver] implementation for [WideNavigationRailState]. */
Savernull169 fun Saver(
170 animationSpec: AnimationSpec<Float>,
171 ) =
172 Saver<WideNavigationRailState, WideNavigationRailValue>(
173 save = { it.targetValue },
<lambda>null174 restore = { WideNavigationRailStateImpl(it, animationSpec) }
175 )
176 }
177 }
178
179 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
180 internal class ModalWideNavigationRailState(
181 state: WideNavigationRailState,
182 density: Density,
183 val animationSpec: AnimationSpec<Float>,
<lambda>null184 ) : WideNavigationRailState by state {
185 internal val anchoredDraggableState: AnchoredDraggableState<WideNavigationRailValue> =
186 AnchoredDraggableState(
187 initialValue = state.targetValue,
188 positionalThreshold = { distance -> distance * 0.5f },
189 velocityThreshold = { with(density) { 400.dp.toPx() } },
190 animationSpec = { animationSpec },
191 )
192
193 /**
194 * The current value of the state.
195 *
196 * If no swipe or animation is in progress, this corresponds to the value the dismissible modal
197 * wide navigation rail is currently in. If a swipe or an animation is in progress, this
198 * corresponds to the value the rail was in before the swipe or animation started.
199 */
200 override val currentValue: WideNavigationRailValue
201 get() = anchoredDraggableState.currentValue
202
203 /**
204 * The target value of the dismissible modal wide navigation rail state.
205 *
206 * If a swipe is in progress, this is the value that the modal rail will animate to if the swipe
207 * finishes. If an animation is running, this is the target value of that animation. Finally, if
208 * no swipe or animation is in progress, this is the same as the [currentValue].
209 */
210 override val targetValue: WideNavigationRailValue
211 get() = anchoredDraggableState.targetValue
212
213 override val isAnimating: Boolean
214 get() = anchoredDraggableState.isAnimationRunning
215
216 override suspend fun expand() = animateTo(WideNavigationRailValue.Expanded)
217
218 override suspend fun collapse() = animateTo(WideNavigationRailValue.Collapsed)
219
220 override suspend fun toggle() {
221 animateTo(!targetValue)
222 }
223
224 override suspend fun snapTo(targetValue: WideNavigationRailValue) {
225 anchoredDraggableState.snapTo(targetValue)
226 }
227
228 /**
229 * Find the closest anchor taking into account the velocity and settle at it with an animation.
230 */
231 internal suspend fun settle(velocity: Float) {
232 anchoredDraggableState.settle(velocity)
233 }
234
235 /**
236 * The current position (in pixels) of the rail, or Float.NaN before the offset is initialized.
237 *
238 * @see [AnchoredDraggableState.offset] for more information.
239 */
240 val currentOffset: Float
241 get() = anchoredDraggableState.offset
242
243 private suspend fun animateTo(
244 targetValue: WideNavigationRailValue,
245 animationSpec: AnimationSpec<Float> = this.animationSpec,
246 velocity: Float = anchoredDraggableState.lastVelocity
247 ) {
248 anchoredDraggableState.anchoredDrag(targetValue = targetValue) { anchors, latestTarget ->
249 val targetOffset = anchors.positionOf(latestTarget)
250 if (!targetOffset.isNaN()) {
251 var prev = if (currentOffset.isNaN()) 0f else currentOffset
252 animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
253 // Our onDrag coerces the value within the bounds, but an animation may
254 // overshoot, for example a spring animation or an overshooting interpolator.
255 // We respect the user's intention and allow the overshoot, but still use
256 // DraggableState's drag for its mutex.
257 dragTo(value, velocity)
258 prev = value
259 }
260 }
261 }
262 }
263 }
264
265 @Stable
266 internal class RailPredictiveBackState {
267 var swipeEdgeMatchesRail by mutableStateOf(true)
268
updatenull269 fun update(
270 isSwipeEdgeLeft: Boolean,
271 isRtl: Boolean,
272 ) {
273 swipeEdgeMatchesRail = (isSwipeEdgeLeft && !isRtl) || (!isSwipeEdgeLeft && isRtl)
274 }
275 }
276