Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animate highlighted text #202

Merged
merged 3 commits into from
Nov 25, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@
*/
package com.infomaniak.swisstransfer.ui.components

import androidx.annotation.FloatRange
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
Expand All @@ -43,7 +48,7 @@ import kotlin.math.sin

private val VERTICAL_PADDING @Composable get() = with(LocalDensity.current) { 3.sp.toPx() }
private val HORIZONTAL_PADDING @Composable get() = with(LocalDensity.current) { 10.sp.toPx() }
private const val ROTATION_ANGLE_DEGREE = -3.0f
private const val ROTATION_ANGLE_DEGREE = -3.0

@Composable
fun HighlightedText(
Expand All @@ -52,27 +57,41 @@ fun HighlightedText(
style: TextStyle,
verticalPadding: Float = VERTICAL_PADDING,
horizontalPadding: Float = HORIZONTAL_PADDING,
angleDegrees: Float = ROTATION_ANGLE_DEGREE,
angleDegrees: Double = ROTATION_ANGLE_DEGREE,
) {
val template = stringResource(templateRes)
val argument = stringResource(argumentRes)
val text = String.format(template, argument)

val highlightedColor = SwissTransferTheme.colors.highlightedColor

var highlightedPath by remember { mutableStateOf<Path?>(null) }
var boundingBoxes by remember { mutableStateOf<List<Rect>>(emptyList()) }
var animationStarted by rememberSaveable { mutableStateOf(false) }

LaunchedEffect(Unit) { animationStarted = true }

val highlightProgress by animateFloatAsState(
targetValue = if (animationStarted) 1f else 0f,
animationSpec = tween(
durationMillis = 600,
easing = FastOutSlowInEasing,
delayMillis = 500,
),
label = "Highlighter progress",
)

Text(
text = text,
style = style,
onTextLayout = { layoutResult ->
val boundingBoxes = layoutResult.getArgumentBoundingBoxes(text, argument)
highlightedPath = boundingBoxes.transformForHighlightedStyle(verticalPadding, horizontalPadding, angleDegrees)
},
onTextLayout = { layoutResult -> boundingBoxes = layoutResult.getArgumentBoundingBoxes(text, argument) },
modifier = Modifier.drawBehind {
highlightedPath?.let {
drawPath(path = it, style = Fill, color = highlightedColor)
}
val highlightedPath = boundingBoxes.transformForHighlightedStyle(
verticalPadding,
horizontalPadding,
angleDegrees,
highlightProgress,
)
drawPath(path = highlightedPath, style = Fill, color = highlightedColor)
},
)
}
Expand All @@ -85,14 +104,20 @@ private fun TextLayoutResult.getArgumentBoundingBoxes(text: String, argument: St
private fun List<Rect>.transformForHighlightedStyle(
verticalPadding: Float,
horizontalPadding: Float,
angleDegrees: Float,
angleDegrees: Double,
@FloatRange(0.0, 1.0) progress: Float,
): Path = Path().apply {
forEach { boundingBox ->
addPath(boundingBox.transformForHighlightedStyle(verticalPadding, horizontalPadding, angleDegrees))
addPath(boundingBox.transformForHighlightedStyle(verticalPadding, horizontalPadding, angleDegrees, progress))
}
}

private fun Rect.transformForHighlightedStyle(verticalPadding: Float, horizontalPadding: Float, angleDegrees: Float): Path {
private fun Rect.transformForHighlightedStyle(
verticalPadding: Float,
horizontalPadding: Float,
angleDegrees: Double,
@FloatRange(0.0, 1.0) progress: Float,
): Path {
return getRotatedRectanglePath(
Rect(
left = left - horizontalPadding,
Expand All @@ -101,14 +126,15 @@ private fun Rect.transformForHighlightedStyle(verticalPadding: Float, horizontal
bottom = bottom + verticalPadding,
),
angleDegrees = angleDegrees,
progress = progress,
)
}

private fun getRotatedRectanglePath(rect: Rect, angleDegrees: Float): Path {
private fun getRotatedRectanglePath(rect: Rect, angleDegrees: Double, @FloatRange(0.0, 1.0) progress: Float): Path {
val centerX = rect.left + (rect.width / 2)
val centerY = rect.top + (rect.height / 2)

val angleRadians = Math.toRadians(angleDegrees.toDouble())
val angleRadians = Math.toRadians(angleDegrees)

// Function to rotate a point around the center
fun rotatePoint(x: Float, y: Float, centerX: Float, centerY: Float, angleRadians: Double): Offset {
Expand All @@ -121,9 +147,12 @@ private fun getRotatedRectanglePath(rect: Rect, angleDegrees: Float): Path {
return Offset(newX, newY)
}

val width = rect.right - rect.left
val right = rect.left + progress * width

val topLeft = rotatePoint(rect.left, rect.top, centerX, centerY, angleRadians)
val topRight = rotatePoint(rect.right, rect.top, centerX, centerY, angleRadians)
val bottomRight = rotatePoint(rect.right, rect.bottom, centerX, centerY, angleRadians)
val topRight = rotatePoint(right, rect.top, centerX, centerY, angleRadians)
val bottomRight = rotatePoint(right, rect.bottom, centerX, centerY, angleRadians)
val bottomLeft = rotatePoint(rect.left, rect.bottom, centerX, centerY, angleRadians)

return Path().apply {
Expand Down
Loading