blob: 88e0baeeb3720ba1d1db305f860a6f30fa7c88d4 [file] [log] [blame]
/*
* Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package org.jetbrains.kotlinx.animation
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import kotlinx.coroutines.*
import kotlinx.coroutines.android.*
import java.util.*
sealed class AnimatedShape {
var x = 0.5f // 0 .. 1
var y = 0.5f // 0 .. 1
var color = Color.BLACK
var r = 0.05f
}
class AnimatedCircle : AnimatedShape()
class AnimatedSquare : AnimatedShape()
private val NO_SHAPES = emptySet<AnimatedShape>()
class AnimationView(
context: Context, attributeSet: AttributeSet
) : View(context, attributeSet), Observer<Set<AnimatedShape>> {
private var shapes = NO_SHAPES
private val paint = Paint()
private val rect = RectF()
override fun onChanged(shapes: Set<AnimatedShape>?) {
this.shapes = shapes ?: NO_SHAPES
invalidate()
}
override fun onDraw(canvas: Canvas) {
val scale = minOf(width, height) / 2.0f
shapes.forEach { shape ->
val x = (shape.x - 0.5f) * scale + width / 2
val y = (shape.y - 0.5f) * scale + height / 2
val r = shape.r * scale
rect.set(x - r, y - r, x + r, y + r)
paint.color = shape.color
when (shape) {
is AnimatedCircle -> canvas.drawArc(rect, 0.0f, 360.0f, true, paint)
is AnimatedSquare -> canvas.drawRect(rect, paint)
}
}
}
}
private val rnd = Random()
class AnimationModel : ViewModel(), CoroutineScope {
override val coroutineContext = Job() + Dispatchers.Main
private val shapes = MutableLiveData<Set<AnimatedShape>>()
fun observe(owner: LifecycleOwner, observer: Observer<Set<AnimatedShape>>) =
shapes.observe(owner, observer)
fun update(shape: AnimatedShape) {
val old = shapes.value ?: NO_SHAPES
shapes.value = if (shape in old) old else old + shape
}
fun addAnimation() {
launch {
animateShape(if (rnd.nextBoolean()) AnimatedCircle() else AnimatedSquare())
}
}
fun clearAnimations() {
coroutineContext.cancelChildren()
shapes.value = NO_SHAPES
}
}
private fun norm(x: Float, y: Float) = Math.hypot(x.toDouble(), y.toDouble()).toFloat()
private const val ACC = 1e-18f
private const val MAX_SPEED = 2e-9f // in screen_fraction/nanos
private const val INIT_POS = 0.8f
private fun Random.nextColor() = Color.rgb(nextInt(256), nextInt(256), nextInt(256))
private fun Random.nextPos() = nextFloat() * INIT_POS + (1 - INIT_POS) / 2
private fun Random.nextSpeed() = nextFloat() * MAX_SPEED - MAX_SPEED / 2
suspend fun AnimationModel.animateShape(shape: AnimatedShape) {
shape.x = rnd.nextPos()
shape.y = rnd.nextPos()
shape.color = rnd.nextColor()
var sx = rnd.nextSpeed()
var sy = rnd.nextSpeed()
var time = System.nanoTime() // nanos
var checkTime = time
while (true) {
val dt = time.let { old -> awaitFrame().also { time = it } - old }
if (dt > 0.5e9) continue // don't animate through over a half second lapses
val dx = shape.x - 0.5f
val dy = shape.y - 0.5f
val dn = norm(dx, dy)
sx -= dx / dn * ACC * dt
sy -= dy / dn * ACC * dt
val sn = norm(sx, sy)
val trim = sn.coerceAtMost(MAX_SPEED)
sx = sx / sn * trim
sy = sy / sn * trim
shape.x += sx * dt
shape.y += sy * dt
update(shape)
// check once a second
if (time > checkTime + 1e9) {
checkTime = time
when (rnd.nextInt(20)) { // roll d20
0 -> {
animateColor(shape) // wait a second & animate color
time = awaitFrame() // and sync with next frame
}
1 -> { // random speed change
sx = rnd.nextSpeed()
sy = rnd.nextSpeed()
}
}
}
}
}
suspend fun AnimationModel.animateColor(shape: AnimatedShape) {
val duration = 1e9f
val startTime = System.nanoTime()
val aColor = shape.color
val bColor = rnd.nextColor()
while (true) {
val time = awaitFrame()
val b = (time - startTime) / duration
if (b >= 1.0f) break
val a = 1 - b
shape.color = Color.rgb(
(Color.red(bColor) * b + Color.red(aColor) * a).toInt(),
(Color.green(bColor) * b + Color.green(aColor) * a).toInt(),
(Color.blue(bColor) * b + Color.blue(aColor) * a).toInt()
)
update(shape)
}
shape.color = bColor
update(shape)
}