aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/kotlin/ca/allanwang/kau/ui/ProgressAnimator.kt
blob: a46e6c551f16360dd39ba56baa8540af1ed97d29 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/*
 * Copyright 2018 Allan Wang
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ca.allanwang.kau.ui

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import androidx.annotation.VisibleForTesting
import ca.allanwang.kau.kotlin.kauRemoveIf

/**
 * Created by Allan Wang on 2017-11-10.
 *
 * Wrapper for value animator specifically dealing with progress values
 * This is typically a float range of 0 to 1, but can be customized
 * 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() : ValueAnimator() {

    companion object {

        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<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

    val animatorCount get() = animators.size

    /**
     * Converts an action to a disposable action
     */
    private fun ProgressAction.asDisposable(): ProgressDisposableAction = { this(it); false }

    private fun ProgressRunnable.asDisposable(): ProgressDisposableRunnable = { this(); false }

    /**
     * If [condition] applies, run the animator.
     * @return [condition]
     */
    private fun ProgressAction.runIf(condition: Boolean, progress: Float): Boolean {
        if (condition) this(progress)
        return condition
    }

    @VisibleForTesting
    internal fun MutableList<ProgressDisposableRunnable>.runAll() = kauRemoveIf { it() }

    @VisibleForTesting
    internal fun apply(progress: Float) {
        animators.kauRemoveIf { it(progress) }
    }

    fun withAnimator(action: ProgressAction) =
        withDisposableAnimator(action.asDisposable())

    /**
     * Range animator. Multiples the range by the current float progress before emission
     */
    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))
            }
        }
    }

    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
                }
            }
        }
    }

    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