1 /*
<lambda>null2  * Copyright 2019 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.material
18 
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.animation.core.VectorConverter
21 import androidx.compose.foundation.interaction.FocusInteraction
22 import androidx.compose.foundation.interaction.HoverInteraction
23 import androidx.compose.foundation.interaction.Interaction
24 import androidx.compose.foundation.interaction.InteractionSource
25 import androidx.compose.foundation.interaction.MutableInteractionSource
26 import androidx.compose.foundation.interaction.PressInteraction
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.Row
29 import androidx.compose.foundation.layout.Spacer
30 import androidx.compose.foundation.layout.defaultMinSize
31 import androidx.compose.foundation.layout.padding
32 import androidx.compose.foundation.layout.sizeIn
33 import androidx.compose.foundation.layout.width
34 import androidx.compose.foundation.shape.CornerSize
35 import androidx.compose.runtime.Composable
36 import androidx.compose.runtime.CompositionLocalProvider
37 import androidx.compose.runtime.LaunchedEffect
38 import androidx.compose.runtime.Stable
39 import androidx.compose.runtime.State
40 import androidx.compose.runtime.remember
41 import androidx.compose.ui.Alignment
42 import androidx.compose.ui.Modifier
43 import androidx.compose.ui.graphics.Color
44 import androidx.compose.ui.graphics.Shape
45 import androidx.compose.ui.semantics.Role
46 import androidx.compose.ui.semantics.role
47 import androidx.compose.ui.semantics.semantics
48 import androidx.compose.ui.unit.Dp
49 import androidx.compose.ui.unit.dp
50 import kotlinx.coroutines.launch
51 
52 /**
53  * [Material Design floating action
54  * button](https://material.io/components/buttons-floating-action-button)
55  *
56  * A floating action button (FAB) represents the primary action of a screen.
57  *
58  * ![Floating action button
59  * image](https://developer.android.com/images/reference/androidx/compose/material/floating-action-button.png)
60  *
61  * This FAB is typically used with an [Icon]:
62  *
63  * @sample androidx.compose.material.samples.SimpleFab
64  *
65  * See [ExtendedFloatingActionButton] for an extended FAB that contains text and an optional icon.
66  *
67  * @param onClick callback invoked when this FAB is clicked
68  * @param modifier [Modifier] to be applied to this FAB.
69  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
70  *   emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or
71  *   preview the FAB in different states. Note that if `null` is provided, interactions will still
72  *   happen internally.
73  * @param shape The [Shape] of this FAB
74  * @param backgroundColor The background color. Use [Color.Transparent] to have no color
75  * @param contentColor The preferred content color for content inside this FAB
76  * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in
77  *   different states. This controls the size of the shadow below the FAB.
78  * @param content the content of this FAB - this is typically an [Icon].
79  */
80 @OptIn(ExperimentalMaterialApi::class)
81 @Composable
82 fun FloatingActionButton(
83     onClick: () -> Unit,
84     modifier: Modifier = Modifier,
85     interactionSource: MutableInteractionSource? = null,
86     shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
87     backgroundColor: Color = MaterialTheme.colors.secondary,
88     contentColor: Color = contentColorFor(backgroundColor),
89     elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
90     content: @Composable () -> Unit
91 ) {
92     @Suppress("NAME_SHADOWING")
93     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
94     Surface(
95         onClick = onClick,
96         modifier = modifier.semantics { role = Role.Button },
97         shape = shape,
98         color = backgroundColor,
99         contentColor = contentColor,
100         elevation = elevation.elevation(interactionSource).value,
101         interactionSource = interactionSource
102     ) {
103         CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
104             ProvideTextStyle(MaterialTheme.typography.button) {
105                 Box(
106                     modifier = Modifier.defaultMinSize(minWidth = FabSize, minHeight = FabSize),
107                     contentAlignment = Alignment.Center
108                 ) {
109                     content()
110                 }
111             }
112         }
113     }
114 }
115 
116 /**
117  * [Material Design extended floating action
118  * button](https://material.io/components/buttons-floating-action-button#extended-fab)
119  *
120  * The extended FAB is wider than a regular FAB, and it includes a text label.
121  *
122  * ![Extended floating action button
123  * image](https://developer.android.com/images/reference/androidx/compose/material/extended-floating-action-button.png)
124  *
125  * This extended FAB contains text and an optional icon that will be placed at the start. See
126  * [FloatingActionButton] for a FAB that just contains some content, typically an icon.
127  *
128  * @sample androidx.compose.material.samples.SimpleExtendedFabWithIcon
129  *
130  * If you want FAB’s container to have a fluid width (to be defined by its relationship to something
131  * else on screen, such as screen width or the layout grid) just apply an appropriate modifier. For
132  * example to fill the whole available width you can do:
133  *
134  * @sample androidx.compose.material.samples.FluidExtendedFab
135  * @param text Text label displayed inside this FAB
136  * @param onClick callback invoked when this FAB is clicked
137  * @param modifier [Modifier] to be applied to this FAB
138  * @param icon Optional icon for this FAB, typically this will be a [Icon].
139  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
140  *   emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or
141  *   preview the FAB in different states. Note that if `null` is provided, interactions will still
142  *   happen internally.
143  * @param shape The [Shape] of this FAB
144  * @param backgroundColor The background color. Use [Color.Transparent] to have no color
145  * @param contentColor The preferred content color. Will be used by text and iconography
146  * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in
147  *   different states. This controls the size of the shadow below the FAB.
148  */
149 @Composable
ExtendedFloatingActionButtonnull150 fun ExtendedFloatingActionButton(
151     text: @Composable () -> Unit,
152     onClick: () -> Unit,
153     modifier: Modifier = Modifier,
154     icon: @Composable (() -> Unit)? = null,
155     interactionSource: MutableInteractionSource? = null,
156     shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
157     backgroundColor: Color = MaterialTheme.colors.secondary,
158     contentColor: Color = contentColorFor(backgroundColor),
159     elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation()
160 ) {
161     FloatingActionButton(
162         modifier = modifier.sizeIn(minWidth = ExtendedFabSize, minHeight = ExtendedFabSize),
163         onClick = onClick,
164         interactionSource = interactionSource,
165         shape = shape,
166         backgroundColor = backgroundColor,
167         contentColor = contentColor,
168         elevation = elevation
169     ) {
170         val startPadding = if (icon == null) ExtendedFabTextPadding else ExtendedFabIconPadding
171         Row(
172             modifier = Modifier.padding(start = startPadding, end = ExtendedFabTextPadding),
173             verticalAlignment = Alignment.CenterVertically
174         ) {
175             if (icon != null) {
176                 icon()
177                 Spacer(Modifier.width(ExtendedFabIconPadding))
178             }
179             text()
180         }
181     }
182 }
183 
184 /**
185  * Represents the elevation for a floating action button in different states.
186  *
187  * See [FloatingActionButtonDefaults.elevation] for the default elevation used in a
188  * [FloatingActionButton] and [ExtendedFloatingActionButton].
189  */
190 @Stable
191 interface FloatingActionButtonElevation {
192     /**
193      * Represents the elevation used in a floating action button, depending on [interactionSource].
194      *
195      * @param interactionSource the [InteractionSource] for this floating action button
196      */
elevationnull197     @Composable fun elevation(interactionSource: InteractionSource): State<Dp>
198 }
199 
200 /** Contains the default values used by [FloatingActionButton] */
201 object FloatingActionButtonDefaults {
202     /**
203      * Creates a [FloatingActionButtonElevation] that will animate between the provided values
204      * according to the Material specification.
205      *
206      * @param defaultElevation the elevation to use when the [FloatingActionButton] has no
207      *   [Interaction]s
208      * @param pressedElevation the elevation to use when the [FloatingActionButton] is pressed.
209      */
210     @Deprecated("Use another overload of elevation", level = DeprecationLevel.HIDDEN)
211     @Composable
212     fun elevation(
213         defaultElevation: Dp = 6.dp,
214         pressedElevation: Dp = 12.dp,
215     ): FloatingActionButtonElevation =
216         elevation(
217             defaultElevation,
218             pressedElevation,
219             hoveredElevation = 8.dp,
220             focusedElevation = 8.dp,
221         )
222 
223     /**
224      * Creates a [FloatingActionButtonElevation] that will animate between the provided values
225      * according to the Material specification.
226      *
227      * @param defaultElevation the elevation to use when the [FloatingActionButton] has no
228      *   [Interaction]s
229      * @param pressedElevation the elevation to use when the [FloatingActionButton] is pressed.
230      * @param hoveredElevation the elevation to use when the [FloatingActionButton] is hovered.
231      * @param focusedElevation the elevation to use when the [FloatingActionButton] is focused.
232      */
233     @Composable
234     fun elevation(
235         defaultElevation: Dp = 6.dp,
236         pressedElevation: Dp = 12.dp,
237         hoveredElevation: Dp = 8.dp,
238         focusedElevation: Dp = 8.dp,
239     ): FloatingActionButtonElevation {
240         return remember(defaultElevation, pressedElevation, hoveredElevation, focusedElevation) {
241             DefaultFloatingActionButtonElevation(
242                 defaultElevation = defaultElevation,
243                 pressedElevation = pressedElevation,
244                 hoveredElevation = hoveredElevation,
245                 focusedElevation = focusedElevation
246             )
247         }
248     }
249 }
250 
251 /** Default [FloatingActionButtonElevation] implementation. */
252 @Stable
253 private class DefaultFloatingActionButtonElevation(
254     private val defaultElevation: Dp,
255     private val pressedElevation: Dp,
256     private val hoveredElevation: Dp,
257     private val focusedElevation: Dp
258 ) : FloatingActionButtonElevation {
259     @Composable
elevationnull260     override fun elevation(interactionSource: InteractionSource): State<Dp> {
261         val animatable =
262             remember(interactionSource) {
263                 FloatingActionButtonElevationAnimatable(
264                     defaultElevation = defaultElevation,
265                     pressedElevation = pressedElevation,
266                     hoveredElevation = hoveredElevation,
267                     focusedElevation = focusedElevation
268                 )
269             }
270 
271         LaunchedEffect(this) {
272             animatable.updateElevation(
273                 defaultElevation = defaultElevation,
274                 pressedElevation = pressedElevation,
275                 hoveredElevation = hoveredElevation,
276                 focusedElevation = focusedElevation
277             )
278         }
279 
280         LaunchedEffect(interactionSource) {
281             val interactions = mutableListOf<Interaction>()
282             interactionSource.interactions.collect { interaction ->
283                 when (interaction) {
284                     is HoverInteraction.Enter -> {
285                         interactions.add(interaction)
286                     }
287                     is HoverInteraction.Exit -> {
288                         interactions.remove(interaction.enter)
289                     }
290                     is FocusInteraction.Focus -> {
291                         interactions.add(interaction)
292                     }
293                     is FocusInteraction.Unfocus -> {
294                         interactions.remove(interaction.focus)
295                     }
296                     is PressInteraction.Press -> {
297                         interactions.add(interaction)
298                     }
299                     is PressInteraction.Release -> {
300                         interactions.remove(interaction.press)
301                     }
302                     is PressInteraction.Cancel -> {
303                         interactions.remove(interaction.press)
304                     }
305                 }
306                 val targetInteraction = interactions.lastOrNull()
307                 launch { animatable.animateElevation(to = targetInteraction) }
308             }
309         }
310 
311         return animatable.asState()
312     }
313 
equalsnull314     override fun equals(other: Any?): Boolean {
315         if (this === other) return true
316         if (other !is DefaultFloatingActionButtonElevation) return false
317 
318         if (defaultElevation != other.defaultElevation) return false
319         if (pressedElevation != other.pressedElevation) return false
320         if (hoveredElevation != other.hoveredElevation) return false
321         return focusedElevation == other.focusedElevation
322     }
323 
hashCodenull324     override fun hashCode(): Int {
325         var result = defaultElevation.hashCode()
326         result = 31 * result + pressedElevation.hashCode()
327         result = 31 * result + hoveredElevation.hashCode()
328         result = 31 * result + focusedElevation.hashCode()
329         return result
330     }
331 }
332 
333 private class FloatingActionButtonElevationAnimatable(
334     private var defaultElevation: Dp,
335     private var pressedElevation: Dp,
336     private var hoveredElevation: Dp,
337     private var focusedElevation: Dp
338 ) {
339     private val animatable = Animatable(defaultElevation, Dp.VectorConverter)
340 
341     private var lastTargetInteraction: Interaction? = null
342     private var targetInteraction: Interaction? = null
343 
Interactionnull344     private fun Interaction?.calculateTarget(): Dp {
345         return when (this) {
346             is PressInteraction.Press -> pressedElevation
347             is HoverInteraction.Enter -> hoveredElevation
348             is FocusInteraction.Focus -> focusedElevation
349             else -> defaultElevation
350         }
351     }
352 
updateElevationnull353     suspend fun updateElevation(
354         defaultElevation: Dp,
355         pressedElevation: Dp,
356         hoveredElevation: Dp,
357         focusedElevation: Dp
358     ) {
359         this.defaultElevation = defaultElevation
360         this.pressedElevation = pressedElevation
361         this.hoveredElevation = hoveredElevation
362         this.focusedElevation = focusedElevation
363         snapElevation()
364     }
365 
snapElevationnull366     private suspend fun snapElevation() {
367         val target = targetInteraction.calculateTarget()
368         if (animatable.targetValue != target) {
369             try {
370                 animatable.snapTo(target)
371             } finally {
372                 lastTargetInteraction = targetInteraction
373             }
374         }
375     }
376 
animateElevationnull377     suspend fun animateElevation(to: Interaction?) {
378         val target = to.calculateTarget()
379         // Update the interaction even if the values are the same, for when we change to another
380         // interaction later
381         targetInteraction = to
382         try {
383             if (animatable.targetValue != target) {
384                 animatable.animateElevation(target = target, from = lastTargetInteraction, to = to)
385             }
386         } finally {
387             lastTargetInteraction = to
388         }
389     }
390 
asStatenull391     fun asState(): State<Dp> = animatable.asState()
392 }
393 
394 private val FabSize = 56.dp
395 private val ExtendedFabSize = 48.dp
396 private val ExtendedFabIconPadding = 12.dp
397 private val ExtendedFabTextPadding = 20.dp
398