/* * Copyright 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.compose.material3 import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkHorizontally import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.material3.tokens.ExtendedFabPrimaryTokens import androidx.compose.material3.tokens.FabPrimaryLargeTokens import androidx.compose.material3.tokens.FabPrimarySmallTokens import androidx.compose.material3.tokens.FabPrimaryTokens import androidx.compose.material3.tokens.MotionTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp /** * Material Design floating action button. * * The FAB represents the most important action on a screen. It puts key actions within reach. * * ![FAB image](https://developer.android.com/images/reference/androidx/compose/material3/fab.png) * * FAB typically contains an icon, for a FAB with text and an icon, see * [ExtendedFloatingActionButton]. * * @sample androidx.compose.material3.samples.FloatingActionButtonSample * * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. See also: [Surface]. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this FAB. You can create and pass in your own `remember`ed instance to observe [Interaction]s * and customize the appearance / behavior of this FAB in different states. * @param content the content of this FAB, typically an [Icon] */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun FloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.shape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable () -> Unit, ) { Surface( onClick = onClick, modifier = modifier, shape = shape, color = containerColor, contentColor = contentColor, tonalElevation = elevation.tonalElevation(interactionSource = interactionSource).value, shadowElevation = elevation.shadowElevation(interactionSource = interactionSource).value, interactionSource = interactionSource, ) { CompositionLocalProvider(LocalContentColor provides contentColor) { // Adding the text style from [ExtendedFloatingActionButton] to all FAB variations. In // the majority of cases this will have no impact, because icons are expected, but if a // developer decides to put some short text to emulate an icon, (like "?") then it will // have the correct styling. ProvideTextStyle( MaterialTheme.typography.fromToken(ExtendedFabPrimaryTokens.LabelTextFont), ) { Box( modifier = Modifier .defaultMinSize( minWidth = FabPrimaryTokens.ContainerWidth, minHeight = FabPrimaryTokens.ContainerHeight, ), contentAlignment = Alignment.Center, ) { content() } } } } } /** * Material Design small floating action button. * * The FAB represents the most important action on a screen. It puts key actions within reach. * * ![Small FAB image](https://developer.android.com/images/reference/androidx/compose/material3/small-fab.png) * * @sample androidx.compose.material3.samples.SmallFloatingActionButtonSample * * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. See also: [Surface]. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this FAB. You can create and pass in your own `remember`ed instance to observe [Interaction]s * and customize the appearance / behavior of this FAB in different states. * @param content the content of this FAB, typically an [Icon] */ @Composable fun SmallFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.smallShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable () -> Unit, ) { FloatingActionButton( onClick = onClick, modifier = modifier.sizeIn( minWidth = FabPrimarySmallTokens.ContainerWidth, minHeight = FabPrimarySmallTokens.ContainerHeight, ), shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, content = content, ) } /** * Material Design large floating action button. * * The FAB represents the most important action on a screen. It puts key actions within reach. * * ![Large FAB image](https://developer.android.com/images/reference/androidx/compose/material3/large-fab.png) * * @sample androidx.compose.material3.samples.LargeFloatingActionButtonSample * * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. See also: [Surface]. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this FAB. You can create and pass in your own `remember`ed instance to observe [Interaction]s * and customize the appearance / behavior of this FAB in different states. * @param content the content of this FAB, typically an [Icon] */ @Composable fun LargeFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.largeShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable () -> Unit, ) { FloatingActionButton( onClick = onClick, modifier = modifier.sizeIn( minWidth = FabPrimaryLargeTokens.ContainerWidth, minHeight = FabPrimaryLargeTokens.ContainerHeight, ), shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, content = content, ) } /** * Material Design extended floating action button. * * Extended FABs help people take primary actions. They're wider than FABs to accommodate a text * label and larger target area. * * ![Extended FAB image](https://developer.android.com/images/reference/androidx/compose/material3/extended-fab.png) * * The other extended floating action button overload supports a text label and icon. * * @sample androidx.compose.material3.samples.ExtendedFloatingActionButtonTextSample * * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. See also: [Surface]. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this FAB. You can create and pass in your own `remember`ed instance to observe [Interaction]s * and customize the appearance / behavior of this FAB in different states. * @param content the content of this FAB, typically a [Text] label */ @Composable fun ExtendedFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.extendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable RowScope.() -> Unit, ) { FloatingActionButton( onClick = onClick, modifier = modifier, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) { Row( modifier = Modifier .sizeIn(minWidth = ExtendedFabMinimumWidth) .padding(horizontal = ExtendedFabTextPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) } } /** * Material Design extended floating action button. * * Extended FABs help people take primary actions. They're wider than FABs to accommodate a text * label and larger target area. * * ![Extended FAB image](https://developer.android.com/images/reference/androidx/compose/material3/extended-fab.png) * * The other extended floating action button overload is for FABs without an icon. * * Default content description for accessibility is extended from the extended fabs icon. For custom * behavior, you can provide your own via [Modifier.semantics]. * * @sample androidx.compose.material3.samples.ExtendedFloatingActionButtonSample * @sample androidx.compose.material3.samples.AnimatedExtendedFloatingActionButtonSample * * @param text label displayed inside this FAB * @param icon optional icon for this FAB, typically an [Icon] * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param expanded controls the expansion state of this FAB. In an expanded state, the FAB will show * both the [icon] and [text]. In a collapsed state, the FAB will show only the [icon]. * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. See also: [Surface]. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this FAB. You can create and pass in your own `remember`ed instance to observe [Interaction]s * and customize the appearance / behavior of this FAB in different states. */ @Composable fun ExtendedFloatingActionButton( text: @Composable () -> Unit, icon: @Composable () -> Unit, onClick: () -> Unit, modifier: Modifier = Modifier, expanded: Boolean = true, shape: Shape = FloatingActionButtonDefaults.extendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { FloatingActionButton( onClick = onClick, modifier = modifier, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) { val startPadding = if (expanded) ExtendedFabStartIconPadding else 0.dp val endPadding = if (expanded) ExtendedFabTextPadding else 0.dp Row( modifier = Modifier .sizeIn( minWidth = if (expanded) ExtendedFabMinimumWidth else FabPrimaryTokens.ContainerWidth ) .padding(start = startPadding, end = endPadding), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = if (expanded) Arrangement.Start else Arrangement.Center ) { icon() AnimatedVisibility( visible = expanded, enter = ExtendedFabExpandAnimation, exit = ExtendedFabCollapseAnimation, ) { Row(Modifier.clearAndSetSemantics {}) { Spacer(Modifier.width(ExtendedFabEndIconPadding)) text() } } } } } /** * Contains the default values used by [FloatingActionButton] */ object FloatingActionButtonDefaults { /** * The recommended size of the icon inside a [LargeFloatingActionButton]. */ val LargeIconSize = FabPrimaryLargeTokens.IconSize /** Default shape for a floating action button. */ val shape: Shape @Composable get() = FabPrimaryTokens.ContainerShape.toShape() /** Default shape for a small floating action button. */ val smallShape: Shape @Composable get() = FabPrimarySmallTokens.ContainerShape.toShape() /** Default shape for a large floating action button. */ val largeShape: Shape @Composable get() = FabPrimaryLargeTokens.ContainerShape.toShape() /** Default shape for an extended floating action button. */ val extendedFabShape: Shape @Composable get() = ExtendedFabPrimaryTokens.ContainerShape.toShape() /** Default container color for a floating action button. */ val containerColor: Color @Composable get() = FabPrimaryTokens.ContainerColor.toColor() /** * Creates a [FloatingActionButtonElevation] that represents the elevation of a * [FloatingActionButton] in different states. For use cases in which a less prominent * [FloatingActionButton] is possible consider the [loweredElevation]. * * @param defaultElevation the elevation used when the [FloatingActionButton] has no other * [Interaction]s. * @param pressedElevation the elevation used when the [FloatingActionButton] is pressed. * @param focusedElevation the elevation used when the [FloatingActionButton] is focused. * @param hoveredElevation the elevation used when the [FloatingActionButton] is hovered. */ @Composable fun elevation( defaultElevation: Dp = FabPrimaryTokens.ContainerElevation, pressedElevation: Dp = FabPrimaryTokens.PressedContainerElevation, focusedElevation: Dp = FabPrimaryTokens.FocusContainerElevation, hoveredElevation: Dp = FabPrimaryTokens.HoverContainerElevation, ): FloatingActionButtonElevation = FloatingActionButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, ) /** * Use this to create a [FloatingActionButton] with a lowered elevation for less emphasis. Use * [elevation] to get a normal [FloatingActionButton]. * * @param defaultElevation the elevation used when the [FloatingActionButton] has no other * [Interaction]s. * @param pressedElevation the elevation used when the [FloatingActionButton] is pressed. * @param focusedElevation the elevation used when the [FloatingActionButton] is focused. * @param hoveredElevation the elevation used when the [FloatingActionButton] is hovered. */ @Composable fun loweredElevation( defaultElevation: Dp = FabPrimaryTokens.LoweredContainerElevation, pressedElevation: Dp = FabPrimaryTokens.LoweredPressedContainerElevation, focusedElevation: Dp = FabPrimaryTokens.LoweredFocusContainerElevation, hoveredElevation: Dp = FabPrimaryTokens.LoweredHoverContainerElevation, ): FloatingActionButtonElevation = FloatingActionButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, ) /** * Use this to create a [FloatingActionButton] that represents the default elevation of a * [FloatingActionButton] used for [BottomAppBar] in different states. * * @param defaultElevation the elevation used when the [FloatingActionButton] has no other * [Interaction]s. * @param pressedElevation the elevation used when the [FloatingActionButton] is pressed. * @param focusedElevation the elevation used when the [FloatingActionButton] is focused. * @param hoveredElevation the elevation used when the [FloatingActionButton] is hovered. */ fun bottomAppBarFabElevation( defaultElevation: Dp = 0.dp, pressedElevation: Dp = 0.dp, focusedElevation: Dp = 0.dp, hoveredElevation: Dp = 0.dp ): FloatingActionButtonElevation = FloatingActionButtonElevation( defaultElevation, pressedElevation, focusedElevation, hoveredElevation ) } /** * Represents the tonal and shadow elevation for a floating action button in different states. * * See [FloatingActionButtonDefaults.elevation] for the default elevation used in a * [FloatingActionButton] and [ExtendedFloatingActionButton]. */ @Stable open class FloatingActionButtonElevation internal constructor( private val defaultElevation: Dp, private val pressedElevation: Dp, private val focusedElevation: Dp, private val hoveredElevation: Dp, ) { @Composable internal fun shadowElevation(interactionSource: InteractionSource): State { return animateElevation(interactionSource = interactionSource) } @Composable internal fun tonalElevation(interactionSource: InteractionSource): State { return animateElevation(interactionSource = interactionSource) } @Composable private fun animateElevation(interactionSource: InteractionSource): State { val interactions = remember { mutableStateListOf() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is HoverInteraction.Enter -> { interactions.add(interaction) } is HoverInteraction.Exit -> { interactions.remove(interaction.enter) } is FocusInteraction.Focus -> { interactions.add(interaction) } is FocusInteraction.Unfocus -> { interactions.remove(interaction.focus) } is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } } } } val interaction = interactions.lastOrNull() val target = when (interaction) { is PressInteraction.Press -> pressedElevation is HoverInteraction.Enter -> hoveredElevation is FocusInteraction.Focus -> focusedElevation else -> defaultElevation } val animatable = remember { Animatable(target, Dp.VectorConverter) } LaunchedEffect(target) { val lastInteraction = when (animatable.targetValue) { pressedElevation -> PressInteraction.Press(Offset.Zero) hoveredElevation -> HoverInteraction.Enter() focusedElevation -> FocusInteraction.Focus() else -> null } animatable.animateElevation( from = lastInteraction, to = interaction, target = target, ) } return animatable.asState() } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is FloatingActionButtonElevation) return false if (defaultElevation != other.defaultElevation) return false if (pressedElevation != other.pressedElevation) return false if (focusedElevation != other.focusedElevation) return false if (hoveredElevation != other.hoveredElevation) return false return true } override fun hashCode(): Int { var result = defaultElevation.hashCode() result = 31 * result + pressedElevation.hashCode() result = 31 * result + focusedElevation.hashCode() result = 31 * result + hoveredElevation.hashCode() return result } } private val ExtendedFabStartIconPadding = 16.dp private val ExtendedFabEndIconPadding = 12.dp private val ExtendedFabTextPadding = 20.dp private val ExtendedFabMinimumWidth = 80.dp private val ExtendedFabCollapseAnimation = fadeOut( animationSpec = tween( durationMillis = MotionTokens.DurationShort2.toInt(), easing = MotionTokens.EasingLinearCubicBezier, ) ) + shrinkHorizontally( animationSpec = tween( durationMillis = MotionTokens.DurationLong2.toInt(), easing = MotionTokens.EasingEmphasizedCubicBezier, ), shrinkTowards = Alignment.Start, ) private val ExtendedFabExpandAnimation = fadeIn( animationSpec = tween( durationMillis = MotionTokens.DurationShort4.toInt(), delayMillis = MotionTokens.DurationShort2.toInt(), easing = MotionTokens.EasingLinearCubicBezier, ), ) + expandHorizontally( animationSpec = tween( durationMillis = MotionTokens.DurationLong2.toInt(), easing = MotionTokens.EasingEmphasizedCubicBezier, ), expandFrom = Alignment.Start, )