aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/kotlin/ca/allanwang/kau/ui/views/RippleCanvas.kt
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/main/kotlin/ca/allanwang/kau/ui/views/RippleCanvas.kt')
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/ui/views/RippleCanvas.kt140
1 files changed, 140 insertions, 0 deletions
diff --git a/core/src/main/kotlin/ca/allanwang/kau/ui/views/RippleCanvas.kt b/core/src/main/kotlin/ca/allanwang/kau/ui/views/RippleCanvas.kt
new file mode 100644
index 0000000..773490c
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/ui/views/RippleCanvas.kt
@@ -0,0 +1,140 @@
+package ca.allanwang.kau.ui.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
+ }
+}