Files
lawnchair/systemUI/anim/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt

253 lines
8.8 KiB
Kotlin

/*
* 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.surfaceeffects.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 androidx.core.graphics.ColorUtils
import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape
/**
* 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) {
protected lateinit var rippleShader: RippleShader
lateinit var rippleShape: RippleShape
private set
private val ripplePaint = Paint()
protected val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f)
var duration: Long = 1750
fun setMaxSize(maxWidth: Float, maxHeight: Float) {
rippleShader.rippleSize.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 = RippleShader.RIPPLE_DEFAULT_COLOR
rippleShader.rawProgress = 0f
rippleShader.sparkleStrength = RippleShader.RIPPLE_SPARKLE_STRENGTH
rippleShader.pixelDensity = resources.displayMetrics.density
ripplePaint.shader = rippleShader
}
/**
* Sets the fade parameters for the base ring.
*
* <p>Base ring indicates a blurred ring below the sparkle ring. See
* [RippleShader.baseRingFadeParams].
*/
@JvmOverloads
fun setBaseRingFadeParams(
fadeInStart: Float = rippleShader.baseRingFadeParams.fadeInStart,
fadeInEnd: Float = rippleShader.baseRingFadeParams.fadeInEnd,
fadeOutStart: Float = rippleShader.baseRingFadeParams.fadeOutStart,
fadeOutEnd: Float = rippleShader.baseRingFadeParams.fadeOutEnd
) {
setFadeParams(
rippleShader.baseRingFadeParams,
fadeInStart,
fadeInEnd,
fadeOutStart,
fadeOutEnd
)
}
/**
* Sets the fade parameters for the sparkle ring.
*
* <p>Sparkle ring refers to the ring that's drawn on top of the base ring. See
* [RippleShader.sparkleRingFadeParams].
*/
@JvmOverloads
fun setSparkleRingFadeParams(
fadeInStart: Float = rippleShader.sparkleRingFadeParams.fadeInStart,
fadeInEnd: Float = rippleShader.sparkleRingFadeParams.fadeInEnd,
fadeOutStart: Float = rippleShader.sparkleRingFadeParams.fadeOutStart,
fadeOutEnd: Float = rippleShader.sparkleRingFadeParams.fadeOutEnd
) {
setFadeParams(
rippleShader.sparkleRingFadeParams,
fadeInStart,
fadeInEnd,
fadeOutStart,
fadeOutEnd
)
}
/**
* Sets the fade parameters for the center fill.
*
* <p>One common use case is set all the params to 1, which completely removes the center fill.
* See [RippleShader.centerFillFadeParams].
*/
@JvmOverloads
fun setCenterFillFadeParams(
fadeInStart: Float = rippleShader.centerFillFadeParams.fadeInStart,
fadeInEnd: Float = rippleShader.centerFillFadeParams.fadeInEnd,
fadeOutStart: Float = rippleShader.centerFillFadeParams.fadeOutStart,
fadeOutEnd: Float = rippleShader.centerFillFadeParams.fadeOutEnd
) {
setFadeParams(
rippleShader.centerFillFadeParams,
fadeInStart,
fadeInEnd,
fadeOutStart,
fadeOutEnd
)
}
private fun setFadeParams(
fadeParams: RippleShader.FadeParams,
fadeInStart: Float,
fadeInEnd: Float,
fadeOutStart: Float,
fadeOutEnd: Float
) {
with(fadeParams) {
this.fadeInStart = fadeInStart
this.fadeInEnd = fadeInEnd
this.fadeOutStart = fadeOutStart
this.fadeOutEnd = fadeOutEnd
}
}
/**
* Sets blur multiplier at start and end of the progress.
*
* <p>It interpolates between [start] and [end]. No need to set it if using default blur.
*/
fun setBlur(start: Float, end: Float) {
rippleShader.blurStart = start
rippleShader.blurEnd = end
}
/**
* Sets the list of [RippleShader.SizeAtProgress].
*
* <p>Note that this clears the list before it sets with the new data.
*/
fun setSizeAtProgresses(vararg targetSizes: RippleShader.SizeAtProgress) {
rippleShader.rippleSize.setSizeAtProgresses(*targetSizes)
}
@JvmOverloads
fun startRipple(onAnimationEnd: Runnable? = null) {
if (animator.isRunning) {
return // Ignore if ripple effect is already playing
}
animator.duration = duration
animator.addUpdateListener { updateListener ->
val now = updateListener.currentPlayTime
val progress = updateListener.animatedValue as Float
rippleShader.rawProgress = progress
rippleShader.distortionStrength = 1 - progress
rippleShader.time = now.toFloat()
invalidate()
}
animator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
onAnimationEnd?.run()
}
}
)
animator.start()
}
/**
* Set the color to be used for the ripple.
*
* The alpha value of the color will be applied to the ripple. The alpha range is [0-255].
*/
fun setColor(color: Int, alpha: Int = RippleShader.RIPPLE_DEFAULT_ALPHA) {
rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
}
/** Set the intensity of the sparkles. */
fun setSparkleStrength(strength: Float) {
rippleShader.sparkleStrength = strength
}
/** Indicates whether the ripple animation is playing. */
fun rippleInProgress(): Boolean = animator.isRunning
override fun onDraw(canvas: Canvas) {
if (!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 = rippleShader.rippleSize.currentWidth
canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
} else if (rippleShape == RippleShape.ELLIPSE) {
val maskWidth = rippleShader.rippleSize.currentWidth * 2
val maskHeight = rippleShader.rippleSize.currentHeight * 2
canvas.drawRect(
/* left= */ centerX - maskWidth,
/* top= */ centerY - maskHeight,
/* right= */ centerX + maskWidth,
/* bottom= */ centerY + maskHeight,
ripplePaint
)
} else { // RippleShape.RoundedBox
// No masking for the rounded box, as it has more blur which requires larger bounds.
// Masking creates sharp bounds even when the masking is 4 times bigger.
canvas.drawPaint(ripplePaint)
}
}
}