diff --git a/app/src/main/kotlin/com/yourssu/handy/demo/FloatingActionButtonPreview.kt b/app/src/main/kotlin/com/yourssu/handy/demo/FloatingActionButtonPreview.kt new file mode 100644 index 0000000..16ed17b --- /dev/null +++ b/app/src/main/kotlin/com/yourssu/handy/demo/FloatingActionButtonPreview.kt @@ -0,0 +1,118 @@ +package com.yourssu.handy.demo + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.yourssu.handy.compose.FloatingActionButton +import com.yourssu.handy.compose.FloatingActionButtonSize +import com.yourssu.handy.compose.FloatingActionButtonType +import com.yourssu.handy.compose.HandyTheme +import com.yourssu.handy.compose.icons.HandyIcons +import com.yourssu.handy.compose.icons.line.ArrowsChevronUp +import com.yourssu.handy.compose.icons.line.ExternalLink + +@Composable +@Preview +fun FloatingActionButtonPreview() { + HandyTheme { + Column( + modifier = Modifier + .background(Color.White) + .wrapContentSize() + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + FloatingActionButton( + onClick = {}, + icon = HandyIcons.Line.ArrowsChevronUp, + sizeType = FloatingActionButtonSize.S, + floatingActionButtonType = FloatingActionButtonType.Primary + ) + FloatingActionButton( + onClick = {}, + icon = HandyIcons.Line.ExternalLink, + sizeType = FloatingActionButtonSize.S, + floatingActionButtonType = FloatingActionButtonType.Primary, + enabled = false + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + FloatingActionButton( + onClick = {}, + icon = HandyIcons.Line.ExternalLink, + sizeType = FloatingActionButtonSize.S, + floatingActionButtonType = FloatingActionButtonType.Secondary, + ) + + FloatingActionButton( + onClick = {}, + icon = HandyIcons.Line.ExternalLink, + sizeType = FloatingActionButtonSize.S, + floatingActionButtonType = FloatingActionButtonType.Secondary, + enabled = false + ) + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + FloatingActionButton( + onClick = {}, + icon = HandyIcons.Line.ExternalLink, + sizeType = FloatingActionButtonSize.L, + floatingActionButtonType = FloatingActionButtonType.Primary + ) + + FloatingActionButton( + onClick = {}, + icon = HandyIcons.Line.ExternalLink, + sizeType = FloatingActionButtonSize.L, + floatingActionButtonType = FloatingActionButtonType.Primary, + enabled = false + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + FloatingActionButton( + onClick = {}, + icon = HandyIcons.Line.ExternalLink, + sizeType = FloatingActionButtonSize.L, + floatingActionButtonType = FloatingActionButtonType.Secondary, + ) + + FloatingActionButton( + onClick = {}, + icon = HandyIcons.Line.ExternalLink, + sizeType = FloatingActionButtonSize.L, + floatingActionButtonType = FloatingActionButtonType.Secondary, + enabled = false + ) + } + } + } + } +} \ No newline at end of file diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/FloatingActionButton.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/FloatingActionButton.kt new file mode 100644 index 0000000..f238320 --- /dev/null +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/FloatingActionButton.kt @@ -0,0 +1,191 @@ +package com.yourssu.handy.compose + +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +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.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.yourssu.handy.compose.foundation.ColorFabPrimaryShadow +import com.yourssu.handy.compose.foundation.ColorFabSecondaryShadow +import com.yourssu.handy.compose.icons.HandyIcons +import com.yourssu.handy.compose.icons.line.ExternalLink + +enum class FloatingActionButtonSize(internal val value: Dp) { + S(40.dp), + L(56.dp), +} + +enum class FloatingActionButtonType { + Primary, + Secondary; +} + +@Composable +fun FloatingActionButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = CircleShape, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + icon: ImageVector = HandyIcons.Line.ExternalLink, + sizeType: FloatingActionButtonSize = FloatingActionButtonSize.S, + floatingActionButtonType: FloatingActionButtonType = FloatingActionButtonType.Primary, +) { + val colors = floatingActionButtonColorByType(type = floatingActionButtonType) + val localPressed by interactionSource.collectIsPressedAsState() + val containerColor = colors.apply { pressed = localPressed } + val contentColor by containerColor.contentColor(enabled) + val borderColor = containerColor.borderColor() + + val containerSize = boxButtonSizeStateBySize(size = sizeType) + + Surface( + onClick = onClick, + modifier = modifier.semantics { role = Role.Button }, + shape = shape, + backgroundColor = containerColor.backgroundColor(enabled).value, + contentColor = contentColor, + interactionSource = interactionSource, + shadowColor = containerColor.shadowColor(enabled).value, + shadowElevation = FloatingActionButtonDefaults.FloatingActionButtonShadowElevation, + ) { + Box( + modifier = Modifier + .size(containerSize.containerSize.value) + .border( + width = FloatingActionButtonDefaults.FloatingActionButtonBorderSize, + color = borderColor.value, + shape = shape + ), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + iconSize = FloatingActionButtonDefaults.iconSize, + ) + } + } +} + + +@Immutable +data class FloatingActionButtonSizeState( + val containerSize: FloatingActionButtonSize = FloatingActionButtonSize.S, +) + + +private fun boxButtonSizeStateBySize( + size: FloatingActionButtonSize, +): FloatingActionButtonSizeState = when (size) { + FloatingActionButtonSize.S -> FloatingActionButtonSizeState( + containerSize = FloatingActionButtonSize.S, + ) + + FloatingActionButtonSize.L -> FloatingActionButtonSizeState( + containerSize = FloatingActionButtonSize.L, + ) +} + +@Composable +private fun floatingActionButtonColorByType( + type: FloatingActionButtonType, +): FloatingActionButtonColorState = when (type) { + FloatingActionButtonType.Primary -> FloatingActionButtonColorState( + contentColor = HandyTheme.colors.iconBasicWhite, + disabledContentColor = HandyTheme.colors.iconBasicWhite, + bgColor = HandyTheme.colors.buttonFabPrimaryEnabled, + disabledBgColor = HandyTheme.colors.buttonFabPrimaryDisabled, + shadowColor = ColorFabPrimaryShadow, + borderColor = Color.Transparent + ) + + FloatingActionButtonType.Secondary -> FloatingActionButtonColorState( + contentColor = HandyTheme.colors.iconBasicTertiary, + disabledContentColor = HandyTheme.colors.iconBasicDisabled, + bgColor = HandyTheme.colors.buttonFabSecondaryEnabled, + disabledBgColor = HandyTheme.colors.buttonFabSecondaryDisabled, + shadowColor = ColorFabSecondaryShadow, + borderColor = HandyTheme.colors.lineBasicLight + ) +} + +@Composable +private fun pressedFloatingActionButtonColorFor(color: Color): Color { + return when (color) { + HandyTheme.colors.buttonFabPrimaryEnabled -> HandyTheme.colors.buttonFabPrimaryPressed + HandyTheme.colors.buttonFabSecondaryEnabled -> HandyTheme.colors.buttonFabSecondaryPressed + else -> color + } +} + +@Immutable +class FloatingActionButtonColorState( + val contentColor: Color = Color.Unspecified, + val disabledContentColor: Color = Color.Unspecified, + val bgColor: Color = Color.Transparent, + val disabledBgColor: Color = Color.Transparent, + val shadowColor: Color = Color.Transparent, + val borderColor: Color = Color.Transparent, + pressed: Boolean = false, +) { + var pressed by mutableStateOf(pressed) + internal set + + @Composable + fun contentColor(enabled: Boolean): State = + rememberUpdatedState( + when { + !enabled -> disabledContentColor + pressed -> pressedFloatingActionButtonColorFor(contentColor) + else -> contentColor + } + ) + + @Composable + fun backgroundColor(enabled: Boolean): State = + rememberUpdatedState( + when { + !enabled -> disabledBgColor + pressed -> pressedFloatingActionButtonColorFor(bgColor) + else -> bgColor + } + ) + + @Composable + fun shadowColor(enabled: Boolean): State = + rememberUpdatedState( + when { + !enabled -> Color.Transparent + else -> shadowColor + } + ) + + @Composable + fun borderColor(): State = + rememberUpdatedState(borderColor) +} + +object FloatingActionButtonDefaults { + val iconSize: IconSize = IconSize.M // 24.dp + val FloatingActionButtonBorderSize = 1.dp + val FloatingActionButtonShadowElevation = 6.dp +} \ No newline at end of file diff --git a/compose/src/main/kotlin/com/yourssu/handy/compose/button/ButtonState.kt b/compose/src/main/kotlin/com/yourssu/handy/compose/button/ButtonState.kt index 2ec5c6c..10c6ba7 100644 --- a/compose/src/main/kotlin/com/yourssu/handy/compose/button/ButtonState.kt +++ b/compose/src/main/kotlin/com/yourssu/handy/compose/button/ButtonState.kt @@ -45,15 +45,6 @@ class ButtonColorState( else -> bgColor } ) - - @Composable - fun shadowColor(enabled: Boolean): State = - rememberUpdatedState( - when { - !enabled -> Color.Transparent - else -> shadowColor - } - ) } @Composable @@ -64,8 +55,6 @@ private fun pressedColorFor(color: Color): Color { HandyTheme.colors.buttonBoxTertiaryEnabled -> HandyTheme.colors.buttonBoxTertiaryPressed HandyTheme.colors.buttonTextPrimaryEnabled -> HandyTheme.colors.buttonTextPrimaryPressed HandyTheme.colors.buttonTextSecondaryEnabled -> HandyTheme.colors.buttonTextSecondaryPressed - HandyTheme.colors.buttonFabPrimaryEnabled -> HandyTheme.colors.buttonFabPrimaryPressed - HandyTheme.colors.buttonFabSecondaryEnabled -> HandyTheme.colors.buttonFabSecondaryPressed else -> color } }