diff options
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.kt | 140 |
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 + } +} |