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