FloatingActionButton.kt

/*
 * 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.VectorConverter
import androidx.compose.animation.animatedValue
import androidx.compose.foundation.IndicationAmbient
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
import androidx.compose.foundation.ProvideTextStyle
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.onCommit
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.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 [androidx.compose.foundation.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 The z-coordinate at which to place this FAB. This controls the size
 * of the shadow below the FAB. See [FloatingActionButtonConstants.animateDefaultElevation] for
 * the default elevation that animates between [Interaction]s.
 * @param icon the content of this FAB
 */
@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: Dp = FloatingActionButtonConstants.animateDefaultElevation(interactionState),
    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
    ) {
        ProvideTextStyle(MaterialTheme.typography.button) {
            Box(
                modifier = Modifier
                    .defaultMinSizeConstraints(minWidth = FabSize, minHeight = FabSize)
                    .indication(interactionState, IndicationAmbient.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
 * [androidx.compose.foundation.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 The z-coordinate at which to place this FAB. This controls the size
 * of the shadow below the button. See [FloatingActionButtonConstants.animateDefaultElevation] for
 * the default elevation that animates between [Interaction]s.
 */
@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: Dp = FloatingActionButtonConstants.animateDefaultElevation(interactionState)
) {
    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()
                }
            }
        }
    }
}

/**
 * Contains the default values used by [FloatingActionButton]
 */
object FloatingActionButtonConstants {
    // TODO: b/152525426 add support for focused and hovered states
    /**
     * Represents the default elevation for a button in different [Interaction]s, and how the
     * elevation animates between them.
     *
     * @param interactionState the [InteractionState] for this [FloatingActionButton], representing
     * the current visual state, such as whether it is [Interaction.Pressed] or not.
     * @param defaultElevation the elevation to use when the [FloatingActionButton] is has no
     * [Interaction]s
     * @param pressedElevation the elevation to use when the [FloatingActionButton] is
     * [Interaction.Pressed].
     */
    @Composable
    fun animateDefaultElevation(
        interactionState: InteractionState,
        defaultElevation: Dp = 6.dp,
        pressedElevation: Dp = 12.dp
        // focused: Dp = 8.dp,
        // hovered: Dp = 8.dp,
    ): Dp {
        class InteractionHolder(var interaction: Interaction?)

        val interaction = interactionState.value.lastOrNull {
            it is Interaction.Pressed
        }

        val target = when (interaction) {
            Interaction.Pressed -> pressedElevation
            else -> defaultElevation
        }

        val previousInteractionHolder = remember { InteractionHolder(interaction) }

        val animatedElevation = animatedValue(target, Dp.VectorConverter)

        onCommit(target) {
            animatedElevation.animateElevation(
                from = previousInteractionHolder.interaction,
                to = interaction,
                target = target
            )

            // Update the last interaction, so we know what AnimationSpec to use if we animate
            // away from a state
            previousInteractionHolder.interaction = interaction
        }

        return animatedElevation.value
    }
}

private val FabSize = 56.dp
private val ExtendedFabSize = 48.dp
private val ExtendedFabIconPadding = 12.dp
private val ExtendedFabTextPadding = 20.dp