aboutsummaryrefslogtreecommitdiff
path: root/library/src/main/kotlin/ca/allanwang/kau/views/RippleCanvas.kt
blob: 805fb216c606febbd6d5a7a5bcd7714076f7678c (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
package ca.allanwang.kau.views

import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import ca.allanwang.kau.utils.adjustAlpha

/**
 * Created by Allan Wang on 2016-11-17.
 *
 *
 * Canvas drawn ripples that keep the previous color
 * Extends to view dimensions
 * Supports multiple ripples from varying locations
 */
class RippleCanvas @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private val paint: Paint = Paint()
    private var baseColor = Color.TRANSPARENT
    private val ripples: MutableList<Ripple> = mutableListOf()

    init {
        paint.isAntiAlias = true
        paint.style = Paint.Style.FILL
    }

    /**
     * Drawing the ripples involves having access to the next layer if it exists,
     * and using its values to decide on the current color.
     * If the next layer requests a fade, we will adjust the alpha of our current layer before drawing.
     * Otherwise we will just draw the color as intended
     */
    override fun onDraw(canvas: Canvas) {
        val itr = ripples.listIterator()
        if (!itr.hasNext()) return canvas.drawColor(baseColor)
        var next = itr.next()
        canvas.drawColor(colorToDraw(baseColor, next.fade, next.radius, next.maxRadius))
        var last = false
        while (!last) {
            val current = next
            if (itr.hasNext()) next = itr.next()
            else last = true
            //We may fade any layer except for the last one
            paint.color = colorToDraw(current.color, next.fade && !last, next.radius, next.maxRadius)
            canvas.drawCircle(current.x, current.y, current.radius, paint)
            if (current.radius == current.maxRadius) {
                if (!last) {
                    itr.previous()
                    itr.remove()
                    itr.next()
                } else {
                    itr.remove()
                }
                baseColor = current.color
            }
        }
    }

    /**
     * Given our current color and next layer's radius & max,
     * we will decide on the alpha of our current layer
     */
    internal fun colorToDraw(color: Int, fade: Boolean, current: Float, goal: Float): Int {
        if (!fade || (current / goal <= FADE_PIVOT)) return color
        val factor = (goal - current) / (goal - FADE_PIVOT * goal)
        return color.adjustAlpha(factor)
    }

    /**
     * Creates a ripple effect from the given starting values
     * [fade] will gradually transition previous ripples to a transparent color so the resulting background is what we want
     * this is typically only necessary if the ripple color has transparency
     */
    fun ripple(color: Int, startX: Float = 0f, startY: Float = 0f, duration: Long = 600L, fade: Boolean = Color.alpha(color) != 255) {
        val w = width.toFloat()
        val h = height.toFloat()
        val x = when (startX) {
            MIDDLE -> w / 2
            END -> w
            else -> startX
        }
        val y = when (startY) {
            MIDDLE -> h / 2
            END -> h
            else -> startY
        }
        val maxRadius = Math.hypot(Math.max(x, w - x).toDouble(), Math.max(y, h - y).toDouble()).toFloat()
        val ripple = Ripple(color, x, y, 0f, maxRadius, fade)
        ripples.add(ripple)
        val animator = ValueAnimator.ofFloat(0f, maxRadius)
        animator.duration = duration
        animator.addUpdateListener { animation ->
            ripple.radius = animation.animatedValue as Float
            invalidate()
        }
        animator.start()
    }

    /**
     * Sets a color directly; clears ripple queue if it exists
     */
    fun set(color: Int) {
        baseColor = color
        ripples.clear()
        invalidate()
    }

    /**
     * Sets a color directly but with a transition
     */
    fun fade(color: Int, duration: Long = 300L) {
        ripples.clear()
        val animator = ValueAnimator.ofObject(ArgbEvaluator(), baseColor, color)
        animator.duration = duration
        animator.addUpdateListener { animation ->
            baseColor = animation.animatedValue as Int
            invalidate()
        }
        animator.start()
    }

    internal class Ripple(val color: Int,
                          val x: Float,
                          val y: Float,
                          var radius: Float,
                          val maxRadius: Float,
                          val fade: Boolean)

    companion object {
        const val MIDDLE = -1.0f
        const val END = -2.0f
        const val FADE_PIVOT = 0.5f
    }
}