diff --git a/build.gradle b/build.gradle
index 1980491..a9cbc77 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,11 +14,15 @@ buildscript {
constraintlayout_version = '2.0.0-beta3'
dynamicanimation_version = '1.0.0'
navigation_version = '2.1.0'
+ espresso_version = '3.2.0'
+ robolectric_version = '4.3.1'
+ androidxtest_version = '1.2.0'
+ androidxtest_junit_version = '1.1.1'
+ mockk_version = '1.9.3'
}
repositories {
google()
jcenter()
-
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
@@ -34,6 +38,11 @@ allprojects {
google()
jcenter()
}
+ configurations.all {
+ resolutionStrategy {
+ force("org.objenesis:objenesis:2.6")
+ }
+ }
}
task clean(type: Delete) {
diff --git a/library/build.gradle b/library/build.gradle
index f9dc356..1d10dfe 100644
--- a/library/build.gradle
+++ b/library/build.gradle
@@ -14,6 +14,8 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
archivesBaseName = "$archivesBaseName-$versionName"
+
+ multiDexEnabled true
}
buildTypes {
@@ -22,6 +24,23 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+
+ testOptions {
+ unitTests {
+ includeAndroidResources true
+ }
+ }
+
+ sourceSets {
+ androidTest {
+ manifest.srcFile 'src/uiTest/AndroidManifest.xml'
+ java.srcDirs += "src/uiTest/java"
+ }
+ test {
+ manifest.srcFile 'src/uiTest/AndroidManifest.xml'
+ java.srcDirs += "src/uiTest/java"
+ }
+ }
}
dependencies {
@@ -32,9 +51,18 @@ dependencies {
implementation "androidx.core:core-ktx:$corektx_version"
api "androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version"
- testImplementation 'junit:junit:4.12'
- androidTestImplementation 'androidx.test:runner:1.2.0'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+ implementation "androidx.multidex:multidex:2.0.1"
+
+ testImplementation "androidx.test.ext:truth:$androidxtest_version"
+ testImplementation "androidx.test.ext:junit:$androidxtest_junit_version"
+ testImplementation "org.robolectric:robolectric:$robolectric_version"
+ testImplementation "io.mockk:mockk:$mockk_version"
+
+ androidTestImplementation "androidx.test.ext:truth:$androidxtest_version"
+ androidTestImplementation "androidx.test.ext:junit:$androidxtest_junit_version"
+ androidTestImplementation "androidx.test:runner:$androidxtest_version"
+ androidTestImplementation "org.robolectric:annotations:$robolectric_version"
+ androidTestImplementation "io.mockk:mockk-android:$mockk_version"
}
repositories {
diff --git a/library/src/androidTest/java/com/github/lcdsmao/springx/ExampleInstrumentedTest.java b/library/src/androidTest/java/com/github/lcdsmao/springx/ExampleInstrumentedTest.java
deleted file mode 100644
index 7fea39c..0000000
--- a/library/src/androidTest/java/com/github/lcdsmao/springx/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.github.lcdsmao.springx;
-
-import android.content.Context;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
- @Test
- public void useAppContext() {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getTargetContext();
-
- assertEquals("com.github.lcdsmao.springx.test", appContext.getPackageName());
- }
-}
diff --git a/library/src/androidTest/java/com/github/lcdsmao/springx/InstrumentationUiTestScope.kt b/library/src/androidTest/java/com/github/lcdsmao/springx/InstrumentationUiTestScope.kt
new file mode 100644
index 0000000..be63732
--- /dev/null
+++ b/library/src/androidTest/java/com/github/lcdsmao/springx/InstrumentationUiTestScope.kt
@@ -0,0 +1,24 @@
+package com.github.lcdsmao.springx
+
+import androidx.test.platform.app.InstrumentationRegistry
+
+@Suppress("unused")
+class InstrumentationUiTestScope : UiTestScope {
+
+ override fun runOnMainSync(block: () -> Unit) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ block.invoke()
+ }
+ }
+
+ override fun waitForIdleSync() {
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ }
+
+ class Runner : UiTestScope.Runner {
+
+ override fun runUiTest(block: () -> Unit) {
+ block.invoke()
+ }
+ }
+}
diff --git a/library/src/main/res/layout/activity_animation.xml b/library/src/main/res/layout/activity_animation.xml
new file mode 100644
index 0000000..5ff64b9
--- /dev/null
+++ b/library/src/main/res/layout/activity_animation.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/library/src/test/java/com/github/lcdsmao/springx/ExampleUnitTest.java b/library/src/test/java/com/github/lcdsmao/springx/ExampleUnitTest.java
deleted file mode 100644
index 160a883..0000000
--- a/library/src/test/java/com/github/lcdsmao/springx/ExampleUnitTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.github.lcdsmao.springx;
-
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see Testing documentation
- */
-public class ExampleUnitTest {
- @Test
- public void addition_isCorrect() {
- assertEquals(4, 2 + 2);
- }
-}
diff --git a/library/src/test/java/com/github/lcdsmao/springx/UnitUiTestScope.kt b/library/src/test/java/com/github/lcdsmao/springx/UnitUiTestScope.kt
new file mode 100644
index 0000000..ec85217
--- /dev/null
+++ b/library/src/test/java/com/github/lcdsmao/springx/UnitUiTestScope.kt
@@ -0,0 +1,46 @@
+package com.github.lcdsmao.springx
+
+import android.os.Handler
+import android.os.Looper
+import org.robolectric.shadows.ShadowLooper
+import java.util.concurrent.CountDownLatch
+import kotlin.concurrent.thread
+
+@Suppress("unused")
+class UnitUiTestScope : UiTestScope {
+
+ private val mainHandler = Handler(Looper.getMainLooper())
+
+ override fun runOnMainSync(block: () -> Unit) {
+ val latch = CountDownLatch(1)
+ mainHandler.post {
+ try {
+ block.invoke()
+ } finally {
+ latch.countDown()
+ }
+ }
+ latch.await()
+ }
+
+ override fun waitForIdleSync() {
+ val latch = CountDownLatch(1)
+ mainHandler.post {
+ ShadowLooper.idleMainLooper()
+ latch.countDown()
+ }
+ latch.await()
+ }
+
+ class Runner : UiTestScope.Runner {
+
+ override fun runUiTest(block: () -> Unit) {
+ val testThread = thread(name = "Unit UI Test Thread") {
+ block.invoke()
+ }
+ while (testThread.isAlive) {
+ ShadowLooper.idleMainLooper()
+ }
+ }
+ }
+}
diff --git a/library/src/uiTest/AndroidManifest.xml b/library/src/uiTest/AndroidManifest.xml
new file mode 100644
index 0000000..4353500
--- /dev/null
+++ b/library/src/uiTest/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/library/src/uiTest/java/com/github/lcdsmao/springx/AnimationActivity.kt b/library/src/uiTest/java/com/github/lcdsmao/springx/AnimationActivity.kt
new file mode 100644
index 0000000..8530ff8
--- /dev/null
+++ b/library/src/uiTest/java/com/github/lcdsmao/springx/AnimationActivity.kt
@@ -0,0 +1,5 @@
+package com.github.lcdsmao.springx
+
+import androidx.appcompat.app.AppCompatActivity
+
+class AnimationActivity : AppCompatActivity(R.layout.activity_animation)
diff --git a/library/src/uiTest/java/com/github/lcdsmao/springx/UiTestScope.kt b/library/src/uiTest/java/com/github/lcdsmao/springx/UiTestScope.kt
new file mode 100644
index 0000000..238ee15
--- /dev/null
+++ b/library/src/uiTest/java/com/github/lcdsmao/springx/UiTestScope.kt
@@ -0,0 +1,28 @@
+package com.github.lcdsmao.springx
+
+interface UiTestScope {
+
+ interface Runner {
+ fun runUiTest(block: () -> Unit)
+ }
+
+ fun runOnMainSync(block: () -> Unit)
+ fun waitForIdleSync()
+}
+
+fun runUiTest(uiTestScope: UiTestScope.() -> Unit) {
+ val (scope, runner) = getUiTestDelegate()
+ runner.runUiTest { uiTestScope(scope) }
+}
+
+private fun getUiTestDelegate(): Pair {
+ val testScopeName =
+ if (System.getProperty("java.runtime.name")!!.toLowerCase().contains("android")) {
+ "com.github.lcdsmao.springx.InstrumentationUiTestScope"
+ } else {
+ "com.github.lcdsmao.springx.UnitUiTestScope"
+ }
+ val runnerName = "$testScopeName\$Runner"
+ return Class.forName(testScopeName).newInstance() as UiTestScope to
+ Class.forName(runnerName).newInstance() as UiTestScope.Runner
+}
diff --git a/library/src/uiTest/java/com/github/lcdsmao/springx/ViewPropertySpringAnimatorTest.kt b/library/src/uiTest/java/com/github/lcdsmao/springx/ViewPropertySpringAnimatorTest.kt
new file mode 100644
index 0000000..4684bac
--- /dev/null
+++ b/library/src/uiTest/java/com/github/lcdsmao/springx/ViewPropertySpringAnimatorTest.kt
@@ -0,0 +1,251 @@
+package com.github.lcdsmao.springx
+
+import android.view.View
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.SpringForce
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth
+import io.mockk.mockk
+import io.mockk.verify
+import io.mockk.verifySequence
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.LooperMode
+
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+class ViewPropertySpringAnimatorTest {
+
+ @get:Rule
+ val activityRule = ActivityScenarioRule(AnimationActivity::class.java)
+
+ private lateinit var animView: View
+
+ @Before
+ fun setup() {
+ activityRule.scenario.onActivity {
+ animView = it.findViewById(R.id.anim_view)
+ }
+ }
+
+ @Test
+ fun testAnimateCommonProperty() = runUiTest {
+ val cases = listOf(
+ DynamicAnimation.X to 100f,
+ DynamicAnimation.Y to -100f,
+ DynamicAnimation.Z to 20f,
+ DynamicAnimation.TRANSLATION_X to -200f,
+ DynamicAnimation.TRANSLATION_Y to 200f,
+ DynamicAnimation.TRANSLATION_Z to -20f,
+ DynamicAnimation.ROTATION to 45f,
+ DynamicAnimation.ROTATION_X to 30f,
+ DynamicAnimation.ROTATION_Y to 60f,
+ DynamicAnimation.SCALE_X to 4f,
+ DynamicAnimation.SCALE_Y to 8f,
+ DynamicAnimation.ALPHA to 0.5f
+ )
+
+ cases.forEach { (p, v) ->
+ val listener = mockk>(relaxed = true)
+ val anim = ViewPropertySpringAnimator(animView).animate(p, v, false)
+ .setListener(listener)
+ runOnMainSync {
+ anim.start()
+ Truth.assertThat(anim.isRunning).isTrue()
+ anim.skipToEnd()
+ }
+ verify(exactly = 1) { listener.onAnimationStart(anim) }
+ verify(exactly = 1, timeout = 1000) { listener.onAnimationEnd(anim) }
+ Truth.assertThat(anim.isRunning).isFalse()
+ Truth.assertThat(p.getValue(animView)).isEqualTo(v)
+ }
+ }
+
+ @Test
+ fun testAnimateByCommonProperty() = runUiTest {
+ val cases = listOf(
+ DynamicAnimation.X to 100f,
+ DynamicAnimation.Y to -100f,
+ DynamicAnimation.Z to 20f,
+ DynamicAnimation.TRANSLATION_X to -200f,
+ DynamicAnimation.TRANSLATION_Y to 200f,
+ DynamicAnimation.TRANSLATION_Z to -20f,
+ DynamicAnimation.ROTATION to 45f,
+ DynamicAnimation.ROTATION_X to 30f,
+ DynamicAnimation.ROTATION_Y to 60f,
+ DynamicAnimation.SCALE_X to 4f,
+ DynamicAnimation.SCALE_Y to 8f,
+ DynamicAnimation.ALPHA to -0.5f
+ )
+
+ cases.forEach { (p, v) ->
+ val oldValue = p.getValue(animView)
+ val listener = mockk>(relaxed = true)
+ val anim = ViewPropertySpringAnimator(animView).animate(p, v, true)
+ .setListener(listener)
+ runOnMainSync {
+ anim.start()
+ Truth.assertThat(anim.isRunning).isTrue()
+ anim.skipToEnd()
+ }
+ verify(exactly = 1) { listener.onAnimationStart(anim) }
+ verify(exactly = 1, timeout = 1000) { listener.onAnimationEnd(anim) }
+ Truth.assertThat(anim.isRunning).isFalse()
+ Truth.assertThat(p.getValue(animView)).isEqualTo(oldValue + v)
+ }
+ }
+
+ @Test
+ fun testAnimateCustomProperty() = runUiTest {
+ val property = object : FloatPropertyCompat("Custom") {
+
+ private var value = 50f
+
+ override fun getValue(view: View): Float {
+ return value
+ }
+
+ override fun setValue(view: View, value: Float) {
+ this.value = value
+ }
+ }
+ val listener = mockk>(relaxed = true)
+ val anim = ViewPropertySpringAnimator(animView).animateProperty(property, 100f)
+ .setListener(listener)
+ runOnMainSync {
+ anim.start()
+ Truth.assertThat(anim.isRunning).isTrue()
+ anim.skipToEnd()
+ }
+ verify(exactly = 1) { listener.onAnimationStart(anim) }
+ verify(exactly = 1, timeout = 1000) { listener.onAnimationEnd(anim) }
+ Truth.assertThat(anim.isRunning).isFalse()
+ Truth.assertThat(property.getValue(animView)).isEqualTo(100f)
+ }
+
+ @Test
+ fun testAnimateByCustomProperty() = runUiTest {
+ val property = object : FloatPropertyCompat("Custom") {
+
+ private var value = 50f
+
+ override fun getValue(view: View): Float {
+ return value
+ }
+
+ override fun setValue(view: View, value: Float) {
+ this.value = value
+ }
+ }
+ val listener = mockk>(relaxed = true)
+ val anim = ViewPropertySpringAnimator(animView).animatePropertyBy(property, 100f)
+ .setListener(listener)
+ runOnMainSync {
+ anim.start()
+ Truth.assertThat(anim.isRunning).isTrue()
+ anim.skipToEnd()
+ }
+ verify(exactly = 1) { listener.onAnimationStart(anim) }
+ verify(exactly = 1, timeout = 1000) { listener.onAnimationEnd(anim) }
+ Truth.assertThat(anim.isRunning).isFalse()
+ Truth.assertThat(property.getValue(animView)).isEqualTo(50f + 100f)
+ }
+
+ @Test
+ fun testAnimatorReuse() = runUiTest {
+ val anim1 = animView.spring().translationX(-100f)
+ val listener = mockk>(relaxed = true)
+ anim1.setListener(listener)
+ runOnMainSync { anim1.start() }
+
+ val anim2 = animView.spring().translationX(100f)
+ runOnMainSync { anim2.start() }
+ Truth.assertThat(anim1).isSameAs(anim2)
+
+ runOnMainSync { anim1.skipToEnd() }
+ verify(exactly = 1, timeout = 1000) { listener.onAnimationEnd(anim1) }
+ Truth.assertThat(animView.translationX).isEqualTo(100f)
+ }
+
+ @Test
+ @Ignore("Fix the implementation of [ViewPropertySpringAnimator] to let this test pass")
+ fun testDampingRatio() = runUiTest {
+ val onEnd = mockk<(Int) -> Unit>(relaxed = true)
+ val anim = animView.spring()
+ .defaultDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
+ .rotation(100f) {
+ dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY
+ onEnd { _, _, _, _ -> onEnd.invoke(2) }
+ }
+ .translationY(100f) {
+ dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY
+ onEnd { _, _, _, _ -> onEnd.invoke(1) }
+ }
+ .translationX(100f) {
+ onEnd { _, _, _, _ -> onEnd.invoke(0) }
+ }
+ .setListener(onEnd = { onEnd.invoke(3) })
+ runOnMainSync { anim.start() }
+ verify(exactly = 4, timeout = 1000L) { onEnd.invoke(any()) }
+ verifySequence {
+ onEnd.invoke(0)
+ onEnd.invoke(1)
+ onEnd.invoke(2)
+ onEnd.invoke(3)
+ }
+ }
+
+ @Test
+ @Ignore("Fix the implementation of [ViewPropertySpringAnimator] to let this test pass")
+ fun testStiffness() = runUiTest {
+ val onEnd = mockk<(Int) -> Unit>(relaxed = true)
+ val anim = animView.spring()
+ .defaultStiffness(SpringForce.STIFFNESS_HIGH)
+ .rotation(100f) {
+ stiffness = SpringForce.STIFFNESS_MEDIUM
+ onEnd { _, _, _, _ -> onEnd.invoke(2) }
+ }
+ .translationY(100f) {
+ stiffness = SpringForce.STIFFNESS_LOW
+ onEnd { _, _, _, _ -> onEnd.invoke(1) }
+ }
+ .translationX(100f) {
+ onEnd { _, _, _, _ -> onEnd.invoke(0) }
+ }
+ .setListener(onEnd = { onEnd.invoke(3) })
+ runOnMainSync { anim.start() }
+ verify(exactly = 4, timeout = 1000L) { onEnd.invoke(any()) }
+ verifySequence {
+ onEnd.invoke(0)
+ onEnd.invoke(1)
+ onEnd.invoke(2)
+ onEnd.invoke(3)
+ }
+ }
+
+ private fun ViewPropertySpringAnimator.animate(
+ property: DynamicAnimation.ViewProperty,
+ value: Float,
+ relative: Boolean
+ ) = apply {
+ when (property) {
+ DynamicAnimation.X -> if (relative) xBy(value) else x(value)
+ DynamicAnimation.Y -> if (relative) yBy(value) else y(value)
+ DynamicAnimation.Z -> if (relative) zBy(value) else z(value)
+ DynamicAnimation.TRANSLATION_X -> if (relative) translationXBy(value) else translationX(value)
+ DynamicAnimation.TRANSLATION_Y -> if (relative) translationYBy(value) else translationY(value)
+ DynamicAnimation.TRANSLATION_Z -> if (relative) translationZBy(value) else translationZ(value)
+ DynamicAnimation.ROTATION -> if (relative) rotationBy(value) else rotation(value)
+ DynamicAnimation.ROTATION_X -> if (relative) rotationXBy(value) else rotationX(value)
+ DynamicAnimation.ROTATION_Y -> if (relative) rotationYBy(value) else rotationY(value)
+ DynamicAnimation.SCALE_X -> if (relative) scaleXBy(value) else scaleX(value)
+ DynamicAnimation.SCALE_Y -> if (relative) scaleYBy(value) else scaleY(value)
+ DynamicAnimation.ALPHA -> if (relative) alphaBy(value) else alpha(value)
+ }
+ }
+}
diff --git a/sample/src/androidTest/java/com/github/lcdsmao/springsample/ExampleInstrumentedTest.kt b/sample/src/androidTest/java/com/github/lcdsmao/springsample/ExampleInstrumentedTest.kt
deleted file mode 100644
index 2998ac0..0000000
--- a/sample/src/androidTest/java/com/github/lcdsmao/springsample/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.lcdsmao.springsample
-
-import androidx.test.InstrumentationRegistry
-import androidx.test.runner.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getTargetContext()
- assertEquals("com.github.lcdsmao.springsample", appContext.packageName)
- }
-}
diff --git a/sample/src/test/java/com/github/lcdsmao/springsample/ExampleUnitTest.kt b/sample/src/test/java/com/github/lcdsmao/springsample/ExampleUnitTest.kt
deleted file mode 100644
index cf796de..0000000
--- a/sample/src/test/java/com/github/lcdsmao/springsample/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.github.lcdsmao.springsample
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}