1 /* <lambda>null2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.surfaceeffects.ripple 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.content.res.Configuration 24 import android.graphics.Canvas 25 import android.graphics.Paint 26 import android.util.AttributeSet 27 import android.view.View 28 import androidx.core.graphics.ColorUtils 29 import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape 30 31 /** 32 * A generic expanding ripple effect. 33 * 34 * Set up the shader with a desired [RippleShape] using [setupShader], [setMaxSize] and [setCenter], 35 * then call [startRipple] to trigger the ripple expansion. 36 */ 37 open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { 38 39 protected lateinit var rippleShader: RippleShader 40 lateinit var rippleShape: RippleShape 41 private set 42 43 private val ripplePaint = Paint() 44 protected val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f) 45 46 var duration: Long = 1750 47 48 fun setMaxSize(maxWidth: Float, maxHeight: Float) { 49 rippleShader.rippleSize.setMaxSize(maxWidth, maxHeight) 50 } 51 52 private var centerX: Float = 0.0f 53 private var centerY: Float = 0.0f 54 fun setCenter(x: Float, y: Float) { 55 this.centerX = x 56 this.centerY = y 57 rippleShader.setCenter(x, y) 58 } 59 60 override fun onConfigurationChanged(newConfig: Configuration?) { 61 rippleShader.pixelDensity = resources.displayMetrics.density 62 super.onConfigurationChanged(newConfig) 63 } 64 65 override fun onAttachedToWindow() { 66 rippleShader.pixelDensity = resources.displayMetrics.density 67 super.onAttachedToWindow() 68 } 69 70 /** Initializes the shader. Must be called before [startRipple]. */ 71 fun setupShader(rippleShape: RippleShape = RippleShape.CIRCLE) { 72 this.rippleShape = rippleShape 73 rippleShader = RippleShader(rippleShape) 74 75 rippleShader.color = RippleShader.RIPPLE_DEFAULT_COLOR 76 rippleShader.rawProgress = 0f 77 rippleShader.sparkleStrength = RippleShader.RIPPLE_SPARKLE_STRENGTH 78 rippleShader.pixelDensity = resources.displayMetrics.density 79 80 ripplePaint.shader = rippleShader 81 } 82 83 /** 84 * Sets the fade parameters for the base ring. 85 * 86 * <p>Base ring indicates a blurred ring below the sparkle ring. See 87 * [RippleShader.baseRingFadeParams]. 88 */ 89 @JvmOverloads 90 fun setBaseRingFadeParams( 91 fadeInStart: Float = rippleShader.baseRingFadeParams.fadeInStart, 92 fadeInEnd: Float = rippleShader.baseRingFadeParams.fadeInEnd, 93 fadeOutStart: Float = rippleShader.baseRingFadeParams.fadeOutStart, 94 fadeOutEnd: Float = rippleShader.baseRingFadeParams.fadeOutEnd 95 ) { 96 setFadeParams( 97 rippleShader.baseRingFadeParams, 98 fadeInStart, 99 fadeInEnd, 100 fadeOutStart, 101 fadeOutEnd 102 ) 103 } 104 105 /** 106 * Sets the fade parameters for the sparkle ring. 107 * 108 * <p>Sparkle ring refers to the ring that's drawn on top of the base ring. See 109 * [RippleShader.sparkleRingFadeParams]. 110 */ 111 @JvmOverloads 112 fun setSparkleRingFadeParams( 113 fadeInStart: Float = rippleShader.sparkleRingFadeParams.fadeInStart, 114 fadeInEnd: Float = rippleShader.sparkleRingFadeParams.fadeInEnd, 115 fadeOutStart: Float = rippleShader.sparkleRingFadeParams.fadeOutStart, 116 fadeOutEnd: Float = rippleShader.sparkleRingFadeParams.fadeOutEnd 117 ) { 118 setFadeParams( 119 rippleShader.sparkleRingFadeParams, 120 fadeInStart, 121 fadeInEnd, 122 fadeOutStart, 123 fadeOutEnd 124 ) 125 } 126 127 /** 128 * Sets the fade parameters for the center fill. 129 * 130 * <p>One common use case is set all the params to 1, which completely removes the center fill. 131 * See [RippleShader.centerFillFadeParams]. 132 */ 133 @JvmOverloads 134 fun setCenterFillFadeParams( 135 fadeInStart: Float = rippleShader.centerFillFadeParams.fadeInStart, 136 fadeInEnd: Float = rippleShader.centerFillFadeParams.fadeInEnd, 137 fadeOutStart: Float = rippleShader.centerFillFadeParams.fadeOutStart, 138 fadeOutEnd: Float = rippleShader.centerFillFadeParams.fadeOutEnd 139 ) { 140 setFadeParams( 141 rippleShader.centerFillFadeParams, 142 fadeInStart, 143 fadeInEnd, 144 fadeOutStart, 145 fadeOutEnd 146 ) 147 } 148 149 private fun setFadeParams( 150 fadeParams: RippleShader.FadeParams, 151 fadeInStart: Float, 152 fadeInEnd: Float, 153 fadeOutStart: Float, 154 fadeOutEnd: Float 155 ) { 156 with(fadeParams) { 157 this.fadeInStart = fadeInStart 158 this.fadeInEnd = fadeInEnd 159 this.fadeOutStart = fadeOutStart 160 this.fadeOutEnd = fadeOutEnd 161 } 162 } 163 164 /** 165 * Sets blur multiplier at start and end of the progress. 166 * 167 * <p>It interpolates between [start] and [end]. No need to set it if using default blur. 168 */ 169 fun setBlur(start: Float, end: Float) { 170 rippleShader.blurStart = start 171 rippleShader.blurEnd = end 172 } 173 174 /** 175 * Sets the list of [RippleShader.SizeAtProgress]. 176 * 177 * <p>Note that this clears the list before it sets with the new data. 178 */ 179 fun setSizeAtProgresses(vararg targetSizes: RippleShader.SizeAtProgress) { 180 rippleShader.rippleSize.setSizeAtProgresses(*targetSizes) 181 } 182 183 @JvmOverloads 184 fun startRipple(onAnimationEnd: Runnable? = null) { 185 if (animator.isRunning) { 186 return // Ignore if ripple effect is already playing 187 } 188 animator.duration = duration 189 animator.addUpdateListener { updateListener -> 190 val now = updateListener.currentPlayTime 191 val progress = updateListener.animatedValue as Float 192 rippleShader.rawProgress = progress 193 rippleShader.distortionStrength = 1 - progress 194 rippleShader.time = now.toFloat() 195 invalidate() 196 } 197 animator.addListener( 198 object : AnimatorListenerAdapter() { 199 override fun onAnimationEnd(animation: Animator) { 200 onAnimationEnd?.run() 201 } 202 } 203 ) 204 animator.start() 205 } 206 207 /** 208 * Set the color to be used for the ripple. 209 * 210 * The alpha value of the color will be applied to the ripple. The alpha range is [0-255]. 211 */ 212 fun setColor(color: Int, alpha: Int = RippleShader.RIPPLE_DEFAULT_ALPHA) { 213 rippleShader.color = ColorUtils.setAlphaComponent(color, alpha) 214 } 215 216 /** Set the intensity of the sparkles. */ 217 fun setSparkleStrength(strength: Float) { 218 rippleShader.sparkleStrength = strength 219 } 220 221 /** Indicates whether the ripple animation is playing. */ 222 fun rippleInProgress(): Boolean = animator.isRunning 223 224 override fun onDraw(canvas: Canvas) { 225 if (!canvas.isHardwareAccelerated) { 226 // Drawing with the ripple shader requires hardware acceleration, so skip if it's 227 // unsupported. 228 return 229 } 230 // To reduce overdraw, we mask the effect to a circle or a rectangle that's bigger than the 231 // active effect area. Values here should be kept in sync with the animation implementation 232 // in the ripple shader. 233 if (rippleShape == RippleShape.CIRCLE) { 234 val maskRadius = rippleShader.rippleSize.currentWidth 235 canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint) 236 } else if (rippleShape == RippleShape.ELLIPSE) { 237 val maskWidth = rippleShader.rippleSize.currentWidth * 2 238 val maskHeight = rippleShader.rippleSize.currentHeight * 2 239 canvas.drawRect( 240 /* left= */ centerX - maskWidth, 241 /* top= */ centerY - maskHeight, 242 /* right= */ centerX + maskWidth, 243 /* bottom= */ centerY + maskHeight, 244 ripplePaint 245 ) 246 } else { // RippleShape.RoundedBox 247 // No masking for the rounded box, as it has more blur which requires larger bounds. 248 // Masking creates sharp bounds even when the masking is 4 times bigger. 249 canvas.drawPaint(ripplePaint) 250 } 251 } 252 } 253