blob: 60c8f3719a2e34c5b6bff5434951c1d632473352 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.systemui.ripple
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import com.android.systemui.ripple.RippleShader.RippleShape
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f
private const val RIPPLE_DEFAULT_COLOR: Int = 0xffffffff.toInt()
/**
* A generic expanding ripple effect.
*
* Set up the shader with a desired [RippleShape] using [setupShader], [setMaxSize] and [setCenter],
* then call [startRipple] to trigger the ripple expansion.
*/
open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private lateinit var rippleShader: RippleShader
lateinit var rippleShape: RippleShape
private set
private val ripplePaint = Paint()
var rippleInProgress: Boolean = false
var duration: Long = 1750
private var maxWidth: Float = 0.0f
private var maxHeight: Float = 0.0f
fun setMaxSize(maxWidth: Float, maxHeight: Float) {
this.maxWidth = maxWidth
this.maxHeight = maxHeight
rippleShader.setMaxSize(maxWidth, maxHeight)
}
private var centerX: Float = 0.0f
private var centerY: Float = 0.0f
fun setCenter(x: Float, y: Float) {
this.centerX = x
this.centerY = y
rippleShader.setCenter(x, y)
}
override fun onConfigurationChanged(newConfig: Configuration?) {
rippleShader.pixelDensity = resources.displayMetrics.density
super.onConfigurationChanged(newConfig)
}
override fun onAttachedToWindow() {
rippleShader.pixelDensity = resources.displayMetrics.density
super.onAttachedToWindow()
}
/** Initializes the shader. Must be called before [startRipple]. */
fun setupShader(rippleShape: RippleShape = RippleShape.CIRCLE) {
this.rippleShape = rippleShape
rippleShader = RippleShader(rippleShape)
rippleShader.color = RIPPLE_DEFAULT_COLOR
rippleShader.progress = 0f
rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
rippleShader.pixelDensity = resources.displayMetrics.density
ripplePaint.shader = rippleShader
}
@JvmOverloads
fun startRipple(onAnimationEnd: Runnable? = null) {
if (rippleInProgress) {
return // Ignore if ripple effect is already playing
}
val animator = ValueAnimator.ofFloat(0f, 1f)
animator.duration = duration
animator.addUpdateListener { updateListener ->
val now = updateListener.currentPlayTime
val progress = updateListener.animatedValue as Float
rippleShader.progress = progress
rippleShader.distortionStrength = 1 - progress
rippleShader.time = now.toFloat()
invalidate()
}
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
rippleInProgress = false
onAnimationEnd?.run()
}
})
animator.start()
rippleInProgress = true
}
/** Set the color to be used for the ripple. */
fun setColor(color: Int) {
rippleShader.color = color
}
/**
* Set whether the ripple should remain filled as the ripple expands.
*
* See [RippleShader.rippleFill].
*/
fun setRippleFill(rippleFill: Boolean) {
rippleShader.rippleFill = rippleFill
}
/**
* Set the intensity of the sparkles.
*/
fun setSparkleStrength(strength: Float) {
rippleShader.sparkleStrength = strength
}
override fun onDraw(canvas: Canvas?) {
if (canvas == null || !canvas.isHardwareAccelerated) {
// Drawing with the ripple shader requires hardware acceleration, so skip
// if it's unsupported.
return
}
// To reduce overdraw, we mask the effect to a circle or a rectangle that's bigger than the
// active effect area. Values here should be kept in sync with the animation implementation
// in the ripple shader.
if (rippleShape == RippleShape.CIRCLE) {
val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
(1 - rippleShader.progress)) * maxWidth
canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
} else {
val maskWidth = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
(1 - rippleShader.progress)) * maxWidth * 2
val maskHeight = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
(1 - rippleShader.progress)) * maxHeight * 2
canvas.drawRect(
/* left= */ centerX - maskWidth,
/* top= */ centerY - maskHeight,
/* right= */ centerX + maskWidth,
/* bottom= */ centerY + maskHeight,
ripplePaint)
}
}
}