/*
* Copyright 2019 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.material
import androidx.compose.animation.AnimatedValueModel
import androidx.compose.animation.VectorConverter
import androidx.compose.animation.asDisposableClock
import androidx.compose.animation.core.AnimationClockObservable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
import androidx.compose.foundation.ProvideTextStyle
import androidx.compose.foundation.AmbientIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSizeConstraints
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredSizeIn
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.AnimationClockAmbient
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* A floating action button (FAB) is a button that represents the primary action of a screen.
*
* This FAB is typically used with an [Icon]:
*
* @sample androidx.compose.material.samples.SimpleFab
*
* See [ExtendedFloatingActionButton] for an extended FAB that contains text and an optional icon.
*
* @param onClick will be called when user clicked on this FAB. The FAB will be disabled
* when it is null.
* @param modifier [Modifier] to be applied to this FAB.
* @param interactionState the [InteractionState] representing the different [Interaction]s
* present on this FAB. You can create and pass in your own remembered [InteractionState] if
* you want to read the [InteractionState] and customize the appearance / behavior of this FAB
* in different [Interaction]s, such as customizing how the [elevation] of this FAB changes when
* it is [Interaction.Pressed].
* @param shape The [Shape] of this FAB
* @param backgroundColor The background color. Use [Color.Transparent] to have no color
* @param contentColor The preferred content color for content inside this FAB
* @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB
* in different states. This controls the size of the shadow below the FAB.
* @param icon the content of this FAB
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionState: InteractionState = remember { InteractionState() },
shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
backgroundColor: Color = MaterialTheme.colors.secondary,
contentColor: Color = contentColorFor(backgroundColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonConstants.defaultElevation(),
icon: @Composable () -> Unit
) {
// TODO(aelias): Avoid manually managing the ripple once http://b/157687898
// is fixed and we have more flexibility to move the clickable modifier
// (see candidate approach aosp/1361921)
Surface(
modifier = modifier.clickable(
onClick = onClick,
interactionState = interactionState,
indication = null
),
shape = shape,
color = backgroundColor,
contentColor = contentColor,
elevation = elevation.elevation(interactionState)
) {
ProvideTextStyle(MaterialTheme.typography.button) {
Box(
modifier = Modifier
.defaultMinSizeConstraints(minWidth = FabSize, minHeight = FabSize)
.indication(interactionState, AmbientIndication.current()),
alignment = Alignment.Center
) { icon() }
}
}
}
/**
* A floating action button (FAB) is a button that represents the primary action of a screen.
*
* This extended FAB contains text and an optional icon that will be placed at the start. See
* [FloatingActionButton] for a FAB that just contains some content, typically an icon.
*
* @sample androidx.compose.material.samples.SimpleExtendedFabWithIcon
*
* If you want FAB’s container to have a fluid width (to be defined by its relationship to something
* else on screen, such as screen width or the layout grid) just apply an appropriate modifier.
* For example to fill the whole available width you can do:
*
* @sample androidx.compose.material.samples.FluidExtendedFab
*
* @param text Text label displayed inside this FAB
* @param onClick will be called when user clicked on this FAB. The FAB will be disabled
* when it is null.
* @param modifier [Modifier] to be applied to this FAB
* @param icon Optional icon for this FAB, typically this will be a
* [Icon].
* @param interactionState the [InteractionState] representing the different [Interaction]s
* present on this FAB. You can create and pass in your own remembered [InteractionState] if
* you want to read the [InteractionState] and customize the appearance / behavior of this FAB
* in different [Interaction]s, such as customizing how the [elevation] of this FAB changes when
* it is [Interaction.Pressed].
* @param shape The [Shape] of this FAB
* @param backgroundColor The background color. Use [Color.Transparent] to have no color
* @param contentColor The preferred content color. Will be used by text and iconography
* @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB
* in different states. This controls the size of the shadow below the FAB.
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ExtendedFloatingActionButton(
text: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
icon: @Composable (() -> Unit)? = null,
interactionState: InteractionState = remember { InteractionState() },
shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
backgroundColor: Color = MaterialTheme.colors.secondary,
contentColor: Color = contentColorFor(backgroundColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonConstants.defaultElevation()
) {
FloatingActionButton(
modifier = modifier.preferredSizeIn(
minWidth = ExtendedFabSize,
minHeight = ExtendedFabSize
),
onClick = onClick,
interactionState = interactionState,
shape = shape,
backgroundColor = backgroundColor,
contentColor = contentColor,
elevation = elevation
) {
Box(
modifier = Modifier.padding(
start = ExtendedFabTextPadding,
end = ExtendedFabTextPadding
),
alignment = Alignment.Center
) {
if (icon == null) {
text()
} else {
Row(verticalAlignment = Alignment.CenterVertically) {
icon()
Spacer(Modifier.preferredWidth(ExtendedFabIconPadding))
text()
}
}
}
}
}
/**
* Represents the elevation for a floating action button in different states.
*
* See [FloatingActionButtonConstants.defaultElevation] for the default elevation used in a
* [FloatingActionButton] and [ExtendedFloatingActionButton].
*/
@ExperimentalMaterialApi
@Stable
interface FloatingActionButtonElevation {
/**
* Represents the elevation used in a floating action button, depending on [interactionState].
*
* @param interactionState the [InteractionState] for this floating action button
*/
fun elevation(interactionState: InteractionState): Dp
}
/**
* Contains the default values used by [FloatingActionButton]
*/
object FloatingActionButtonConstants {
// TODO: b/152525426 add support for focused and hovered states
/**
* Creates a [FloatingActionButtonElevation] that will animate between the provided values
* according to the Material specification.
*
* @param defaultElevation the elevation to use when the [FloatingActionButton] has no
* [Interaction]s
* @param pressedElevation the elevation to use when the [FloatingActionButton] is
* [Interaction.Pressed].
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun defaultElevation(
defaultElevation: Dp = 6.dp,
pressedElevation: Dp = 12.dp
// focused: Dp = 8.dp,
// hovered: Dp = 8.dp,
): FloatingActionButtonElevation {
val clock = AnimationClockAmbient.current.asDisposableClock()
return remember(defaultElevation, pressedElevation, clock) {
DefaultFloatingActionButtonElevation(
defaultElevation = defaultElevation,
pressedElevation = pressedElevation,
clock = clock
)
}
}
}
/**
* Default [FloatingActionButtonElevation] implementation.
*/
@OptIn(ExperimentalMaterialApi::class)
@Stable
private class DefaultFloatingActionButtonElevation(
private val defaultElevation: Dp,
private val pressedElevation: Dp,
private val clock: AnimationClockObservable
) : FloatingActionButtonElevation {
private val lazyAnimatedElevation = LazyAnimatedValue<Dp, AnimationVector1D> { target ->
AnimatedValueModel(initialValue = target, typeConverter = Dp.VectorConverter, clock = clock)
}
override fun elevation(interactionState: InteractionState): Dp {
val interaction = interactionState.value.lastOrNull {
it is Interaction.Pressed
}
val target = when (interaction) {
Interaction.Pressed -> pressedElevation
else -> defaultElevation
}
val animatedElevation = lazyAnimatedElevation.animatedValueForTarget(target)
if (animatedElevation.targetValue != target) {
val lastInteraction = when (animatedElevation.targetValue) {
pressedElevation -> Interaction.Pressed
else -> null
}
animatedElevation.animateElevation(
from = lastInteraction,
to = interaction,
target = target
)
}
return animatedElevation.value
}
}
private val FabSize = 56.dp
private val ExtendedFabSize = 48.dp
private val ExtendedFabIconPadding = 12.dp
private val ExtendedFabTextPadding = 20.dp