Skip to content

Instantly share code, notes, and snippets.

@alexjlockwood
Last active September 26, 2020 17:52
Show Gist options
  • Save alexjlockwood/a7e778ea263d251d4afdf694aed0f3f6 to your computer and use it in GitHub Desktop.
Save alexjlockwood/a7e778ea263d251d4afdf694aed0f3f6 to your computer and use it in GitHub Desktop.
A circle square animation implemented using Jetpack Compose. Inspired by @beesandbombs (twitter.com/beesandbombs).
package com.alexjlockwood.circlesquare
import androidx.compose.animation.animatedFloat
import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.repeatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.runtime.onActive
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.drawscope.withTransform
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
import kotlin.math.sqrt
private const val PI = Math.PI.toFloat()
private const val HALF_PI = PI / 2
@Composable
fun CircleSquare(modifier: Modifier = Modifier) {
val animatedProgress = animatedFloat(0f)
onActive {
animatedProgress.animateTo(
targetValue = 1f,
anim = repeatable(
iterations = AnimationConstants.Infinite,
animation = tween(durationMillis = 2500, easing = LinearEasing),
),
)
}
val t = animatedProgress.value
Canvas(modifier = modifier) {
translate(size.width / 2f, size.height / 2f) {
if (t <= 0.5) {
val tt = map(t, 0f, 0.5f, 0f, 1f)
val rotation = 90f * ease(tt, 3f)
rotate(rotation, 0f, 0f) {
drawCircles(270f, -360f * ease(tt, 3f))
}
} else {
val tt = map(t, 0.5f, 1f, 0f, 1f)
val rotation = -90f * ease(tt, 3f)
rotate(rotation, 0f, 0f) {
drawCircles(360f, 0f)
}
rotate(-rotation, 0f, 0f) {
val rectSize = 2 * size.circleRadius()
drawRect(
color = Color.White,
topLeft = Offset(-rectSize / 2f, -rectSize / 2f),
size = Size(rectSize, rectSize),
)
}
}
}
}
}
private fun DrawScope.drawCircles(sweepAngle: Float, rotation: Float) {
val circleRadius = size.circleRadius()
for (i in 0 until 4) {
val r = circleRadius * sqrt(2f)
val theta = (HALF_PI + PI * i) / 2f
val tx = r * cos(theta)
val ty = r * sin(theta)
withTransform({
translate(-tx, -ty)
rotate(rotation, 0f, 0f)
}, {
val rectSize = 2 * (circleRadius - circleRadius / 16f)
drawArc(
color = Color.Black,
startAngle = 90f * (i + 1),
sweepAngle = sweepAngle,
useCenter = true,
topLeft = Offset(-rectSize / 2f, -rectSize / 2f),
size = Size(rectSize, rectSize),
)
})
}
}
private fun Size.circleRadius(): Float {
return min(width, height) / 4f / sqrt(2f)
}
private fun ease(p: Float, g: Float): Float {
return if (p < 0.5f) {
0.5f * pow(2 * p, g)
} else {
1 - 0.5f * pow(2 * (1 - p), g)
}
}
private fun map(value: Float, start1: Float, stop1: Float, start2: Float, stop2: Float): Float {
return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1))
}
private fun pow(n: Float, e: Float): Float {
return n.pow(e)
}
package com.alexjlockwood.circlesquare
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.unit.dp
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CircleSquare(modifier = Modifier.fillMaxSize().padding(16.dp))
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment