Compound: add SuperButton and GradientFloatingActionButton components
Also add `ButtonSize.Small` and adjust existing `Button` component
This commit is contained in:
@@ -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,
|
||||
) {
|
||||
val minHeight = when (size) {
|
||||
ButtonSize.Small -> 32.dp
|
||||
ButtonSize.Medium -> 40.dp
|
||||
ButtonSize.Large -> 48.dp
|
||||
}
|
||||
@@ -145,6 +146,13 @@ private fun ButtonInternal(
|
||||
val hasStartDrawable = showProgress || leadingIcon != null
|
||||
|
||||
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) {
|
||||
ButtonStyle.Filled,
|
||||
ButtonStyle.Outlined -> if (hasStartDrawable) {
|
||||
@@ -195,7 +203,7 @@ private fun ButtonInternal(
|
||||
}
|
||||
|
||||
val textStyle = when (size) {
|
||||
ButtonSize.Medium -> MaterialTheme.typography.labelLarge
|
||||
ButtonSize.Small, ButtonSize.Medium -> MaterialTheme.typography.labelLarge
|
||||
ButtonSize.Large -> ElementTheme.typography.fontBodyLgMedium
|
||||
}
|
||||
|
||||
@@ -259,6 +267,7 @@ sealed interface IconSource {
|
||||
}
|
||||
|
||||
enum class ButtonSize {
|
||||
Small,
|
||||
Medium,
|
||||
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)
|
||||
@Composable
|
||||
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)
|
||||
@Composable
|
||||
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)
|
||||
@Composable
|
||||
internal fun TextButtonMediumPreview() {
|
||||
|
||||
Reference in New Issue
Block a user