-
-
Save segunfamisa/8c718604b8618b97fb75b3d6cc10bc22 to your computer and use it in GitHub Desktop.
Exploring custom text rendering in Jetpack Compose
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @Composable | |
| fun AnimatedWarpedText( | |
| text: String, | |
| modifier: Modifier = Modifier, | |
| 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, | |
| maxLines: Int = Int.MAX_VALUE, | |
| style: TextStyle = LocalTextStyle.current, | |
| color: Color = LocalContentColor.current, | |
| ) { | |
| BoxWithConstraints(modifier) { | |
| val scope = this | |
| val density = LocalDensity.current | |
| val mergedStyle = style.merge( | |
| color = color, | |
| textAlign = textAlign ?: TextAlign.Unspecified, | |
| fontSize = fontSize, | |
| fontStyle = fontStyle, | |
| fontWeight = fontWeight, | |
| fontFamily = fontFamily, | |
| letterSpacing = letterSpacing, | |
| textDecoration = textDecoration, | |
| lineHeight = lineHeight, | |
| ) | |
| // measure text, considering the max width constraint | |
| val textMeasurer = rememberTextMeasurer() | |
| val textLayout = remember(text, mergedStyle, scope.constraints) { | |
| textMeasurer.measure( | |
| text = text, | |
| style = mergedStyle, | |
| constraints = scope.constraints | |
| ) | |
| } | |
| val canvasSize = with(density) { | |
| DpSize(textLayout.size.width.toDp(), textLayout.size.height.toDp()) | |
| } | |
| val infiniteTransition = rememberInfiniteTransition() | |
| val sinusoidalAmplitude by | |
| infiniteTransition.animateFloat( | |
| initialValue = with(density) { -5.dp.toPx() }, | |
| targetValue = with(density) { 5.dp.toPx() }, | |
| animationSpec = | |
| infiniteRepeatable( | |
| animation = tween(2000, easing = LinearEasing), | |
| repeatMode = RepeatMode.Reverse, | |
| ), | |
| ) | |
| Canvas(modifier = modifier.size(canvasSize)) { | |
| for (lineIndex in 0 until textLayout.lineCount) { | |
| val startCharIndex = textLayout.getLineStart(lineIndex) | |
| val endCharIndex = textLayout.getLineEnd(lineIndex) | |
| for (charIndex in startCharIndex until endCharIndex) { | |
| val rect = textLayout.getBoundingBox(charIndex) | |
| val char = textLayout.layoutInput.text[charIndex].toString() | |
| withTransform({ | |
| translate( | |
| left = 0f, | |
| top = sinusoidalAmplitude * sin(charIndex * 0.7).toFloat() | |
| ) | |
| }) { | |
| drawText( | |
| textMeasurer = textMeasurer, | |
| text = char, | |
| topLeft = Offset(x = rect.left, y = rect.top), | |
| style = mergedStyle, | |
| overflow = overflow, | |
| softWrap = softWrap, | |
| maxLines = maxLines, | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Accompanying blog post: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @Composable | |
| fun FadedText( | |
| text: String, | |
| modifier: Modifier = Modifier, | |
| 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, | |
| maxLines: Int = Int.MAX_VALUE, | |
| style: TextStyle = LocalTextStyle.current, | |
| color: Color = LocalContentColor.current, | |
| ) { | |
| BoxWithConstraints(modifier) { | |
| val scope = this | |
| val density = LocalDensity.current | |
| val mergedStyle = style.merge( | |
| color = color, | |
| textAlign = textAlign ?: TextAlign.Unspecified, | |
| fontSize = fontSize, | |
| fontStyle = fontStyle, | |
| fontWeight = fontWeight, | |
| fontFamily = fontFamily, | |
| letterSpacing = letterSpacing, | |
| textDecoration = textDecoration, | |
| lineHeight = lineHeight, | |
| ) | |
| // measure text, considering the max width constraint | |
| val textMeasurer = rememberTextMeasurer() | |
| val textLayout = remember(text, mergedStyle, scope.constraints) { | |
| textMeasurer.measure( | |
| text = text, | |
| style = mergedStyle, | |
| constraints = scope.constraints | |
| ) | |
| } | |
| val canvasSize = with(density) { | |
| DpSize(textLayout.size.width.toDp(), textLayout.size.height.toDp()) | |
| } | |
| Canvas(modifier = modifier.size(canvasSize)) { | |
| for (lineIndex in 0 until textLayout.lineCount) { | |
| val startCharIndex = textLayout.getLineStart(lineIndex) | |
| val endCharIndex = textLayout.getLineEnd(lineIndex) | |
| val lineLeftCoordinate = textLayout.getLineLeft(lineIndex) | |
| val lineTopCoordinate = textLayout.getLineTop(lineIndex) | |
| val alpha = mergedStyle.color.alpha * lineIndex.toFloat() / textLayout.lineCount | |
| val lineText = text.substring(startCharIndex, endCharIndex) | |
| drawText( | |
| textMeasurer = textMeasurer, | |
| text = lineText, | |
| topLeft = Offset(x = lineLeftCoordinate, y = lineTopCoordinate), | |
| style = mergedStyle.copy(color = mergedStyle.color.copy(alpha = alpha)), | |
| overflow = overflow, | |
| softWrap = softWrap, | |
| maxLines = maxLines, | |
| ) | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @Composable | |
| fun TypewriterText( | |
| text: String, | |
| modifier: Modifier = Modifier, | |
| 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, | |
| maxLines: Int = Int.MAX_VALUE, | |
| style: TextStyle = LocalTextStyle.current, | |
| color: Color = LocalContentColor.current, | |
| ) { | |
| BoxWithConstraints(modifier) { | |
| val scope = this | |
| val density = LocalDensity.current | |
| val mergedStyle = style.merge( | |
| color = color, | |
| textAlign = textAlign ?: TextAlign.Unspecified, | |
| fontSize = fontSize, | |
| fontStyle = fontStyle, | |
| fontWeight = fontWeight, | |
| fontFamily = fontFamily, | |
| letterSpacing = letterSpacing, | |
| textDecoration = textDecoration, | |
| lineHeight = lineHeight, | |
| ) | |
| // measure text, considering the max width constraint | |
| val textMeasurer = rememberTextMeasurer() | |
| val textLayout = remember(text, mergedStyle, scope.constraints) { | |
| textMeasurer.measure( | |
| text = text, | |
| style = mergedStyle, | |
| constraints = scope.constraints | |
| ) | |
| } | |
| // initialize the animation | |
| val animatedCharacterCount = remember { Animatable(0f) } | |
| LaunchedEffect(text) { | |
| animatedCharacterCount.animateTo( | |
| targetValue = text.length.toFloat(), | |
| animationSpec = tween(durationMillis = text.length * 50, easing = LinearEasing) | |
| ) | |
| } | |
| val canvasSize = with(density) { | |
| DpSize(textLayout.size.width.toDp(), textLayout.size.height.toDp()) | |
| } | |
| Canvas(modifier = modifier.size(canvasSize)) { | |
| val lines = textLayout.lineCount | |
| val visibleChars = animatedCharacterCount.value.toInt() | |
| for (lineIndex in 0 until lines) { | |
| val startCharIndex = textLayout.getLineStart(lineIndex) | |
| val endCharIndex = textLayout.getLineEnd(lineIndex) | |
| if (visibleChars > startCharIndex) { | |
| val topCoordinate = textLayout.getLineTop(lineIndex) | |
| val leftCoordinate = textLayout.getLineLeft(lineIndex) | |
| // we only want to show as far as the end character index | |
| val displayedEndIndex = minOf(endCharIndex, visibleChars) | |
| val displayedText = | |
| textLayout.layoutInput.text.substring(startCharIndex, displayedEndIndex) | |
| drawText( | |
| textMeasurer = textMeasurer, | |
| text = displayedText, | |
| topLeft = Offset(x = leftCoordinate, y = topCoordinate), | |
| style = mergedStyle, | |
| overflow = overflow, | |
| softWrap = softWrap, | |
| maxLines = maxLines, | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @Composable | |
| fun WarpedText( | |
| text: String, | |
| modifier: Modifier = Modifier, | |
| 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, | |
| maxLines: Int = Int.MAX_VALUE, | |
| style: TextStyle = LocalTextStyle.current, | |
| color: Color = LocalContentColor.current, | |
| ) { | |
| BoxWithConstraints(modifier) { | |
| val scope = this | |
| val density = LocalDensity.current | |
| val maxWidthPx = with(density) { scope.maxWidth.toPx() } | |
| val mergedStyle = style.merge( | |
| color = color, | |
| textAlign = textAlign ?: TextAlign.Unspecified, | |
| fontSize = fontSize, | |
| fontStyle = fontStyle, | |
| fontWeight = fontWeight, | |
| fontFamily = fontFamily, | |
| letterSpacing = letterSpacing, | |
| textDecoration = textDecoration, | |
| lineHeight = lineHeight, | |
| ) | |
| // measure text, considering the max width constraint | |
| val textMeasurer = rememberTextMeasurer() | |
| val textLayout = remember(text, mergedStyle, constraints) { | |
| textMeasurer.measure( | |
| text = text, | |
| style = mergedStyle, | |
| constraints = constraints | |
| ) | |
| } | |
| val canvasSize = with(density) { | |
| DpSize(textLayout.size.width.toDp(), textLayout.size.height.toDp()) | |
| } | |
| Canvas(modifier = modifier.size(canvasSize)) { | |
| for (lineIndex in 0 until textLayout.lineCount) { | |
| val startCharIndex = textLayout.getLineStart(lineIndex) | |
| val endCharIndex = textLayout.getLineEnd(lineIndex) | |
| for (charIndex in startCharIndex until endCharIndex) { | |
| val rect = textLayout.getBoundingBox(charIndex) | |
| val char = textLayout.layoutInput.text[charIndex].toString() | |
| withTransform({ | |
| translate( | |
| left = 0f, | |
| top = 5 * sin(charIndex * 0.7).toFloat() | |
| ) | |
| }) { | |
| drawText( | |
| textMeasurer = textMeasurer, | |
| text = char, | |
| topLeft = Offset(x = rect.left, y = rect.top), | |
| style = mergedStyle, | |
| overflow = overflow, | |
| softWrap = softWrap, | |
| maxLines = maxLines, | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment