Skip to content

Commit d1f50a3

Browse files
committed
Support Android UI.awaitFrame for animation;
sample coroutine-based animation application for Android.
1 parent 74619c1 commit d1f50a3

File tree

30 files changed

+642
-1
lines changed

30 files changed

+642
-1
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
apply plugin: 'com.android.application'
2+
apply plugin: 'kotlin-android'
3+
apply plugin: 'kotlin-android-extensions'
4+
5+
android {
6+
compileSdkVersion 26
7+
defaultConfig {
8+
applicationId "org.jetbrains.kotlinx.animation"
9+
minSdkVersion 15
10+
targetSdkVersion 26
11+
versionCode 1
12+
versionName "1.0"
13+
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
14+
}
15+
buildTypes {
16+
release {
17+
minifyEnabled false
18+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19+
}
20+
}
21+
}
22+
23+
dependencies {
24+
implementation fileTree(dir: 'libs', include: ['*.jar'])
25+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
26+
implementation 'com.android.support:appcompat-v7:26.1.0'
27+
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
28+
implementation 'com.android.support:design:26.1.0'
29+
implementation "android.arch.lifecycle:extensions:1.0.0"
30+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
31+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
32+
33+
testImplementation 'junit:junit:4.12'
34+
androidTestImplementation 'com.android.support.test:runner:1.0.1'
35+
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
36+
}
37+
38+
kotlin {
39+
experimental {
40+
coroutines "enable"
41+
}
42+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-keepclassmembernames class kotlinx.** {
2+
volatile <fields>;
3+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="org.jetbrains.kotlinx.animation">
4+
5+
<application
6+
android:allowBackup="true"
7+
android:icon="@mipmap/ic_launcher"
8+
android:label="@string/app_name"
9+
android:roundIcon="@mipmap/ic_launcher_round"
10+
android:supportsRtl="true"
11+
android:theme="@style/AppTheme">
12+
<activity
13+
android:name="org.jetbrains.kotlinx.animation.MainActivity"
14+
android:label="@string/app_name"
15+
android:theme="@style/AppTheme.NoActionBar">
16+
<intent-filter>
17+
<action android:name="android.intent.action.MAIN" />
18+
<category android:name="android.intent.category.LAUNCHER" />
19+
</intent-filter>
20+
</activity>
21+
</application>
22+
23+
</manifest>
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package org.jetbrains.kotlinx.animation
2+
3+
import android.arch.lifecycle.LifecycleOwner
4+
import android.arch.lifecycle.MutableLiveData
5+
import android.arch.lifecycle.Observer
6+
import android.arch.lifecycle.ViewModel
7+
import android.content.Context
8+
import android.graphics.Canvas
9+
import android.graphics.Color
10+
import android.graphics.Paint
11+
import android.graphics.RectF
12+
import android.util.AttributeSet
13+
import android.view.View
14+
import kotlinx.coroutines.experimental.Job
15+
import kotlinx.coroutines.experimental.android.UI
16+
import kotlinx.coroutines.experimental.launch
17+
import java.util.*
18+
19+
sealed class AnimatedShape {
20+
var x = 0.5f // 0 .. 1
21+
var y = 0.5f // 0 .. 1
22+
var color = Color.BLACK
23+
var r = 0.05f
24+
}
25+
26+
class AnimatedCircle : AnimatedShape()
27+
class AnimatedSquare : AnimatedShape()
28+
29+
private val NO_SHAPES = emptySet<AnimatedShape>()
30+
31+
class AnimationView(
32+
context: Context, attributeSet: AttributeSet
33+
) : View(context, attributeSet), Observer<Set<AnimatedShape>> {
34+
private var shapes = NO_SHAPES
35+
private val paint = Paint()
36+
private val rect = RectF()
37+
38+
override fun onChanged(shapes: Set<AnimatedShape>?) {
39+
this.shapes = shapes ?: NO_SHAPES
40+
invalidate()
41+
}
42+
43+
override fun onDraw(canvas: Canvas) {
44+
val scale = minOf(width, height) / 2.0f
45+
shapes.forEach { shape ->
46+
val x = (shape.x - 0.5f) * scale + width / 2
47+
val y = (shape.y - 0.5f) * scale + height / 2
48+
val r = shape.r * scale
49+
rect.set(x - r, y - r, x + r, y + r)
50+
paint.color = shape.color
51+
when (shape) {
52+
is AnimatedCircle -> canvas.drawArc(rect, 0.0f, 360.0f, true, paint)
53+
is AnimatedSquare -> canvas.drawRect(rect, paint)
54+
}
55+
}
56+
}
57+
}
58+
59+
private val rnd = Random()
60+
61+
class AnimationModel : ViewModel() {
62+
private val shapes = MutableLiveData<Set<AnimatedShape>>()
63+
private val jobs = arrayListOf<Job>()
64+
65+
fun observe(owner: LifecycleOwner, observer: Observer<Set<AnimatedShape>>) =
66+
shapes.observe(owner, observer)
67+
68+
fun update(shape: AnimatedShape) {
69+
val old = shapes.value ?: NO_SHAPES
70+
shapes.value = if (shape in old) old else old + shape
71+
}
72+
73+
fun addAnimation() {
74+
jobs += launch(UI) {
75+
animateShape(if (rnd.nextBoolean()) AnimatedCircle() else AnimatedSquare())
76+
}
77+
}
78+
79+
fun clearAnimations() {
80+
jobs.forEach { it.cancel() }
81+
shapes.value = NO_SHAPES
82+
}
83+
}
84+
85+
private fun norm(x: Float, y: Float) = Math.hypot(x.toDouble(), y.toDouble()).toFloat()
86+
87+
private const val ACC = 1e-18f
88+
private const val MAX_SPEED = 2e-9f // in screen_fraction/nanos
89+
private const val INIT_POS = 0.8f
90+
91+
private fun Random.nextColor() = Color.rgb(nextInt(256), nextInt(256), nextInt(256))
92+
private fun Random.nextPos() = nextFloat() * INIT_POS + (1 - INIT_POS) / 2
93+
private fun Random.nextSpeed() = nextFloat() * MAX_SPEED - MAX_SPEED / 2
94+
95+
suspend fun AnimationModel.animateShape(shape: AnimatedShape) {
96+
shape.x = rnd.nextPos()
97+
shape.y = rnd.nextPos()
98+
shape.color = rnd.nextColor()
99+
var sx = rnd.nextSpeed()
100+
var sy = rnd.nextSpeed()
101+
var time = System.nanoTime() // nanos
102+
var checkTime = time
103+
while (true) {
104+
val dt = time.let { old -> UI.awaitFrame().also { time = it } - old }
105+
if (dt > 0.5e9) continue // don't animate through over a half second lapses
106+
val dx = shape.x - 0.5f
107+
val dy = shape.y - 0.5f
108+
val dn = norm(dx, dy)
109+
sx -= dx / dn * ACC * dt
110+
sy -= dy / dn * ACC * dt
111+
val sn = norm(sx, sy)
112+
val trim = sn.coerceAtMost(MAX_SPEED)
113+
sx = sx / sn * trim
114+
sy = sy / sn * trim
115+
shape.x += sx * dt
116+
shape.y += sy * dt
117+
update(shape)
118+
// check once a second
119+
if (time > checkTime + 1e9) {
120+
checkTime = time
121+
when (rnd.nextInt(20)) { // roll d20
122+
0 -> {
123+
animateColor(shape) // wait a second & animate color
124+
time = UI.awaitFrame() // and sync with next frame
125+
}
126+
1 -> { // random speed change
127+
sx = rnd.nextSpeed()
128+
sy = rnd.nextSpeed()
129+
}
130+
}
131+
}
132+
}
133+
}
134+
135+
suspend fun AnimationModel.animateColor(shape: AnimatedShape) {
136+
val duration = 1e9f
137+
val startTime = System.nanoTime()
138+
val aColor = shape.color
139+
val bColor = rnd.nextColor()
140+
while (true) {
141+
val time = UI.awaitFrame()
142+
val b = (time - startTime) / duration
143+
if (b >= 1.0f) break
144+
val a = 1 - b
145+
shape.color = Color.rgb(
146+
(Color.red(bColor) * b + Color.red(aColor) * a).toInt(),
147+
(Color.green(bColor) * b + Color.green(aColor) * a).toInt(),
148+
(Color.blue(bColor) * b + Color.blue(aColor) * a).toInt()
149+
)
150+
update(shape)
151+
}
152+
shape.color = bColor
153+
update(shape)
154+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.jetbrains.kotlinx.animation
2+
3+
import android.arch.lifecycle.ViewModelProviders
4+
import android.os.Bundle
5+
import android.support.v7.app.AppCompatActivity
6+
import kotlinx.android.synthetic.main.activity_main.*
7+
import kotlinx.android.synthetic.main.content_main.*
8+
9+
class MainActivity : AppCompatActivity() {
10+
override fun onCreate(savedInstanceState: Bundle?) {
11+
super.onCreate(savedInstanceState)
12+
setContentView(R.layout.activity_main)
13+
setSupportActionBar(toolbar)
14+
15+
val animationModel = ViewModelProviders.of(this).get(AnimationModel::class.java)
16+
animationModel.observe(this, animationView)
17+
18+
addButton.setOnClickListener {
19+
animationModel.addAnimation()
20+
}
21+
22+
removeButton.setOnClickListener {
23+
animationModel.clearAnimations()
24+
}
25+
}
26+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:aapt="http://schemas.android.com/aapt"
3+
android:width="108dp"
4+
android:height="108dp"
5+
android:viewportHeight="108"
6+
android:viewportWidth="108">
7+
<path
8+
android:fillType="evenOdd"
9+
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
10+
android:strokeColor="#00000000"
11+
android:strokeWidth="1">
12+
<aapt:attr name="android:fillColor">
13+
<gradient
14+
android:endX="78.5885"
15+
android:endY="90.9159"
16+
android:startX="48.7653"
17+
android:startY="61.0927"
18+
android:type="linear">
19+
<item
20+
android:color="#44000000"
21+
android:offset="0.0" />
22+
<item
23+
android:color="#00000000"
24+
android:offset="1.0" />
25+
</gradient>
26+
</aapt:attr>
27+
</path>
28+
<path
29+
android:fillColor="#FFFFFF"
30+
android:fillType="nonZero"
31+
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
32+
android:strokeColor="#00000000"
33+
android:strokeWidth="1" />
34+
</vector>

0 commit comments

Comments
 (0)