Skip to content

Instantly share code, notes, and snippets.

@Sal7one
Created May 5, 2024 07:00
Show Gist options
  • Save Sal7one/444ad688bf8f3338fca4d66da904aa2d to your computer and use it in GitHub Desktop.
Save Sal7one/444ad688bf8f3338fca4d66da904aa2d to your computer and use it in GitHub Desktop.
Animated heart jetpack compose, with gaps
// infinite with gap of 1
@Composable
fun AnimatedHeartShapeRaw(
brush: Brush = Brush.verticalGradient(colors = listOf(Color.Magenta, Color.Magenta)), // Default gradient from Magenta to Blue
) {
val animationPercentage = remember { Animatable(0f) } // Animation state from 0 to 1
LaunchedEffect(Unit) {
animationPercentage.animateTo(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 3000, easing = LinearEasing),
repeatMode = RepeatMode.Restart // The animation will go from 0 to 1 and then from 1 to 0
)
)
}
Canvas(modifier = Modifier.fillMaxSize()) {
val width = size.width
val height = size.height
val path = getHeartPath(width, height)
val pathMeasure = PathMeasure(path.asAndroidPath(), false)
val totalLength = pathMeasure.length
val gapSize = totalLength * 0.05f // 5% of total path length for the gap
// Adjusting visible length of the path
val visibleLength = totalLength - gapSize
// Creating a PathEffect to animate a gap moving through the heart
val pathEffect = PathEffect.dashPathEffect(
floatArrayOf(
visibleLength,
gapSize
), // The first float is the visible segment, the second is the invisible segment (the gap)
phase = totalLength - totalLength * animationPercentage.value // The phase moves back and forth with the animation
)
drawPath(
path = path,
brush = brush, // Using a single color for the entire path
style = Stroke(width = 5f, pathEffect = pathEffect)
)
}
}
@Composable
fun AnimatedHeartShapeWithGaps(
numGaps: Int = 3, // Number of gaps
gapSizeFraction: Float = 0.02f, // Fraction of total path length to be used as the gap size
brush: Brush = Brush.verticalGradient(colors = listOf(Color.Magenta, Color.Magenta)), // Default gradient from Magenta to Blue
strokeWidth: Float = 5f,
animationDurationMillis: Int = 2000
) {
val animationPercentage = remember(numGaps){ Animatable(0f) }
LaunchedEffect(numGaps) {
animationPercentage.animateTo(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = animationDurationMillis,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
)
)
}
Canvas(modifier = Modifier.fillMaxSize()) {
val width = size.width
val height = size.height
val path = getHeartPath(width, height)
val pathMeasure = PathMeasure(path.asAndroidPath(), false)
val totalLength = pathMeasure.length
// Calculate the gap size and segment length
val gapSize = totalLength * gapSizeFraction
val segmentLength =
(totalLength - numGaps * gapSize) / (numGaps) // Calculating the visible segments
val pathEffect = PathEffect.dashPathEffect(
floatArrayOf(segmentLength, gapSize), // Visible segment and gap size
phase = -(totalLength - segmentLength - gapSize * numGaps) * animationPercentage.value // Adjusting phase for reverse movement
)
drawPath(
path = path,
brush = brush, // Use the gradient brush for drawing
style = Stroke(width = strokeWidth, pathEffect = pathEffect)
)
}
}
@Composable
fun HeartShapeAnimated(
brush: Brush = Brush.verticalGradient(colors = listOf(Color.Magenta, Color.Magenta)), // Default gradient from Magenta to Blue
animationDurationMillis: Int = 2000
) {
val animationPercentage = remember { Animatable(0f) } // Animation state from 0 to 1
// Start the animation when the composable enters the composition
LaunchedEffect(Unit) {
animationPercentage.animateTo(
targetValue = 1f,
animationSpec = tween(animationDurationMillis)
)
}
Canvas(modifier = Modifier.fillMaxSize()) {
val width = size.width
val height = size.height
val path = getHeartPath(width, height)
val pathLength = PathMeasure(path.asAndroidPath(), false).length
val pathEffect = PathEffect.dashPathEffect(
floatArrayOf(pathLength, pathLength),
phase = pathLength * (1 - animationPercentage.value)
)
drawPath(
path = path,
brush = brush,
style = Stroke(
width = 5f,
pathEffect = pathEffect
) // Ensure the stroke width matches your design
)
}
}
fun getHeartPath(width: Float, height: Float): Path {
return Path().apply {
reset()
// Starting slightly more to the right
moveTo(0.02f * width, 0.3418f * height)
// Adjusting control points to ensure symmetry
cubicTo(
0.02f * width,
0.56075f * height,
0.19432f * width,
0.77611f * height,
0.46971f * width,
0.96114f * height
)
cubicTo(
0.47996f * width,
0.96783f * height,
0.49461f * width,
0.97502f * height,
0.50486f * width,
0.97502f * height
)
cubicTo(
0.51512f * width,
0.97502f * height,
0.52977f * width,
0.96783f * height,
0.54051f * width,
0.96114f * height
)
cubicTo(
0.81541f * width,
0.77611f * height,
0.98973f * width,
0.56075f * height,
0.98973f * width,
0.3418f * height
)
cubicTo(
0.98973f * width,
0.15985f * height,
0.87108f * width,
0.03135f * height,
0.71287f * width,
0.03135f * height
)
cubicTo(
0.62254f * width,
0.03135f * height,
0.5493f * width,
0.07658f * height,
0.50486f * width,
0.14597f * height
)
cubicTo(
0.46141f * width,
0.0771f * height,
0.38719f * width,
0.03135f * height,
0.29686f * width,
0.03135f * height
)
cubicTo(
0.13865f * width,
0.03135f * height,
0.02f * width,
0.15985f * height,
0.02f * width,
0.3418f * height
)
close()
}
}
// Screen code and color pallete
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposeChallengeTheme {
var numOfGaps by remember {
mutableIntStateOf(3)
}
var brush by remember {
mutableStateOf(Brush.linearGradient(listOf(Color.Magenta, Color.Magenta)))
}
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = "Once", textAlign = TextAlign.Center)
Box(
modifier = Modifier.size(110.dp)
) {
HeartShapeAnimated(brush = brush)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = "Animated", textAlign = TextAlign.Center)
Box(
modifier = Modifier.size(110.dp)
) {
AnimatedHeartShapeRaw(brush = brush)
}
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = "With Gaps", textAlign = TextAlign.Center)
Box(
modifier = Modifier.size(110.dp)
) {
AnimatedHeartShapeWithGaps(
numGaps = numOfGaps,
brush = brush
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = { numOfGaps++ }) {
Text(text = "+")
}
Text(
text = "$numOfGaps", style = TextStyle(
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold
)
)
Button(onClick = {
if (numOfGaps != 2)
numOfGaps--
}) {
Text(text = "-")
}
}
}
}
ColorPalette { selectedBrush ->
brush = selectedBrush
}
}
}
}
}
}
}
@Composable
fun ColorPalette(
onColorSelected: (Brush) -> Unit
) {
val colors = listOf(
Color.Red,
Color.Green,
Color.Blue,
Color.Yellow,
Color.Magenta,
Color.Cyan
)
val gradients = listOf(
Brush.horizontalGradient(listOf(Color.Red, Color.Yellow)),
Brush.verticalGradient( listOf(Color.Green, Color.Blue)),
Brush.radialGradient( listOf(Color.Magenta, Color.Cyan))
)
Row(
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
) {
// Displaying solid colors
colors.forEach { color ->
Box(
modifier = Modifier
.size(50.dp)
.background(
Brush.linearGradient(
listOf(
color,
color
)
)
) // Ensuring two colors are present even if they are the same.
.clickable { onColorSelected(Brush.linearGradient(listOf(color, color))) }
)
}
// Displaying gradient brushes
gradients.forEach { gradient ->
Box(
modifier = Modifier
.size(50.dp)
.background(gradient)
.clickable { onColorSelected(gradient) }
)
}
}
}
@Sal7one
Copy link
Author

Sal7one commented May 5, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment