Skip to content

Instantly share code, notes, and snippets.

@alexjlockwood
Last active January 10, 2024 14:07
Show Gist options
  • Save alexjlockwood/e3ff7b9a05dd91ff0955b90950bf7ee5 to your computer and use it in GitHub Desktop.
Save alexjlockwood/e3ff7b9a05dd91ff0955b90950bf7ee5 to your computer and use it in GitHub Desktop.
Kotlin implementation of a Ring of Circles animation, inspired by https://twitter.com/InfinityLoopGIF/status/1101584983259533312
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
private const val N = 16
private const val PERIOD1 = -10000.0
private const val PERIOD2 = -500.0
class RingOfCirclesView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val ringRadius: Double
get() = Math.min(width, height) * 0.35
private val waveRadius: Double
get() = Math.min(width, height) * 0.10
private val ballRadius: Double
get() = waveRadius / 4.0
private val gap: Double
get() = ballRadius / 2.0
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = View.MeasureSpec.getSize(widthMeasureSpec)
val height = View.MeasureSpec.getSize(heightMeasureSpec)
val size = if (width > height) height else width
setMeasuredDimension(size, size)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val now = System.currentTimeMillis()
canvas.save()
canvas.translate(width / 2f, height / 2f)
for (i in 0..N) {
drawCircle(canvas, i, now, false)
}
paint.style = Paint.Style.STROKE
paint.color = Color.WHITE
paint.strokeWidth = (ballRadius + gap * 2).toFloat()
canvas.drawCircle(0f, 0f, ringRadius.toFloat(), paint)
paint.style = Paint.Style.STROKE
paint.color = Color.BLACK
paint.strokeWidth = ballRadius.toFloat()
canvas.drawCircle(0f, 0f, ringRadius.toFloat(), paint)
for (i in 0..N) {
drawCircle(canvas, i, now, true)
}
canvas.restore()
postInvalidateOnAnimation()
}
private fun drawCircle(canvas: Canvas, i: Int, now: Long, above: Boolean) {
val angle0 = (i / N.toDouble() + now / PERIOD1) % 1.0 * (2 * Math.PI)
val angle1 = angle0 + now / PERIOD2
if (Math.cos(angle1) < 0 == above) {
return
}
canvas.save()
canvas.rotate(Math.toDegrees(angle0).toFloat())
canvas.translate((ringRadius + Math.sin(angle1) * waveRadius).toFloat(), 0f)
paint.style = Paint.Style.STROKE
paint.color = Color.WHITE
paint.strokeWidth = (gap * 2).toFloat()
canvas.drawCircle(0f, 0f, ballRadius.toFloat(), paint)
paint.style = Paint.Style.FILL
paint.color = Color.BLACK
canvas.drawCircle(0f, 0f, ballRadius.toFloat(), paint)
canvas.restore()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment