Merge pull request #2696 from element-hq/misc/add-super-button-component
Compound: add SuperButton and GradientFAB components
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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,
|
||||
) {
|
||||
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,193 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
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,6 +203,7 @@ private fun ButtonInternal(
|
||||
}
|
||||
|
||||
val textStyle = when (size) {
|
||||
ButtonSize.Small,
|
||||
ButtonSize.Medium -> MaterialTheme.typography.labelLarge
|
||||
ButtonSize.Large -> ElementTheme.typography.fontBodyLgMedium
|
||||
}
|
||||
@@ -259,6 +268,7 @@ sealed interface IconSource {
|
||||
}
|
||||
|
||||
enum class ButtonSize {
|
||||
Small,
|
||||
Medium,
|
||||
Large
|
||||
}
|
||||
@@ -317,6 +327,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 +354,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 +381,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() {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user