secant-android-wallet/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Button.kt

360 lines
12 KiB
Kotlin

package co.electriccoin.zcash.ui.design.component
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ButtonDefaults.buttonColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.PaintingStyle
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Preview
@Composable
private fun ButtonComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Column {
PrimaryButton(onClick = { }, text = "Primary")
SecondaryButton(onClick = { }, text = "Secondary")
TertiaryButton(onClick = { }, text = "Tertiary")
NavigationButton(onClick = { }, text = "Navigation")
DangerousButton(onClick = { }, text = "Dangerous")
}
}
}
}
@Composable
@Suppress("LongParameterList")
fun PrimaryButton(
onClick: () -> Unit,
text: String,
modifier: Modifier = Modifier,
outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingNone,
vertical = ZcashTheme.dimens.spacingSmall
),
enabled: Boolean = true,
buttonColor: Color = MaterialTheme.colorScheme.primary,
textColor: Color = MaterialTheme.colorScheme.onPrimary,
) {
Button(
shape = RectangleShape,
enabled = enabled,
modifier =
modifier.then(Modifier.fillMaxWidth())
.padding(outerPaddingValues)
.shadow(
contentColor = textColor,
strokeColor = buttonColor,
strokeWidth = 1.dp,
offsetX = ZcashTheme.dimens.buttonShadowOffsetX,
offsetY = ZcashTheme.dimens.buttonShadowOffsetY,
spread = ZcashTheme.dimens.buttonShadowSpread,
)
.translationClick(
// + 6dp to exactly cover the bottom shadow
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp,
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
)
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
.border(1.dp, Color.Black),
colors =
buttonColors(
containerColor = buttonColor,
disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
),
onClick = onClick,
) {
Text(
style = ZcashTheme.extendedTypography.buttonText,
textAlign = TextAlign.Center,
text = text.uppercase(),
color = textColor
)
}
}
@Composable
@Suppress("LongParameterList")
fun SecondaryButton(
onClick: () -> Unit,
text: String,
modifier: Modifier = Modifier,
outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingNone,
vertical = ZcashTheme.dimens.spacingSmall
),
enabled: Boolean = true,
buttonColor: Color = MaterialTheme.colorScheme.secondary,
textColor: Color = MaterialTheme.colorScheme.onSecondary,
) {
Button(
shape = RectangleShape,
enabled = enabled,
modifier =
modifier.then(Modifier.fillMaxWidth())
.padding(outerPaddingValues)
.shadow(
contentColor = textColor,
strokeColor = textColor,
offsetX = ZcashTheme.dimens.buttonShadowOffsetX,
offsetY = ZcashTheme.dimens.buttonShadowOffsetY,
spread = ZcashTheme.dimens.buttonShadowSpread,
)
.translationClick(
// + 6dp to exactly cover the bottom shadow
translationX = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp,
translationY = ZcashTheme.dimens.buttonShadowOffsetX + 6.dp
)
.defaultMinSize(ZcashTheme.dimens.buttonWidth, ZcashTheme.dimens.buttonHeight)
.border(1.dp, Color.Black),
colors =
buttonColors(
containerColor = buttonColor,
disabledContainerColor = ZcashTheme.colors.disabledButtonColor,
disabledContentColor = ZcashTheme.colors.disabledButtonTextColor
),
onClick = onClick,
) {
Text(
style = ZcashTheme.extendedTypography.buttonText,
textAlign = TextAlign.Center,
text = text.uppercase(),
color = textColor
)
}
}
@Composable
fun NavigationButton(
onClick: () -> Unit,
text: String,
modifier: Modifier = Modifier,
outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingSmall
),
) {
Button(
shape = RectangleShape,
onClick = onClick,
modifier =
modifier.then(
Modifier
.padding(outerPaddingValues)
),
colors = buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
) {
Text(
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
text = text,
color = MaterialTheme.colorScheme.onSecondary
)
}
}
@Composable
fun TertiaryButton(
onClick: () -> Unit,
text: String,
modifier: Modifier = Modifier,
outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingSmall
),
enabled: Boolean = true
) {
Button(
shape = RectangleShape,
onClick = onClick,
modifier =
modifier.then(
Modifier
.fillMaxWidth()
.padding(outerPaddingValues)
),
enabled = enabled,
elevation = ButtonDefaults.buttonElevation(0.dp, 0.dp, 0.dp),
colors = buttonColors(containerColor = ZcashTheme.colors.tertiary)
) {
Text(
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
text = text,
color = ZcashTheme.colors.onTertiary
)
}
}
@Composable
fun DangerousButton(
onClick: () -> Unit,
text: String,
modifier: Modifier = Modifier,
outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingSmall
),
) {
Button(
shape = RectangleShape,
onClick = onClick,
modifier =
modifier.then(
Modifier
.fillMaxWidth()
.padding(outerPaddingValues)
),
colors = buttonColors(containerColor = ZcashTheme.colors.dangerous)
) {
Text(
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
text = text,
color = ZcashTheme.colors.onDangerous
)
}
}
@Suppress("LongParameterList")
fun Modifier.shadow(
contentColor: Color = Color.Black,
strokeColor: Color = Color.Black,
strokeWidth: Dp = 2.dp,
offsetY: Dp = 0.dp,
offsetX: Dp = 0.dp,
spread: Dp = 0f.dp,
modifier: Modifier = Modifier
) = this.then(
modifier.drawBehind {
this.drawIntoCanvas {
val strokePaint = Paint()
strokePaint.style = PaintingStyle.Stroke
strokePaint.strokeWidth = strokeWidth.toPx()
val contentPaint = Paint()
strokePaint.style = PaintingStyle.Fill
// Reusable inner function to be able to reach modifier and canvas properties
fun drawShadowLayer(
paint: Paint,
color: Color,
paddingWidth: Float
) {
val frameworkPaint = paint.asFrameworkPaint()
val spreadPixel = spread.toPx()
val leftPixel = (0f - spreadPixel) + offsetX.toPx()
val topPixel = (0f - spreadPixel) + offsetY.toPx()
val rightPixel = (this.size.width + spreadPixel)
val bottomPixel = (this.size.height + spreadPixel)
frameworkPaint.color = color.toArgb()
it.drawRoundRect(
left = leftPixel + paddingWidth,
top = topPixel + paddingWidth,
right = rightPixel - paddingWidth,
bottom = bottomPixel - paddingWidth,
radiusX = 0f,
radiusY = 0f,
paint
)
}
// Draw stroke and then content paints
drawShadowLayer(strokePaint, strokeColor, 0f)
drawShadowLayer(contentPaint, contentColor, strokeWidth.toPx())
}
}
)
private enum class ButtonState { Pressed, Idle }
fun Modifier.translationClick(
translationX: Dp = 0.dp,
translationY: Dp = 0.dp
) = composed {
var buttonState by remember { mutableStateOf(ButtonState.Idle) }
val translationXAnimated by animateFloatAsState(
targetValue =
if (buttonState == ButtonState.Pressed) {
translationX.value
} else {
0f
},
label = "ClickTranslationXAnimation",
animationSpec =
tween(
durationMillis = 100
)
)
val translationYAnimated by animateFloatAsState(
targetValue =
if (buttonState == ButtonState.Pressed) {
translationY.value
} else {
0f
},
label = "ClickTranslationYAnimation",
animationSpec =
tween(
durationMillis = 100
)
)
this
.graphicsLayer {
this.translationX = translationXAnimated
this.translationY = translationYAnimated
}
.pointerInput(buttonState) {
awaitPointerEventScope {
buttonState =
if (buttonState == ButtonState.Pressed) {
waitForUpOrCancellation()
ButtonState.Idle
} else {
awaitFirstDown(false)
ButtonState.Pressed
}
}
}
}