1 /* 2 * 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 package com.android.systemui.surfaceeffects.ripple 17 18 import android.graphics.RuntimeShader 19 import android.util.Log 20 import android.view.animation.Interpolator 21 import android.view.animation.PathInterpolator 22 import androidx.annotation.VisibleForTesting 23 import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary 24 import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary 25 26 /** 27 * Shader class that renders an expanding ripple effect. The ripple contains three elements: 28 * 1. an expanding filled [RippleShape] that appears in the beginning and quickly fades away 29 * 2. an expanding ring that appears throughout the effect 30 * 3. an expanding ring-shaped area that reveals noise over #2. 31 * 32 * The ripple shader will be default to the circle shape if not specified. 33 * 34 * Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java. 35 */ 36 class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) : 37 RuntimeShader(buildShader(rippleShape)) { 38 39 /** Shapes that the [RippleShader] supports. */ 40 enum class RippleShape { 41 CIRCLE, 42 ROUNDED_BOX, 43 ELLIPSE 44 } 45 // language=AGSL 46 companion object { 47 private val TAG = RippleShader::class.simpleName 48 49 // Default fade in/ out values. The value range is [0,1]. 50 const val DEFAULT_FADE_IN_START = 0f 51 const val DEFAULT_FADE_OUT_END = 1f 52 53 const val DEFAULT_BASE_RING_FADE_IN_END = 0.1f 54 const val DEFAULT_BASE_RING_FADE_OUT_START = 0.3f 55 56 const val DEFAULT_SPARKLE_RING_FADE_IN_END = 0.1f 57 const val DEFAULT_SPARKLE_RING_FADE_OUT_START = 0.4f 58 59 const val DEFAULT_CENTER_FILL_FADE_IN_END = 0f 60 const val DEFAULT_CENTER_FILL_FADE_OUT_START = 0f 61 const val DEFAULT_CENTER_FILL_FADE_OUT_END = 0.6f 62 63 const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f 64 const val RIPPLE_DEFAULT_COLOR: Int = 0xffffffff.toInt() 65 const val RIPPLE_DEFAULT_ALPHA: Int = 115 // full opacity is 255. 66 67 private const val SHADER_UNIFORMS = 68 """ 69 uniform vec2 in_center; 70 uniform vec2 in_size; 71 uniform float in_cornerRadius; 72 uniform float in_thickness; 73 uniform float in_time; 74 uniform float in_distort_radial; 75 uniform float in_distort_xy; 76 uniform float in_fadeSparkle; 77 uniform float in_fadeFill; 78 uniform float in_fadeRing; 79 uniform float in_blur; 80 uniform float in_pixelDensity; 81 layout(color) uniform vec4 in_color; 82 uniform float in_sparkle_strength; 83 """ 84 85 private const val SHADER_CIRCLE_MAIN = 86 """ 87 vec4 main(vec2 p) { 88 vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy); 89 float radius = in_size.x * 0.5; 90 float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur); 91 float inside = soften(sdCircle(p_distorted-in_center, radius * 1.25), in_blur); 92 float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175) 93 * (1.-sparkleRing) * in_fadeSparkle; 94 95 float rippleInsideAlpha = (1.-inside) * in_fadeFill; 96 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing; 97 float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a; 98 vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha; 99 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); 100 } 101 """ 102 103 private const val SHADER_ROUNDED_BOX_MAIN = 104 """ 105 vec4 main(vec2 p) { 106 float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius, 107 in_thickness), in_blur); 108 float inside = soften(sdRoundedBox(p-in_center, in_size * 1.25, in_cornerRadius), 109 in_blur); 110 float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175) 111 * (1.-sparkleRing) * in_fadeSparkle; 112 113 float rippleInsideAlpha = (1.-inside) * in_fadeFill; 114 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing; 115 float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a; 116 vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha; 117 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); 118 } 119 """ 120 121 private const val SHADER_ELLIPSE_MAIN = 122 """ 123 vec4 main(vec2 p) { 124 vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy); 125 126 float sparkleRing = soften(ellipseRing(p_distorted-in_center, in_size), in_blur); 127 float inside = soften(sdEllipse(p_distorted-in_center, in_size * 1.2), in_blur); 128 float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175) 129 * (1.-sparkleRing) * in_fadeSparkle; 130 131 float rippleInsideAlpha = (1.-inside) * in_fadeFill; 132 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing; 133 float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a; 134 vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha; 135 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); 136 } 137 """ 138 139 private const val CIRCLE_SHADER = 140 SHADER_UNIFORMS + 141 ShaderUtilLibrary.SHADER_LIB + 142 SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + 143 SdfShaderLibrary.CIRCLE_SDF + 144 SHADER_CIRCLE_MAIN 145 private const val ROUNDED_BOX_SHADER = 146 SHADER_UNIFORMS + 147 ShaderUtilLibrary.SHADER_LIB + 148 SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + 149 SdfShaderLibrary.ROUNDED_BOX_SDF + 150 SHADER_ROUNDED_BOX_MAIN 151 private const val ELLIPSE_SHADER = 152 SHADER_UNIFORMS + 153 ShaderUtilLibrary.SHADER_LIB + 154 SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + 155 SdfShaderLibrary.ELLIPSE_SDF + 156 SHADER_ELLIPSE_MAIN 157 buildShadernull158 private fun buildShader(rippleShape: RippleShape): String = 159 when (rippleShape) { 160 RippleShape.CIRCLE -> CIRCLE_SHADER 161 RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER 162 RippleShape.ELLIPSE -> ELLIPSE_SHADER 163 } 164 subProgressnull165 private fun subProgress(start: Float, end: Float, progress: Float): Float { 166 // Avoid division by 0. 167 if (start == end) { 168 // If start and end are the same and progress has exceeded the start/ end point, 169 // treat it as 1, otherwise 0. 170 return if (progress > start) 1f else 0f 171 } 172 173 val min = Math.min(start, end) 174 val max = Math.max(start, end) 175 val sub = Math.min(Math.max(progress, min), max) 176 return (sub - start) / (end - start) 177 } 178 getFadenull179 private fun getFade(fadeParams: FadeParams, rawProgress: Float): Float { 180 val fadeIn = subProgress(fadeParams.fadeInStart, fadeParams.fadeInEnd, rawProgress) 181 val fadeOut = 182 1f - subProgress(fadeParams.fadeOutStart, fadeParams.fadeOutEnd, rawProgress) 183 184 return Math.min(fadeIn, fadeOut) 185 } 186 lerpnull187 private fun lerp(start: Float, stop: Float, amount: Float): Float { 188 return start + (stop - start) * amount 189 } 190 191 // Copied from [Interpolators#STANDARD]. This is to remove dependency on AnimationLib. 192 private val STANDARD: Interpolator = PathInterpolator(0.2f, 0f, 0f, 1f) 193 } 194 195 /** Sets the center position of the ripple. */ setCenternull196 fun setCenter(x: Float, y: Float) { 197 setFloatUniform("in_center", x, y) 198 } 199 200 /** 201 * Blur multipliers for the ripple. 202 * 203 * <p>It interpolates from [blurStart] to [blurEnd] based on the [progress]. Increase number to 204 * add more blur. 205 */ 206 var blurStart: Float = 1.25f 207 var blurEnd: Float = 0.5f 208 209 /** Size of the ripple. */ 210 val rippleSize = RippleSize() 211 212 /** 213 * Linear progress of the ripple. Float value between [0, 1]. 214 * 215 * <p>Note that the progress here is expected to be linear without any curve applied. 216 */ 217 var rawProgress: Float = 0.0f 218 set(value) { 219 field = value 220 progress = STANDARD.getInterpolation(value) 221 222 setFloatUniform("in_fadeSparkle", getFade(sparkleRingFadeParams, value)) 223 setFloatUniform("in_fadeRing", getFade(baseRingFadeParams, value)) 224 setFloatUniform("in_fadeFill", getFade(centerFillFadeParams, value)) 225 } 226 227 /** Progress with Standard easing curve applied. */ 228 private var progress: Float = 0.0f 229 set(value) { 230 field = value 231 232 rippleSize.update(value) 233 234 setFloatUniform("in_size", rippleSize.currentWidth, rippleSize.currentHeight) 235 setFloatUniform("in_thickness", rippleSize.currentHeight * 0.5f) 236 // Corner radius is always max of the min between the current width and height. 237 setFloatUniform( 238 "in_cornerRadius", 239 Math.min(rippleSize.currentWidth, rippleSize.currentHeight) 240 ) 241 setFloatUniform("in_blur", lerp(1.25f, 0.5f, value)) 242 } 243 244 /** Play time since the start of the effect. */ 245 var time: Float = 0.0f 246 set(value) { 247 field = value 248 setFloatUniform("in_time", value) 249 } 250 251 /** A hex value representing the ripple color, in the format of ARGB */ 252 var color: Int = 0xffffff 253 set(value) { 254 field = value 255 setColorUniform("in_color", value) 256 } 257 258 /** 259 * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus with 260 * strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 it's 261 * opaque white and looks the most grainy. 262 */ 263 var sparkleStrength: Float = 0.0f 264 set(value) { 265 field = value 266 setFloatUniform("in_sparkle_strength", value) 267 } 268 269 /** Distortion strength of the ripple. Expected value between[0, 1]. */ 270 var distortionStrength: Float = 0.0f 271 set(value) { 272 field = value 273 setFloatUniform("in_distort_radial", 75 * rawProgress * value) 274 setFloatUniform("in_distort_xy", 75 * value) 275 } 276 277 /** 278 * Pixel density of the screen that the effects are rendered to. 279 * 280 * <p>This value should come from [resources.displayMetrics.density]. 281 */ 282 var pixelDensity: Float = 1.0f 283 set(value) { 284 field = value 285 setFloatUniform("in_pixelDensity", value) 286 } 287 288 /** Parameters that are used to fade in/ out of the sparkle ring. */ 289 val sparkleRingFadeParams = 290 FadeParams( 291 DEFAULT_FADE_IN_START, 292 DEFAULT_SPARKLE_RING_FADE_IN_END, 293 DEFAULT_SPARKLE_RING_FADE_OUT_START, 294 DEFAULT_FADE_OUT_END 295 ) 296 297 /** 298 * Parameters that are used to fade in/ out of the base ring. 299 * 300 * <p>Note that the shader draws the sparkle ring on top of the base ring. 301 */ 302 val baseRingFadeParams = 303 FadeParams( 304 DEFAULT_FADE_IN_START, 305 DEFAULT_BASE_RING_FADE_IN_END, 306 DEFAULT_BASE_RING_FADE_OUT_START, 307 DEFAULT_FADE_OUT_END 308 ) 309 310 /** Parameters that are used to fade in/ out of the center fill. */ 311 val centerFillFadeParams = 312 FadeParams( 313 DEFAULT_FADE_IN_START, 314 DEFAULT_CENTER_FILL_FADE_IN_END, 315 DEFAULT_CENTER_FILL_FADE_OUT_START, 316 DEFAULT_CENTER_FILL_FADE_OUT_END 317 ) 318 319 /** 320 * Parameters used for fade in and outs of the ripple. 321 * 322 * <p>Note that all the fade in/ outs are "linear" progression. 323 * 324 * ``` 325 * (opacity) 326 * 1 327 * │ 328 * maxAlpha ← ―――――――――――― 329 * │ / \ 330 * │ / \ 331 * minAlpha ←――――/ \―――― (alpha change) 332 * │ 333 * │ 334 * 0 ―――↑―――↑―――――――――↑―――↑――――1 (progress) 335 * fadeIn fadeOut 336 * Start & End Start & End 337 * ``` 338 * 339 * <p>If no fade in/ out is needed, set [fadeInStart] and [fadeInEnd] to 0; [fadeOutStart] and 340 * [fadeOutEnd] to 1. 341 */ 342 data class FadeParams( 343 /** 344 * The starting point of the fade out which ends at [fadeInEnd], given that the animation 345 * goes from 0 to 1. 346 */ 347 var fadeInStart: Float = DEFAULT_FADE_IN_START, 348 /** 349 * The endpoint of the fade in when the fade in starts at [fadeInStart], given that the 350 * animation goes from 0 to 1. 351 */ 352 var fadeInEnd: Float, 353 /** 354 * The starting point of the fade out which ends at 1, given that the animation goes from 0 355 * to 1. 356 */ 357 var fadeOutStart: Float, 358 359 /** The endpoint of the fade out, given that the animation goes from 0 to 1. */ 360 var fadeOutEnd: Float = DEFAULT_FADE_OUT_END, 361 ) 362 363 /** 364 * Desired size of the ripple at a point t in [progress]. 365 * 366 * <p>Note that [progress] is curved and normalized. Below is an example usage: 367 * SizeAtProgress(t= 0f, width= 0f, height= 0f), SizeAtProgress(t= 0.2f, width= 500f, height= 368 * 700f), SizeAtProgress(t= 1f, width= 100f, height= 300f) 369 * 370 * <p>For simple ripple effects, you will want to use [setMaxSize] as it is translated into: 371 * SizeAtProgress(t= 0f, width= 0f, height= 0f), SizeAtProgress(t= 1f, width= maxWidth, height= 372 * maxHeight) 373 */ 374 data class SizeAtProgress( 375 /** Time t in [0,1] progress range. */ 376 var t: Float, 377 /** Target width size of the ripple at time [t]. */ 378 var width: Float, 379 /** Target height size of the ripple at time [t]. */ 380 var height: Float 381 ) 382 383 /** Updates and stores the ripple size. */ 384 inner class RippleSize { 385 @VisibleForTesting var sizes = mutableListOf<SizeAtProgress>() 386 @VisibleForTesting var currentSizeIndex = 0 387 @VisibleForTesting val initialSize = SizeAtProgress(0f, 0f, 0f) 388 389 var currentWidth: Float = 0f 390 private set 391 var currentHeight: Float = 0f 392 private set 393 394 /** 395 * Sets the max size of the ripple. 396 * 397 * <p>Use this if the ripple shape simply changes linearly. 398 */ setMaxSizenull399 fun setMaxSize(width: Float, height: Float) { 400 setSizeAtProgresses(initialSize, SizeAtProgress(1f, width, height)) 401 } 402 403 /** 404 * Sets the list of [sizes]. 405 * 406 * <p>Note that setting this clears the existing sizes. 407 */ setSizeAtProgressesnull408 fun setSizeAtProgresses(vararg sizes: SizeAtProgress) { 409 // Reset everything. 410 this.sizes.clear() 411 currentSizeIndex = 0 412 413 this.sizes.addAll(sizes) 414 this.sizes.sortBy { it.t } 415 } 416 417 /** 418 * Updates the current ripple size based on the progress. 419 * 420 * <p>Should be called when progress updates. 421 */ updatenull422 fun update(progress: Float) { 423 val targetIndex = updateTargetIndex(progress) 424 val prevIndex = Math.max(targetIndex - 1, 0) 425 426 val targetSize = sizes[targetIndex] 427 val prevSize = sizes[prevIndex] 428 429 val subProgress = subProgress(prevSize.t, targetSize.t, progress) 430 431 currentWidth = targetSize.width * subProgress + prevSize.width 432 currentHeight = targetSize.height * subProgress + prevSize.height 433 } 434 updateTargetIndexnull435 private fun updateTargetIndex(progress: Float): Int { 436 if (sizes.isEmpty()) { 437 // It could be empty on init. 438 if (progress > 0f) { 439 Log.e( 440 TAG, 441 "Did you forget to set the ripple size? Use [setMaxSize] or " + 442 "[setSizeAtProgresses] before playing the animation." 443 ) 444 } 445 // If there's no size is set, we set everything to 0 and return early. 446 setSizeAtProgresses(initialSize) 447 return currentSizeIndex 448 } 449 450 var candidate = sizes[currentSizeIndex] 451 452 while (progress > candidate.t) { 453 currentSizeIndex = Math.min(currentSizeIndex + 1, sizes.size - 1) 454 candidate = sizes[currentSizeIndex] 455 } 456 457 return currentSizeIndex 458 } 459 } 460 } 461