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 * 
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 * 
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