aboutsummaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/email/EmailBuilder.kt4
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/ui/ProgressAnimator.kt179
-rw-r--r--core/src/main/res-public/values/public.xml118
-rw-r--r--core/src/test/kotlin/ca/allanwang/kau/ui/ProgressAnimatorTest.kt75
4 files changed, 325 insertions, 51 deletions
diff --git a/core/src/main/kotlin/ca/allanwang/kau/email/EmailBuilder.kt b/core/src/main/kotlin/ca/allanwang/kau/email/EmailBuilder.kt
index 1f959ab..78661b1 100644
--- a/core/src/main/kotlin/ca/allanwang/kau/email/EmailBuilder.kt
+++ b/core/src/main/kotlin/ca/allanwang/kau/email/EmailBuilder.kt
@@ -69,7 +69,7 @@ class EmailBuilder(val email: String, val subject: String) {
val emailBuilder = StringBuilder()
emailBuilder.append(message).append("\n\n")
if (deviceDetails) {
- val deviceItems = mutableMapOf(
+ val deviceItems = mutableListOf(
"OS Version" to "${System.getProperty("os.version")} (${Build.VERSION.INCREMENTAL})",
"OS SDK" to Build.VERSION.SDK_INT,
"Device (Manufacturer)" to "${Build.DEVICE} (${Build.MANUFACTURER})",
@@ -80,7 +80,7 @@ class EmailBuilder(val email: String, val subject: String) {
if (context is Activity) {
val metric = DisplayMetrics()
context.windowManager.defaultDisplay.getMetrics(metric)
- deviceItems["Screen Dimensions"] = "${metric.widthPixels} x ${metric.heightPixels}"
+ deviceItems.add("Screen Dimensions" to "${metric.widthPixels} x ${metric.heightPixels}")
}
deviceItems.forEach { (k, v) -> emailBuilder.append("$k: $v\n") }
}
diff --git a/core/src/main/kotlin/ca/allanwang/kau/ui/ProgressAnimator.kt b/core/src/main/kotlin/ca/allanwang/kau/ui/ProgressAnimator.kt
index 6f8bbc1..a46e6c5 100644
--- a/core/src/main/kotlin/ca/allanwang/kau/ui/ProgressAnimator.kt
+++ b/core/src/main/kotlin/ca/allanwang/kau/ui/ProgressAnimator.kt
@@ -18,7 +18,8 @@ package ca.allanwang.kau.ui
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
-import android.view.animation.Interpolator
+import androidx.annotation.VisibleForTesting
+import ca.allanwang.kau.kotlin.kauRemoveIf
/**
* Created by Allan Wang on 2017-11-10.
@@ -28,77 +29,157 @@ import android.view.animation.Interpolator
* This differs in that everything can be done with simple listeners, which will be bundled
* and added to the backing [ValueAnimator]
*/
-class ProgressAnimator private constructor(private vararg val values: Float) {
+class ProgressAnimator private constructor() : ValueAnimator() {
companion object {
- inline fun ofFloat(crossinline builder: ProgressAnimator.() -> Unit) = ofFloat(0f, 1f) { builder() }
- fun ofFloat(vararg values: Float, builder: ProgressAnimator.() -> Unit) = ProgressAnimator(*values).apply {
- builder()
- build()
+ fun ofFloat(builder: ProgressAnimator.() -> Unit): ProgressAnimator = ProgressAnimator().apply {
+ setFloatValues(0f, 1f)
+ addUpdateListener { apply(it.animatedValue as Float) }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
+ isCancelled = false
+ startActions.runAll()
+ }
+
+ override fun onAnimationCancel(animation: Animator?) {
+ isCancelled = true
+ cancelActions.runAll()
+ }
+
+ override fun onAnimationEnd(animation: Animator?) {
+ endActions.runAll()
+ isCancelled = false
+ }
+ })
+ }.apply(builder)
+
+ /**
+ * Gets output of a linear function starting at [start] when [progress] is 0 and [end] when [progress] is 1 at point [progress].
+ */
+ fun progress(start: Float, end: Float, progress: Float): Float = start + (end - start) * progress
+
+ fun progress(start: Float, end: Float, progress: Float, min: Float, max: Float): Float = when {
+ min == max -> throw IllegalArgumentException("Progress range cannot be 0 (min == max == $min")
+ progress <= min -> start
+ progress >= max -> end
+ else -> {
+ val trueProgress = (progress - min) / (max - min)
+ start + (end - start) * trueProgress
+ }
}
}
- private val animators: MutableList<(Float) -> Unit> = mutableListOf()
- private val startActions: MutableList<() -> Unit> = mutableListOf()
- private val endActions: MutableList<() -> Unit> = mutableListOf()
+ private val animators: MutableList<ProgressDisposableAction> = mutableListOf()
+ @VisibleForTesting
+ internal val startActions: MutableList<ProgressDisposableRunnable> = mutableListOf()
+ @VisibleForTesting
+ internal val cancelActions: MutableList<ProgressDisposableRunnable> = mutableListOf()
+ @VisibleForTesting
+ internal val endActions: MutableList<ProgressDisposableRunnable> = mutableListOf()
+ var isCancelled: Boolean = false
+ private set
- var duration: Long = -1L
- var interpolator: Interpolator? = null
+ val animatorCount get() = animators.size
/**
- * Add more changes to the [ValueAnimator] before running
+ * Converts an action to a disposable action
*/
- var extraConfigs: ValueAnimator.() -> Unit = {}
+ private fun ProgressAction.asDisposable(): ProgressDisposableAction = { this(it); false }
+
+ private fun ProgressRunnable.asDisposable(): ProgressDisposableRunnable = { this(); false }
/**
- * Range animator. Multiples the range by the current float progress before emission
+ * If [condition] applies, run the animator.
+ * @return [condition]
*/
- fun withAnimator(from: Float, to: Float, animator: (Float) -> Unit) = animators.add {
- val range = to - from
- animator(range * it + from)
+ private fun ProgressAction.runIf(condition: Boolean, progress: Float): Boolean {
+ if (condition) this(progress)
+ return condition
}
- /**
- * Standard animator. Emits progress value as is
- */
- fun withAnimator(animator: (Float) -> Unit) = animators.add(animator)
+ @VisibleForTesting
+ internal fun MutableList<ProgressDisposableRunnable>.runAll() = kauRemoveIf { it() }
- /**
- * Start action to be called once when the animator first begins
- */
- fun withStartAction(action: () -> Unit) = startActions.add(action)
+ @VisibleForTesting
+ internal fun apply(progress: Float) {
+ animators.kauRemoveIf { it(progress) }
+ }
+
+ fun withAnimator(action: ProgressAction) =
+ withDisposableAnimator(action.asDisposable())
/**
- * End action to be called once when the animator ends
+ * Range animator. Multiples the range by the current float progress before emission
*/
- fun withEndAction(action: () -> Unit) = endActions.add(action)
-
- fun build() {
- ValueAnimator.ofFloat(*values).apply {
- if (this@ProgressAnimator.duration > 0L)
- duration = this@ProgressAnimator.duration
- if (this@ProgressAnimator.interpolator != null)
- interpolator = this@ProgressAnimator.interpolator
- addUpdateListener {
- val progress = it.animatedValue as Float
- animators.forEach { it(progress) }
+ fun withAnimator(from: Float, to: Float, action: ProgressAction) =
+ withDisposableAnimator(from, to, action.asDisposable())
+
+ fun withDisposableAnimator(action: ProgressDisposableAction) = animators.add(action)
+
+ fun withDisposableAnimator(from: Float, to: Float, action: ProgressDisposableAction) {
+ if (to != from) {
+ animators.add {
+ action(progress(from, to, it))
}
- addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationStart(animation: Animator?) {
- startActions.forEach { it() }
- }
+ }
+ }
- override fun onAnimationEnd(animation: Animator?) {
- endActions.forEach { it() }
+ fun withRangeAnimator(min: Float, max: Float, start: Float, end: Float, progress: Float, action: ProgressAction) {
+ if (min >= max) {
+ throw IllegalArgumentException("Range animator must have min < max; currently min=$min, max=$max")
+ }
+ withDisposableAnimator {
+ when {
+ it > max -> true
+ it < min -> false
+ else -> {
+ action(progress(start, end, progress, min, max))
+ false
}
+ }
+ }
+ }
- override fun onAnimationCancel(animation: Animator?) {
- endActions.forEach { it() }
- }
- })
- extraConfigs()
- start()
+ fun withPointAnimator(point: Float, action: ProgressAction) {
+ animators.add {
+ action.runIf(it >= point, it)
+ }
+ }
+
+ fun withDelayedStartAction(skipCount: Int, action: ProgressAction) {
+ var count = 0
+ animators.add {
+ action.runIf(count++ >= skipCount, it)
}
}
+
+ /**
+ * Start action to be called once when the animator first begins
+ */
+ fun withStartAction(action: ProgressRunnable) = withDisposableStartAction(action.asDisposable())
+
+ fun withDisposableStartAction(action: ProgressDisposableRunnable) = startActions.add(action)
+
+ fun withCancelAction(action: ProgressRunnable) = withDisposableCancelAction(action.asDisposable())
+
+ fun withDisposableCancelAction(action: ProgressDisposableRunnable) = cancelActions.add(action)
+
+ fun withEndAction(action: ProgressRunnable) = withDisposableEndAction(action.asDisposable())
+
+ fun withDisposableEndAction(action: ProgressDisposableRunnable) = endActions.add(action)
+
+ fun reset() {
+ if (isRunning) cancel()
+ animators.clear()
+ startActions.clear()
+ cancelActions.clear()
+ endActions.clear()
+ isCancelled = false
+ }
}
+
+private typealias ProgressAction = (Float) -> Unit
+private typealias ProgressDisposableAction = (Float) -> Boolean
+private typealias ProgressRunnable = () -> Unit
+private typealias ProgressDisposableRunnable = () -> Boolean
diff --git a/core/src/main/res-public/values/public.xml b/core/src/main/res-public/values/public.xml
new file mode 100644
index 0000000..ea8ed73
--- /dev/null
+++ b/core/src/main/res-public/values/public.xml
@@ -0,0 +1,118 @@
+<resources xmlns:tools='http://schemas.android.com/tools' tools:ignore='ResourceName'>
+<!-- AUTO-GENERATED FILE. DO NOT MODIFY. public.xml is generated by the generatepublicxml gradle task -->
+ <public name='kau_slide_in_top' type='anim' />
+ <public name='kau_slide_in_left' type='anim' />
+ <public name='kau_slide_out_right' type='anim' />
+ <public name='kau_slide_out_right_top' type='anim' />
+ <public name='kau_fade_in' type='anim' />
+ <public name='kau_slide_out_top' type='anim' />
+ <public name='kau_slide_out_bottom' type='anim' />
+ <public name='kau_fade_out' type='anim' />
+ <public name='kau_slide_out_left' type='anim' />
+ <public name='kau_slide_out_left_top' type='anim' />
+ <public name='kau_slide_in_bottom' type='anim' />
+ <public name='kau_slide_in_right' type='anim' />
+ <public name='kau_transparent' type='drawable' />
+ <public name='kau_selectable_white' type='drawable' />
+ <public name='kau_shadow_overlay' type='color' />
+ <public name='kau_activity_horizontal_margin' type='dimen' />
+ <public name='kau_activity_vertical_margin' type='dimen' />
+ <public name='kau_dialog_margin' type='dimen' />
+ <public name='kau_dialog_margin_bottom' type='dimen' />
+ <public name='kau_fab_margin' type='dimen' />
+ <public name='kau_appbar_padding_top' type='dimen' />
+ <public name='kau_splash_logo' type='dimen' />
+ <public name='kau_progress_bar_height' type='dimen' />
+ <public name='kau_account_image_size' type='dimen' />
+ <public name='kau_status_bar_height' type='dimen' />
+ <public name='kau_drag_dismiss_distance' type='dimen' />
+ <public name='kau_drag_dismiss_distance_large' type='dimen' />
+ <public name='kau_spacing_normal' type='dimen' />
+ <public name='kau_spacing_micro' type='dimen' />
+ <public name='kau_spacing_large' type='dimen' />
+ <public name='kau_spacing_xlarge' type='dimen' />
+ <public name='kau_spacing_huge' type='dimen' />
+ <public name='kau_padding_small' type='dimen' />
+ <public name='kau_padding_normal' type='dimen' />
+ <public name='kau_padding_large' type='dimen' />
+ <public name='kau_fab_size' type='dimen' />
+ <public name='kau_fab_radius' type='dimen' />
+ <public name='kau_display_4_text_size' type='dimen' />
+ <public name='kau_avatar_size' type='dimen' />
+ <public name='kau_avatar_bounds' type='dimen' />
+ <public name='kau_avatar_padding' type='dimen' />
+ <public name='kau_avatar_margin' type='dimen' />
+ <public name='kau_avatar_ripple_radius' type='dimen' />
+ <public name='kau_about_app' type='string' />
+ <public name='kau_about_x' type='string' />
+ <public name='kau_add_account' type='string' />
+ <public name='kau_amoled' type='string' />
+ <public name='kau_back' type='string' />
+ <public name='kau_cancel' type='string' />
+ <public name='kau_changelog' type='string' />
+ <public name='kau_close' type='string' />
+ <public name='kau_contact_us' type='string' />
+ <public name='kau_copy' type='string' />
+ <public name='kau_custom' type='string' />
+ <public name='kau_dark' type='string' />
+ <public name='kau_default' type='string' />
+ <public name='kau_do_not_show_again' type='string' />
+ <public name='kau_done' type='string' />
+ <public name='kau_error' type='string' />
+ <public name='kau_exit' type='string' />
+ <public name='kau_exit_confirmation' type='string' />
+ <public name='kau_exit_confirmation_x' type='string' />
+ <public name='kau_glass' type='string' />
+ <public name='kau_got_it' type='string' />
+ <public name='kau_great' type='string' />
+ <public name='kau_hide' type='string' />
+ <public name='kau_light' type='string' />
+ <public name='kau_login' type='string' />
+ <public name='kau_logout' type='string' />
+ <public name='kau_logout_confirm_as_x' type='string' />
+ <public name='kau_lorem_ipsum' type='string' />
+ <public name='kau_manage_account' type='string' />
+ <public name='kau_maybe' type='string' />
+ <public name='kau_menu' type='string' />
+ <public name='kau_no' type='string' />
+ <public name='kau_no_results_found' type='string' />
+ <public name='kau_none' type='string' />
+ <public name='kau_ok' type='string' />
+ <public name='kau_play_store' type='string' />
+ <public name='kau_rate' type='string' />
+ <public name='kau_report_bug' type='string' />
+ <public name='kau_search' type='string' />
+ <public name='kau_send_feedback' type='string' />
+ <public name='kau_send_via' type='string' />
+ <public name='kau_settings' type='string' />
+ <public name='kau_share' type='string' />
+ <public name='kau_text_copied' type='string' />
+ <public name='kau_thank_you' type='string' />
+ <public name='kau_uh_oh' type='string' />
+ <public name='kau_warning' type='string' />
+ <public name='kau_x_days' type='plurals' />
+ <public name='kau_x_hours' type='plurals' />
+ <public name='kau_x_minutes' type='plurals' />
+ <public name='kau_x_seconds' type='plurals' />
+ <public name='kau_yes' type='string' />
+ <public name='kau_permission_denied' type='string' />
+ <public name='kau_0' type='string' />
+ <public name='kau_bullet_point' type='string' />
+ <public name='Kau' type='style' />
+ <public name='Kau.Translucent' type='style' />
+ <public name='KauFadeIn' type='style' />
+ <public name='KauFadeInFadeOut' type='style' />
+ <public name='KauSlideInRight' type='style' />
+ <public name='KauSlideInBottom' type='style' />
+ <public name='KauSlideInFadeOut' type='style' />
+ <public name='KauSlideInSlideOutRight' type='style' />
+ <public name='KauSlideInSlideOutBottom' type='style' />
+ <public name='kau_enter_slide_bottom' type='transition' />
+ <public name='kau_enter_slide_top' type='transition' />
+ <public name='kau_exit_slide_bottom' type='transition' />
+ <public name='kau_exit_slide_top' type='transition' />
+ <public name='kau_enter_slide_right' type='transition' />
+ <public name='kau_exit_slide_right' type='transition' />
+ <public name='kau_exit_slide_left' type='transition' />
+ <public name='kau_enter_slide_left' type='transition' />
+</resources> \ No newline at end of file
diff --git a/core/src/test/kotlin/ca/allanwang/kau/ui/ProgressAnimatorTest.kt b/core/src/test/kotlin/ca/allanwang/kau/ui/ProgressAnimatorTest.kt
new file mode 100644
index 0000000..60f8680
--- /dev/null
+++ b/core/src/test/kotlin/ca/allanwang/kau/ui/ProgressAnimatorTest.kt
@@ -0,0 +1,75 @@
+package ca.allanwang.kau.ui
+
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class ProgressAnimatorTest {
+
+ private fun ProgressAnimator.test() {
+ startActions.runAll()
+ var value = 0f
+ while (value < 1f) {
+ apply(value)
+ value += 0.05f
+ }
+ apply(1f)
+ endActions.runAll()
+ }
+
+ @Test
+ fun `basic run`() {
+ var i = 0f
+ ProgressAnimator.ofFloat {
+ withAnimator {
+ i = it
+ }
+ }.test()
+ assertEquals(1f, i)
+ }
+
+ @Test
+ fun `start end hooks`() {
+ var i = 0
+ ProgressAnimator.ofFloat {
+ withStartAction { i = 1 }
+ withDisposableAnimator { assertEquals(1, i); true }
+ withEndAction {
+ assertEquals(0, animatorCount, "Disposable animator not removed")
+ i = 2
+ }
+ }.test()
+ assertEquals(2, i)
+ }
+
+ @Test
+ fun `disposable actions`() {
+ var i = 0f
+ ProgressAnimator.ofFloat {
+ withDisposableAnimator {
+ i = if (it < 0.5f) it else 0.5f
+ i > 0.5f
+ }
+ withAnimator {
+ assertEquals(Math.min(it, 0.5f), i)
+ }
+ }.test()
+ }
+
+ @Test
+ fun `point action`() {
+ var called = false
+ var i = 0f
+ ProgressAnimator.ofFloat {
+ withPointAnimator(0.5f) {
+ assertFalse(called)
+ i = it
+ called = true
+ }
+ }.test()
+ assertTrue(called)
+ assertTrue(i > 0.5f)
+ }
+
+} \ No newline at end of file