I am trying to create a text effect in Jetpack Compose where the text uses a brush that looks like a set of colors smoothly changing diagonally. The individual letters in the text should move up and down, as well as make half turns.
I have encountered two problems:
Text(
text = word,
fontSize = fontSize,
style = LocalTextStyle.current.copy(brush = gradientBrush),
modifier = modifier
)
Row {
word.forEachIndexed { index, char ->
val angle by rotationAngles[index]
val offset by letterOffsets[index]
Text(
text = char.toString(),
fontSize = fontSize,
style = LocalTextStyle.current.copy(brush = gradientBrush),
modifier = Modifier
.rotate(angle)
.offset(y = offset.dp)
)
}
}
Here are two demonstrations:
How can I create an animation of moving up, down, and half-turning for each letter individually, while applying the brush to the whole word in Jetpack Compose?
Here is the full code:
@Preview(showBackground = true)
@Composable
fun RotatingLettersPreview() {
RotatingLetters("Medium")
}
@OptIn(ExperimentalTextApi::class)
@Composable
fun RotatingLetters(word: String,
fontSize: TextUnit = TextUnit.Unspecified,
modifier: Modifier = Modifier) {
val colorGradient = listOf(
Color(0xFFC0EFFF), // Light blue
Color(0xFF9BDBFB), // Medium blue
Color(0xFF75C2F9), // Dark blue
Color(0xFFF7D7C4), // Light pink
Color(0xFFF5B2C7), // Medium pink
Color(0xFFF28BAE), // Dark pink
Color(0xFFF7F2D4), // Light beige
Color(0xFFF5E0C3), // Medium beige
)
val fontSizeInPx = with(LocalDensity.current) { 30.sp.toPx() }
val doubleFontSizeInPx = fontSizeInPx * 2
val infiniteTransitionForOffset = rememberInfiniteTransition(label = "")
val offsetAnimation by infiniteTransitionForOffset.animateFloat(
initialValue = 0f,
targetValue = doubleFontSizeInPx,
animationSpec = infiniteRepeatable(tween(200000, easing = LinearEasing)), label = ""
)
val gradientBrush = remember(offsetAnimation) {
object : ShaderBrush() {
override fun createShader(size: Size): Shader {
val widthOffset = size.width * offsetAnimation
val heightOffset = size.height * offsetAnimation
return LinearGradientShader(
colors = colorGradient,
from = Offset(widthOffset, heightOffset),
to = Offset(widthOffset + size.width, heightOffset + size.height),
tileMode = TileMode.Mirror
)
}
}
}
val infiniteTransitionForRotation = rememberInfiniteTransition(label = "")
val rotationDirection = remember { mutableStateOf(1) }
val rotationAngles = List(word.length) { index ->
infiniteTransitionForRotation.animateFloat(
initialValue = 0f,
targetValue =
if (index % 2 == 0) 5f * rotationDirection.value else -5f * rotationDirection.value,
animationSpec = infiniteRepeatable(
animation = tween(250, easing = LinearEasing),
repeatMode = RepeatMode.Restart
), label = ""
)
}
LaunchedEffect(key1 = Unit) {
while (true) {
delay(250)
rotationDirection.value *= -1
}
}
val letterOffsets = List(word.length) { index ->
infiniteTransitionForRotation.animateFloat(
initialValue = 0f,
targetValue = if (index % 2 == 0) 2f else -2f,
animationSpec = infiniteRepeatable(
animation = tween(250, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
), label = ""
)
}
Text(
text = word,
fontSize = fontSize,
style = LocalTextStyle.current.copy(brush = gradientBrush),
modifier = modifier
)
/*
Row {
word.forEachIndexed { index, char ->
val angle by rotationAngles[index]
val offset by letterOffsets[index]
Text(
text = char.toString(),
fontSize = fontSize,
style = LocalTextStyle.current.copy(brush = gradientBrush),
modifier = Modifier
.rotate(angle)
.offset(y = offset.dp)
)
}
}
*/
}
you can achieve a similar effect by using a combination of drawWithContent and clipping each letter to create a single continuous gradient across all letters. Here is a conceptual solution:
Create a custom composable that uses Canvas to draw the text manually, allowing you to apply a single gradient brush across the entire word. Use drawWithContent to clip the drawing area to the bounds of each character when drawing them, so they can each animate independently while still sharing the same brush. Here's an example of how you might implement it:
@Composable
fun AnimatedGradientText(word: String, gradientBrush: Brush, fontSize: TextUnit, modifier: Modifier = Modifier) {
// Calculate text size, character positions, etc.
val textPaint = remember {
Paint().asFrameworkPaint().apply {
isAntiAlias = true
style = android.graphics.Paint.Style.FILL
}
}
// For each letter, create a rotation and offset animation.
val rotationAngles = remember { /* ... Your rotation animations ... */ }
val letterOffsets = remember { /* ... Your offset animations ... */ }
Canvas(modifier = modifier) {
// Calculate text size and positions
val textLayoutResult = remember(word, fontSize) { /* ... Measure text ... */ }
word.forEachIndexed { index, char ->
// Get animation values
val angle: Float by rotationAngles[index]
val offset: Float by letterOffsets[index]
// Save the current canvas state
save()
// Position the canvas for this character
translate(left + characterPositions[index], top)
// Apply the rotation transformation
rotate(angle)
// Set the shader to the paint
textPaint.shader = gradientBrush.asAndroidShader()
// Draw this character
drawContext.canvas.nativeCanvas.drawText(
char.toString(),
0f,
0f + offset,
textPaint
)
// Restore the canvas to avoid affecting subsequent characters
restore()
}
}
}