diff --git a/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/composables/cards/CategoryCard.kt b/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/composables/cards/CategoryCard.kt index 22f10c8f..355d23d4 100644 --- a/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/composables/cards/CategoryCard.kt +++ b/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/composables/cards/CategoryCard.kt @@ -28,6 +28,7 @@ import com.jeluchu.jchucomponents.ktx.colors.applyOpacity import com.jeluchu.jchucomponents.ktx.strings.empty import com.jeluchu.jchucomponents.ui.R import com.jeluchu.jchucomponents.ui.composables.chips.Type +import com.jeluchu.jchucomponents.ui.composables.chips.TypeColors import com.jeluchu.jchucomponents.ui.extensions.modifier.cornerRadius @Composable @@ -60,8 +61,10 @@ fun CategoryCard( Spacer(modifier = Modifier.align(Alignment.Center)) Type( modifier = Modifier.align(Alignment.BottomEnd), - type = title, - textColor = textColor.applyOpacity(enabled), + text = title, + colors = TypeColors( + container = textColor.applyOpacity(enabled) + ), fontSize = fontSize, style = textStyle ) diff --git a/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/composables/chips/Type.kt b/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/composables/chips/Type.kt index 2a80b62e..0e085acf 100644 --- a/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/composables/chips/Type.kt +++ b/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/composables/chips/Type.kt @@ -8,14 +8,21 @@ package com.jeluchu.jchucomponents.ui.composables.chips import androidx.compose.foundation.ExperimentalFoundationApi 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.padding +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.TextUnit @@ -24,53 +31,94 @@ import androidx.compose.ui.unit.sp import com.jeluchu.jchucomponents.ui.extensions.modifier.cornerRadius import com.jeluchu.jchucomponents.ui.foundation.text.MarqueeText import com.jeluchu.jchucomponents.ui.themes.artichoke +import com.jeluchu.jchucomponents.ui.themes.cosmicLatte -@OptIn(ExperimentalFoundationApi::class) @Composable fun Type( - type: String, + text: String, modifier: Modifier = Modifier, - textColor: Color = artichoke, - bgColor: Color = artichoke.copy(alpha = 0.1f), + shape: Shape = CircleShape, + colors: TypeColors = TypeColors(), textAlign: TextAlign? = null, + fontWeight: FontWeight? = null, fontSize: TextUnit = 12.sp, - isLongText: Boolean = false, style: TextStyle = LocalTextStyle.current -) { - if (isLongText) - MarqueeText( - modifier = modifier - .padding(vertical = 4.dp) - .clip(10.cornerRadius()) - .background(bgColor) - .padding(10.dp, 2.dp), - text = type, - style = style, - fontSize = fontSize, - color = textColor, - textAlign = textAlign - ) - else - Text( - modifier = modifier - .padding(vertical = 4.dp) - .clip(10.cornerRadius()) - .background(bgColor) - .padding(10.dp, 2.dp), - text = type, - style = style, - fontSize = fontSize, - textAlign = textAlign, - color = textColor - ) -} +) = MarqueeText( + modifier = modifier + .clip(shape) + .background(colors.container) + .padding(10.dp, 2.dp), + text = text, + style = style, + fontSize = fontSize, + fontWeight = fontWeight, + gradientEdgeColor = colors.gradientEdge, + color = colors.content, + textAlign = textAlign +) + +@Immutable +class TypeColors constructor( + val content: Color = artichoke, + val gradientEdge: Color = Color.White, + val container: Color = artichoke.copy(alpha = 0.1f), +) @ExperimentalFoundationApi @Preview @Composable -fun VillagerTypePreview() { - Type( - "20/09", - Modifier - ) +fun TypePreview() { + Column( + modifier = Modifier.padding(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text("A simple text") + Type("20/09") + Text("A long simple text with default gradients") + Type("The world is a Vampire! And this text is a example text of Marquee Type to check feature") + Text("A long simple text with custom gradients") + Type( + text = "The world is a Vampire! And this text is a example text of Marquee Type to check feature", + colors = TypeColors( + container = artichoke, + content = cosmicLatte, + gradientEdge = artichoke + ) + ) + Text("A long simple text with custom shape") + Type( + text = "The world is a Vampire! And this text is a example text of Marquee Type to check feature", + shape = 5.cornerRadius(), + colors = TypeColors( + container = artichoke, + content = cosmicLatte, + gradientEdge = artichoke + ) + ) + + Text("An example in a Row Composable") + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Type( + modifier = Modifier.weight(1f), + text = "The world is a Vampire! And this text is a example text of Marquee Type to check feature", + shape = 5.cornerRadius(), + colors = TypeColors( + container = artichoke, + content = cosmicLatte, + gradientEdge = artichoke + ) + ) + Type( + modifier = Modifier.weight(1f), + text = "The world is a Vampire! And this text is a example text of Marquee Type to check feature", + colors = TypeColors( + container = artichoke, + content = cosmicLatte, + gradientEdge = artichoke + ) + ) + } + } } \ No newline at end of file diff --git a/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/foundation/text/MarqueeText.kt b/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/foundation/text/MarqueeText.kt index 40954ced..3727f50c 100644 --- a/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/foundation/text/MarqueeText.kt +++ b/jchucomponents-ui/src/main/kotlin/com/jeluchu/jchucomponents/ui/foundation/text/MarqueeText.kt @@ -98,6 +98,147 @@ import kotlinx.coroutines.delay * */ +@Composable +fun MarqueeText( + text: String, + modifier: Modifier = Modifier, + textModifier: Modifier = Modifier, + gradientEdgeColor: Color = Color.White, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, +) { + val createText = @Composable { localModifier: Modifier -> + Text( + text, + textAlign = textAlign, + modifier = localModifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = 1, + onTextLayout = onTextLayout, + style = style, + ) + } + var offset by remember { mutableStateOf(0) } + val textLayoutInfoState = remember { mutableStateOf(null) } + LaunchedEffect(textLayoutInfoState.value) { + val textLayoutInfo = textLayoutInfoState.value ?: return@LaunchedEffect + if (textLayoutInfo.textWidth <= textLayoutInfo.containerWidth) return@LaunchedEffect + val duration = 7500 * textLayoutInfo.textWidth / textLayoutInfo.containerWidth + val delay = 1000L + + do { + val animation = TargetBasedAnimation( + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = duration, + delayMillis = 1000, + easing = LinearEasing, + ), + repeatMode = RepeatMode.Restart + ), + typeConverter = Int.VectorConverter, + initialValue = 0, + targetValue = -textLayoutInfo.textWidth + ) + val startTime = withFrameNanos { it } + do { + val playTime = withFrameNanos { it } - startTime + offset = (animation.getValueFromNanos(playTime)) + } while (!animation.isFinishedFromNanos(playTime)) + delay(delay) + } while (true) + } + + SubcomposeLayout( + modifier = modifier.clipToBounds() + ) { constraints -> + val infiniteWidthConstraints = constraints.copy(maxWidth = Int.MAX_VALUE) + var mainText = subcompose(MarqueeLayers.MainText) { + createText(textModifier) + }.first().measure(infiniteWidthConstraints) + + var gradient: Placeable? = null + + var secondPlaceableWithOffset: Pair? = null + if (mainText.width <= constraints.maxWidth) { + mainText = subcompose(MarqueeLayers.SecondaryText) { + createText(textModifier.fillMaxWidth()) + }.first().measure(constraints) + textLayoutInfoState.value = null + } else { + val spacing = constraints.maxWidth * 2 / 3 + textLayoutInfoState.value = TextLayoutInfo( + textWidth = mainText.width + spacing, + containerWidth = constraints.maxWidth + ) + val secondTextOffset = mainText.width + offset + spacing + val secondTextSpace = constraints.maxWidth - secondTextOffset + if (secondTextSpace > 0) { + secondPlaceableWithOffset = subcompose(MarqueeLayers.SecondaryText) { + createText(textModifier) + }.first().measure(infiniteWidthConstraints) to secondTextOffset + } + gradient = subcompose(MarqueeLayers.EdgesGradient) { + Row { + GradientEdge(gradientEdgeColor, Color.Transparent) + Spacer(Modifier.weight(1f)) + GradientEdge(Color.Transparent, gradientEdgeColor) + } + }.first().measure(constraints.copy(maxHeight = mainText.height)) + } + + layout( + width = constraints.maxWidth, + height = mainText.height + ) { + mainText.place(offset, 0) + secondPlaceableWithOffset?.let { + it.first.place(it.second, 0) + } + gradient?.place(0, 0) + } + } +} + +@Composable +private fun GradientEdge( + startColor: Color, endColor: Color, +) { + Box( + modifier = Modifier + .width(10.dp) + .fillMaxHeight() + .background( + brush = Brush.horizontalGradient( + 0f to startColor, 1f to endColor, + ) + ) + ) +} + +private enum class MarqueeLayers { MainText, SecondaryText, EdgesGradient } +private data class TextLayoutInfo(val textWidth: Int, val containerWidth: Int) + fun ContentDrawScope.drawFadedEdge( leftEdge: Boolean, edgeWidth: Dp