Compound: add SuperButton and GradientFloatingActionButton components
Also add `ButtonSize.Small` and adjust existing `Button` component
This commit is contained in:
parent
4da9dc548d
commit
08c25a1d52
3 changed files with 382 additions and 1 deletions
|
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 io.element.android.libraries.designsystem.components.button
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.geometry.center
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.LinearGradientShader
|
||||||
|
import androidx.compose.ui.graphics.RadialGradientShader
|
||||||
|
import androidx.compose.ui.graphics.Shader
|
||||||
|
import androidx.compose.ui.graphics.ShaderBrush
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.annotations.CoreColorToken
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.compound.tokens.generated.internal.LightColorTokens
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
|
||||||
|
@OptIn(CoreColorToken::class)
|
||||||
|
@Composable
|
||||||
|
fun GradientFloatingActionButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
shape: Shape = RoundedCornerShape(25),
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
val linearShaderBrush = remember {
|
||||||
|
object : ShaderBrush() {
|
||||||
|
override fun createShader(size: Size): Shader {
|
||||||
|
return LinearGradientShader(
|
||||||
|
from = Offset(size.width, size.height),
|
||||||
|
to = Offset(size.width, 0f),
|
||||||
|
colors = listOf(
|
||||||
|
LightColorTokens.colorBlue900,
|
||||||
|
LightColorTokens.colorGreen700,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val radialShaderBrush = remember {
|
||||||
|
object : ShaderBrush() {
|
||||||
|
override fun createShader(size: Size): Shader {
|
||||||
|
return RadialGradientShader(
|
||||||
|
center = size.center,
|
||||||
|
radius = size.width / 2,
|
||||||
|
colors = listOf(
|
||||||
|
LightColorTokens.colorGreen700,
|
||||||
|
LightColorTokens.colorBlue900,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.graphicsLayer(shape = shape, clip = false)
|
||||||
|
.clip(shape)
|
||||||
|
.drawBehind {
|
||||||
|
drawRect(brush = radialShaderBrush, alpha = 0.4f)
|
||||||
|
drawRect(brush = linearShaderBrush)
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
enabled = true,
|
||||||
|
onClick = onClick,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = rememberRipple(color = Color.White)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides Color.White) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun GradientFloatingActionButtonPreview() {
|
||||||
|
ElementPreview {
|
||||||
|
Box(modifier = Modifier.padding(20.dp)) {
|
||||||
|
GradientFloatingActionButton(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Icon(imageVector = CompoundIcons.ChatNew(), contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun GradientSendButtonPreview() {
|
||||||
|
ElementPreview {
|
||||||
|
Box(modifier = Modifier.padding(20.dp)) {
|
||||||
|
GradientFloatingActionButton(
|
||||||
|
shape = CircleShape,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(start = 2.dp),
|
||||||
|
imageVector = CompoundIcons.SendSolid(),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 io.element.android.libraries.designsystem.components.button
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.LinearGradientShader
|
||||||
|
import androidx.compose.ui.graphics.Shader
|
||||||
|
import androidx.compose.ui.graphics.ShaderBrush
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.annotations.CoreColorToken
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.internal.DarkColorTokens
|
||||||
|
import io.element.android.compound.tokens.generated.internal.LightColorTokens
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
|
|
||||||
|
@OptIn(CoreColorToken::class)
|
||||||
|
@Composable
|
||||||
|
fun SuperButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
shape: Shape = RoundedCornerShape(50),
|
||||||
|
buttonSize: ButtonSize = ButtonSize.Large,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val contentPadding = remember(buttonSize) {
|
||||||
|
when (buttonSize) {
|
||||||
|
ButtonSize.Large -> PaddingValues(horizontal = 24.dp, vertical = 13.dp)
|
||||||
|
ButtonSize.Medium -> PaddingValues(horizontal = 20.dp, vertical = 9.dp)
|
||||||
|
ButtonSize.Small -> PaddingValues(horizontal = 16.dp, vertical = 5.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box {
|
||||||
|
val isLightTheme = ElementTheme.isLightTheme
|
||||||
|
val colors = remember(isLightTheme) {
|
||||||
|
if (isLightTheme) {
|
||||||
|
listOf(
|
||||||
|
LightColorTokens.colorBlue900,
|
||||||
|
LightColorTokens.colorGreen1100,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
DarkColorTokens.colorBlue900,
|
||||||
|
DarkColorTokens.colorGreen1100,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val shaderBrush = remember(colors) {
|
||||||
|
object : ShaderBrush() {
|
||||||
|
override fun createShader(size: Size): Shader {
|
||||||
|
return LinearGradientShader(
|
||||||
|
from = Offset(0f, size.height),
|
||||||
|
to = Offset(size.width, 0f),
|
||||||
|
colors = colors,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val border = if (enabled) {
|
||||||
|
BorderStroke(1.dp, shaderBrush)
|
||||||
|
} else {
|
||||||
|
BorderStroke(1.dp, ElementTheme.colors.borderDisabled)
|
||||||
|
}
|
||||||
|
val backgroundColor = ElementTheme.colors.bgCanvasDefault
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.minimumInteractiveComponentSize()
|
||||||
|
.graphicsLayer(shape = shape, clip = false)
|
||||||
|
.clip(shape)
|
||||||
|
.border(border, shape)
|
||||||
|
.drawBehind {
|
||||||
|
drawRect(backgroundColor)
|
||||||
|
drawRect(brush = shaderBrush, alpha = 0.04f)
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = onClick,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = rememberRipple()
|
||||||
|
)
|
||||||
|
.padding(contentPadding),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalContentColor provides if (enabled) ElementTheme.colors.textPrimary else ElementTheme.colors.textDisabled,
|
||||||
|
LocalTextStyle provides ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun SuperButtonPreview() {
|
||||||
|
ElementPreview {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
SuperButton(
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
buttonSize = ButtonSize.Large,
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Text("Super button!")
|
||||||
|
}
|
||||||
|
|
||||||
|
SuperButton(
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
buttonSize = ButtonSize.Medium,
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Text("Super button!")
|
||||||
|
}
|
||||||
|
|
||||||
|
SuperButton(
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
buttonSize = ButtonSize.Small,
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Text("Super button!")
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
SuperButton(
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
buttonSize = ButtonSize.Large,
|
||||||
|
enabled = false,
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Text("Super button!")
|
||||||
|
}
|
||||||
|
|
||||||
|
SuperButton(
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
buttonSize = ButtonSize.Medium,
|
||||||
|
enabled = false,
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Text("Super button!")
|
||||||
|
}
|
||||||
|
|
||||||
|
SuperButton(
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
buttonSize = ButtonSize.Small,
|
||||||
|
enabled = false,
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Text("Super button!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -138,6 +138,7 @@ private fun ButtonInternal(
|
||||||
leadingIcon: IconSource? = null,
|
leadingIcon: IconSource? = null,
|
||||||
) {
|
) {
|
||||||
val minHeight = when (size) {
|
val minHeight = when (size) {
|
||||||
|
ButtonSize.Small -> 32.dp
|
||||||
ButtonSize.Medium -> 40.dp
|
ButtonSize.Medium -> 40.dp
|
||||||
ButtonSize.Large -> 48.dp
|
ButtonSize.Large -> 48.dp
|
||||||
}
|
}
|
||||||
|
|
@ -145,6 +146,13 @@ private fun ButtonInternal(
|
||||||
val hasStartDrawable = showProgress || leadingIcon != null
|
val hasStartDrawable = showProgress || leadingIcon != null
|
||||||
|
|
||||||
val contentPadding = when (size) {
|
val contentPadding = when (size) {
|
||||||
|
ButtonSize.Small -> {
|
||||||
|
if (hasStartDrawable) {
|
||||||
|
PaddingValues(start = 8.dp, top = 5.dp, end = 16.dp, bottom = 5.dp)
|
||||||
|
} else {
|
||||||
|
PaddingValues(start = 16.dp, top = 5.dp, end = 16.dp, bottom = 5.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
ButtonSize.Medium -> when (style) {
|
ButtonSize.Medium -> when (style) {
|
||||||
ButtonStyle.Filled,
|
ButtonStyle.Filled,
|
||||||
ButtonStyle.Outlined -> if (hasStartDrawable) {
|
ButtonStyle.Outlined -> if (hasStartDrawable) {
|
||||||
|
|
@ -195,7 +203,7 @@ private fun ButtonInternal(
|
||||||
}
|
}
|
||||||
|
|
||||||
val textStyle = when (size) {
|
val textStyle = when (size) {
|
||||||
ButtonSize.Medium -> MaterialTheme.typography.labelLarge
|
ButtonSize.Small, ButtonSize.Medium -> MaterialTheme.typography.labelLarge
|
||||||
ButtonSize.Large -> ElementTheme.typography.fontBodyLgMedium
|
ButtonSize.Large -> ElementTheme.typography.fontBodyLgMedium
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,6 +267,7 @@ sealed interface IconSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ButtonSize {
|
enum class ButtonSize {
|
||||||
|
Small,
|
||||||
Medium,
|
Medium,
|
||||||
Large
|
Large
|
||||||
}
|
}
|
||||||
|
|
@ -317,6 +326,15 @@ internal enum class ButtonStyle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Buttons)
|
||||||
|
@Composable
|
||||||
|
internal fun FilledButtonSmallPreview() {
|
||||||
|
ButtonCombinationPreview(
|
||||||
|
style = ButtonStyle.Filled,
|
||||||
|
size = ButtonSize.Small,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.Buttons)
|
@Preview(group = PreviewGroup.Buttons)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun FilledButtonMediumPreview() {
|
internal fun FilledButtonMediumPreview() {
|
||||||
|
|
@ -335,6 +353,15 @@ internal fun FilledButtonLargePreview() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Buttons)
|
||||||
|
@Composable
|
||||||
|
internal fun OutlinedButtonSmallPreview() {
|
||||||
|
ButtonCombinationPreview(
|
||||||
|
style = ButtonStyle.Outlined,
|
||||||
|
size = ButtonSize.Small,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.Buttons)
|
@Preview(group = PreviewGroup.Buttons)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun OutlinedButtonMediumPreview() {
|
internal fun OutlinedButtonMediumPreview() {
|
||||||
|
|
@ -353,6 +380,15 @@ internal fun OutlinedButtonLargePreview() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Buttons)
|
||||||
|
@Composable
|
||||||
|
internal fun TextButtonSmallPreview() {
|
||||||
|
ButtonCombinationPreview(
|
||||||
|
style = ButtonStyle.Text,
|
||||||
|
size = ButtonSize.Small,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.Buttons)
|
@Preview(group = PreviewGroup.Buttons)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TextButtonMediumPreview() {
|
internal fun TextButtonMediumPreview() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue