diff options
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | artifacts.gradle | 8 | ||||
-rw-r--r-- | buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy | 57 | ||||
-rw-r--r-- | core-ui/src/main/res-public/values/public.xml | 10 | ||||
-rw-r--r-- | core/src/main/kotlin/ca/allanwang/kau/email/EmailBuilder.kt | 4 | ||||
-rw-r--r-- | core/src/main/kotlin/ca/allanwang/kau/ui/ProgressAnimator.kt | 179 | ||||
-rw-r--r-- | core/src/main/res-public/values/public.xml | 118 | ||||
-rw-r--r-- | core/src/test/kotlin/ca/allanwang/kau/ui/ProgressAnimatorTest.kt | 75 | ||||
-rw-r--r-- | docs/Migration.md | 16 |
9 files changed, 411 insertions, 58 deletions
diff --git a/.travis.yml b/.travis.yml index ee679b3..7bd509f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ env: - ANDROID_API=28 - EMULATOR_API=19 - ANDROID_BUILD_TOOLS=28.0.3 +git: + depth: 500 android: components: - tools diff --git a/artifacts.gradle b/artifacts.gradle index c0c344a..ab291f7 100644 --- a/artifacts.gradle +++ b/artifacts.gradle @@ -31,8 +31,6 @@ artifacts { // We assume resources within res-public are public task generatepublicxml { - println "Generating public XML" - def resDir = project.projectDir.absolutePath + "/src/main/res-public" def publicFolder = file(resDir + "/values") @@ -51,10 +49,12 @@ task generatepublicxml { '**/values/*.xml' ], exclude: '**/public.xml' - ); + ) + + println "Generating public XML: ${project.name}" // Create new public.xml with writer - new File(resDir + "/values/public.xml").withWriter { writer -> + file(resDir + "/values/public.xml").withWriter { writer -> // Create MarkupBuilder with 4 space indent def destXml = new MarkupBuilder(new IndentPrinter(writer, " ", true)); def destXmlMkp = destXml.getMkp(); diff --git a/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy b/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy new file mode 100644 index 0000000..d767a8a --- /dev/null +++ b/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy @@ -0,0 +1,57 @@ +package ca.allanwang.kau + +class Versions { + static def coreMinSdk = 19 + static def minSdk = 21 + static def targetSdk = 28 + + // https://developer.android.com/studio/releases/build-tools + static def buildTools = '28.0.3' + + // https://developer.android.com/topic/libraries/support-library/revisions + static def supportLibs = '28.0.0' + + // https://kotlinlang.org/docs/reference/using-gradle.html + static def kotlin = '1.2.71' + + // https://github.com/mikepenz/AboutLibraries/releases + static def aboutLibraries = '6.1.1' + + // https://github.com/Kotlin/anko/releases + static def anko = '0.10.5' + + // https://github.com/wasabeef/Blurry/releases + static def blurry = '2.1.1' + + // https://dl.google.com/dl/android/maven2/com/android/support/constraint/group-index.xml + static def constraintLayout = '1.1.3' + + // https://github.com/mikepenz/FastAdapter#using-maven + static def fastAdapter = '3.2.9' + static def fastAdapterCommons = fastAdapter + + // https://github.com/bumptech/glide/releases + static def glide = '4.8.0' + + // https://github.com/mikepenz/Android-Iconics#1-provide-the-gradle-dependency + static def iconics = '3.0.4' + static def iconicsGoogle = '3.0.1.2' + static def iconicsMaterial = '2.2.0.4' + static def iconicsCommunity = '2.0.46.1' + + // https://github.com/afollestad/material-dialogs/releases + static def materialDialog = '0.9.6.0' + + static def espresso = '3.0.1' + static def junit = '4.12' + static def testRunner = '1.0.1' + + static def gradlePlugin = '3.2.1' + static def mavenPlugin = '2.1' + static def playPublishPlugin = '1.2.2' + + // https://github.com/KeepSafe/dexcount-gradle-plugin/releases + static def dexCountPlugin = '0.8.4' + // https://github.com/gladed/gradle-android-git-version/releases + static def gitVersionPlugin = '0.4.5' +}
\ No newline at end of file diff --git a/core-ui/src/main/res-public/values/public.xml b/core-ui/src/main/res-public/values/public.xml new file mode 100644 index 0000000..f46b3eb --- /dev/null +++ b/core-ui/src/main/res-public/values/public.xml @@ -0,0 +1,10 @@ +<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_recycler_detached_background' type='layout' /> + <public name='kau_elastic_recycler_activity' type='layout' /> + <public name='kau_recycler_textslider' type='layout' /> + <public name='Kau.Translucent' type='style' /> + <public name='Kau.Translucent.NoAnimation' type='style' /> + <public name='Kau.Translucent.SlideBottom' type='style' /> + <public name='Kau.Translucent.SlideTop' type='style' /> +</resources>
\ No newline at end of file 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 diff --git a/docs/Migration.md b/docs/Migration.md index c0816d4..593056e 100644 --- a/docs/Migration.md +++ b/docs/Migration.md @@ -4,9 +4,19 @@ Below are some highlights on major refactoring/breaking changes # v5.0.0 -* Material Dialog is now 3.x. This leads to a whole new API, but fortunately it is based around kotlin. Please refer to [MD's documents](https://github.com/afollestad/material-dialogs/tree/3.0.0-rc2/documentation) for the new methods. - * Alongside such changes, `:colorpicker` is no longer as necessary. It exists mainly to provide an internal interface for other submodules. - +## Material Dialog Update + +Material Dialog is now 3.x. +This leads to a whole new API, but fortunately it is based around kotlin. +Please refer to [MD's documents](https://github.com/afollestad/material-dialogs/tree/3.0.0-rc2/documentation) for the new methods. + +Alongside such changes, `:colorpicker` is no longer as necessary. It exists mainly to provide an internal interface for other submodules. + +## Update ProgressAnimator + +`ProgressAnimator` has been completely rewritten to be an extension of `ValueAnimator`. +This for the most part is not a breaking change, apart from the fact that creating an animator will not start it immediately. +Make sure to call `.start()` to begin the animation. # v4.0.1-alpha02 |