Make RippleShader support circle and rounded box shapes.
There will be a follow up CL for adding a oval shape. This CL breaks the RippleShader into smaller pieces and glue only the neccessary parts together when initializing. Note that this CL, the rounded box shape is not being used. Bug: b/237282686 Test: Manual, WiredChargingRippleControllerTest Change-Id: I09091a612e3307a76a0bc938ce0810153541243a
This commit is contained in:
@@ -75,12 +75,12 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
|
||||
}
|
||||
private var radius: Float = 0f
|
||||
set(value) {
|
||||
rippleShader.radius = value
|
||||
rippleShader.setMaxSize(value * 2f, value * 2f)
|
||||
field = value
|
||||
}
|
||||
private var origin: PointF = PointF()
|
||||
set(value) {
|
||||
rippleShader.origin = value
|
||||
rippleShader.setCenter(value.x, value.y)
|
||||
field = value
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ package com.android.systemui.charging
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.PointF
|
||||
import android.os.SystemProperties
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
@@ -85,7 +84,7 @@ class WiredChargingRippleController @Inject constructor(
|
||||
private var debounceLevel = 0
|
||||
|
||||
@VisibleForTesting
|
||||
var rippleView: RippleView = RippleView(context, attrs = null)
|
||||
var rippleView: RippleView = RippleView(context, attrs = null).also { it.setupShader() }
|
||||
|
||||
init {
|
||||
pluggedIn = batteryController.isPluggedIn
|
||||
@@ -177,20 +176,25 @@ class WiredChargingRippleController @Inject constructor(
|
||||
context.display.getRealMetrics(displayMetrics)
|
||||
val width = displayMetrics.widthPixels
|
||||
val height = displayMetrics.heightPixels
|
||||
rippleView.radius = Integer.max(width, height).toFloat()
|
||||
rippleView.origin = when (RotationUtils.getExactRotation(context)) {
|
||||
val maxDiameter = Integer.max(width, height) * 2f
|
||||
rippleView.setMaxSize(maxDiameter, maxDiameter)
|
||||
when (RotationUtils.getExactRotation(context)) {
|
||||
RotationUtils.ROTATION_LANDSCAPE -> {
|
||||
PointF(width * normalizedPortPosY, height * (1 - normalizedPortPosX))
|
||||
rippleView.setCenter(
|
||||
width * normalizedPortPosY, height * (1 - normalizedPortPosX))
|
||||
}
|
||||
RotationUtils.ROTATION_UPSIDE_DOWN -> {
|
||||
PointF(width * (1 - normalizedPortPosX), height * (1 - normalizedPortPosY))
|
||||
rippleView.setCenter(
|
||||
width * (1 - normalizedPortPosX), height * (1 - normalizedPortPosY))
|
||||
}
|
||||
RotationUtils.ROTATION_SEASCAPE -> {
|
||||
PointF(width * (1 - normalizedPortPosY), height * normalizedPortPosX)
|
||||
rippleView.setCenter(
|
||||
width * (1 - normalizedPortPosY), height * normalizedPortPosX)
|
||||
}
|
||||
else -> {
|
||||
// ROTATION_NONE
|
||||
PointF(width * normalizedPortPosX, height * normalizedPortPosY)
|
||||
rippleView.setCenter(
|
||||
width * normalizedPortPosX, height * normalizedPortPosY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PointF;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ContextThemeWrapper;
|
||||
@@ -34,6 +33,7 @@ import android.widget.TextView;
|
||||
import com.android.settingslib.Utils;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.animation.Interpolators;
|
||||
import com.android.systemui.ripple.RippleShader;
|
||||
import com.android.systemui.ripple.RippleView;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
@@ -138,6 +138,8 @@ public class WirelessChargingLayout extends FrameLayout {
|
||||
animatorSetScrim.start();
|
||||
|
||||
mRippleView = findViewById(R.id.wireless_charging_ripple);
|
||||
// TODO: Make rounded box shape if the device is tablet.
|
||||
mRippleView.setupShader(RippleShader.RippleShape.CIRCLE);
|
||||
OnAttachStateChangeListener listener = new OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View view) {
|
||||
@@ -230,11 +232,11 @@ public class WirelessChargingLayout extends FrameLayout {
|
||||
if (mRippleView != null) {
|
||||
int width = getMeasuredWidth();
|
||||
int height = getMeasuredHeight();
|
||||
mRippleView.setColor(
|
||||
Utils.getColorAttr(mRippleView.getContext(),
|
||||
android.R.attr.colorAccent).getDefaultColor());
|
||||
mRippleView.setOrigin(new PointF(width / 2, height / 2));
|
||||
mRippleView.setRadius(Math.max(width, height) * 0.5f);
|
||||
mRippleView.setCenter(width * 0.5f, height * 0.5f);
|
||||
float maxSize = Math.max(width, height);
|
||||
mRippleView.setMaxSize(maxSize, maxSize);
|
||||
mRippleView.setColor(Utils.getColorAttr(mRippleView.getContext(),
|
||||
android.R.attr.colorAccent).getDefaultColor());
|
||||
}
|
||||
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
|
||||
@@ -19,7 +19,6 @@ package com.android.systemui.media.taptotransfer.receiver
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.StatusBarManager
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.Icon
|
||||
import android.media.MediaRoute2Info
|
||||
@@ -201,10 +200,10 @@ class MediaTttChipControllerReceiver @Inject constructor(
|
||||
val height = windowBounds.height()
|
||||
val width = windowBounds.width()
|
||||
|
||||
rippleView.radius = height / 5f
|
||||
val maxDiameter = height / 2.5f
|
||||
rippleView.setMaxSize(maxDiameter, maxDiameter)
|
||||
// Center the ripple on the bottom of the screen in the middle.
|
||||
rippleView.origin = PointF(/* x= */ width / 2f, /* y= */ height.toFloat())
|
||||
|
||||
rippleView.setCenter(width * 0.5f, height.toFloat())
|
||||
val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
|
||||
val colorWithAlpha = ColorUtils.setAlphaComponent(color, 70)
|
||||
rippleView.setColor(colorWithAlpha)
|
||||
|
||||
@@ -23,10 +23,9 @@ import com.android.systemui.ripple.RippleView
|
||||
/**
|
||||
* An expanding ripple effect for the media tap-to-transfer receiver chip.
|
||||
*/
|
||||
class ReceiverChipRippleView(
|
||||
context: Context?, attrs: AttributeSet?
|
||||
) : RippleView(context, attrs) {
|
||||
class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleView(context, attrs) {
|
||||
init {
|
||||
setupShader()
|
||||
setRippleFill(true)
|
||||
duration = 3000L
|
||||
}
|
||||
|
||||
@@ -20,92 +20,83 @@ import android.graphics.RuntimeShader
|
||||
import android.util.MathUtils
|
||||
|
||||
/**
|
||||
* Shader class that renders an expanding charging ripple effect. A charging ripple contains
|
||||
* three elements:
|
||||
* 1. an expanding filled circle that appears in the beginning and quickly fades away
|
||||
* Shader class that renders an expanding ripple effect. The ripple contains three elements:
|
||||
*
|
||||
* 1. an expanding filled [RippleShape] that appears in the beginning and quickly fades away
|
||||
* 2. an expanding ring that appears throughout the effect
|
||||
* 3. an expanding ring-shaped area that reveals noise over #2.
|
||||
*
|
||||
* The ripple shader will be default to the circle shape if not specified.
|
||||
*
|
||||
* Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java.
|
||||
*/
|
||||
class RippleShader internal constructor() : RuntimeShader(SHADER) {
|
||||
class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.CIRCLE) :
|
||||
RuntimeShader(buildShader(rippleShape)) {
|
||||
|
||||
/** Shapes that the [RippleShader] supports. */
|
||||
enum class RippleShape {
|
||||
CIRCLE,
|
||||
ROUNDED_BOX
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SHADER_UNIFORMS = """uniform vec2 in_origin;
|
||||
private const val SHADER_UNIFORMS = """uniform vec2 in_center;
|
||||
uniform vec2 in_size;
|
||||
uniform float in_progress;
|
||||
uniform float in_maxRadius;
|
||||
uniform float in_cornerRadius;
|
||||
uniform float in_thickness;
|
||||
uniform float in_time;
|
||||
uniform float in_distort_radial;
|
||||
uniform float in_distort_xy;
|
||||
uniform float in_radius;
|
||||
uniform float in_fadeSparkle;
|
||||
uniform float in_fadeCircle;
|
||||
uniform float in_fadeFill;
|
||||
uniform float in_fadeRing;
|
||||
uniform float in_blur;
|
||||
uniform float in_pixelDensity;
|
||||
layout(color) uniform vec4 in_color;
|
||||
uniform float in_sparkle_strength;"""
|
||||
private const val SHADER_LIB = """float triangleNoise(vec2 n) {
|
||||
n = fract(n * vec2(5.3987, 5.4421));
|
||||
n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));
|
||||
float xy = n.x * n.y;
|
||||
return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;
|
||||
}
|
||||
const float PI = 3.1415926535897932384626;
|
||||
|
||||
float threshold(float v, float l, float h) {
|
||||
return step(l, v) * (1.0 - step(h, v));
|
||||
}
|
||||
private const val SHADER_CIRCLE_MAIN = """vec4 main(vec2 p) {
|
||||
// distortion only applies to circle
|
||||
vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
|
||||
float radius = in_size.x * 0.5;
|
||||
|
||||
float sparkles(vec2 uv, float t) {
|
||||
float n = triangleNoise(uv);
|
||||
float s = 0.0;
|
||||
for (float i = 0; i < 4; i += 1) {
|
||||
float l = i * 0.01;
|
||||
float h = l + 0.1;
|
||||
float o = smoothstep(n - l, h, n);
|
||||
o *= abs(sin(PI * o * (t + 0.55 * i)));
|
||||
s += o;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
float sparkleRing = softRing(p_distorted, in_center, radius, in_blur);
|
||||
float inside = softCircle(p_distorted, in_center, radius * 1.2, in_blur);
|
||||
float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
|
||||
* (1.-sparkleRing) * in_fadeSparkle;
|
||||
|
||||
float softCircle(vec2 uv, vec2 xy, float radius, float blur) {
|
||||
float blurHalf = blur * 0.5;
|
||||
float d = distance(uv, xy);
|
||||
return 1. - smoothstep(1. - blurHalf, 1. + blurHalf, d / radius);
|
||||
}
|
||||
float rippleInsideAlpha = (1.-inside) * in_fadeFill;
|
||||
float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
|
||||
float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * 0.45;
|
||||
vec4 ripple = in_color * rippleAlpha;
|
||||
return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
|
||||
}
|
||||
"""
|
||||
|
||||
float softRing(vec2 uv, vec2 xy, float radius, float blur) {
|
||||
float thickness_half = radius * 0.25;
|
||||
float circle_outer = softCircle(uv, xy, radius + thickness_half, blur);
|
||||
float circle_inner = softCircle(uv, xy, radius - thickness_half, blur);
|
||||
return circle_outer - circle_inner;
|
||||
}
|
||||
private const val SHADER_ROUNDED_BOX_MAIN = """vec4 main(vec2 p) {
|
||||
float sparkleRing = softRoundedBoxRing(p, in_center, in_size, in_cornerRadius,
|
||||
in_thickness, in_blur);
|
||||
float inside = softRoundedBox(p, in_center, in_size * 1.2, in_cornerRadius, in_blur);
|
||||
float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
|
||||
* (1.-sparkleRing) * in_fadeSparkle;
|
||||
|
||||
vec2 distort(vec2 p, vec2 origin, float time,
|
||||
float distort_amount_radial, float distort_amount_xy) {
|
||||
float2 distance = origin - p;
|
||||
float angle = atan(distance.y, distance.x);
|
||||
return p + vec2(sin(angle * 8 + time * 0.003 + 1.641),
|
||||
cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial
|
||||
+ vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123),
|
||||
cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy;
|
||||
}"""
|
||||
private const val SHADER_MAIN = """vec4 main(vec2 p) {
|
||||
vec2 p_distorted = distort(p, in_origin, in_time, in_distort_radial,
|
||||
in_distort_xy);
|
||||
float rippleInsideAlpha = (1.-inside) * in_fadeFill;
|
||||
float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
|
||||
float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * 0.45;
|
||||
vec4 ripple = in_color * rippleAlpha;
|
||||
return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
|
||||
}
|
||||
"""
|
||||
|
||||
// Draw shapes
|
||||
float sparkleRing = softRing(p_distorted, in_origin, in_radius, in_blur);
|
||||
float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
|
||||
* sparkleRing * in_fadeSparkle;
|
||||
float circle = softCircle(p_distorted, in_origin, in_radius * 1.2, in_blur);
|
||||
float rippleAlpha = max(circle * in_fadeCircle,
|
||||
softRing(p_distorted, in_origin, in_radius, in_blur) * in_fadeRing) * 0.45;
|
||||
vec4 ripple = in_color * rippleAlpha;
|
||||
return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
|
||||
}"""
|
||||
private const val SHADER = SHADER_UNIFORMS + SHADER_LIB + SHADER_MAIN
|
||||
private const val CIRCLE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB +
|
||||
SdfShaderLibrary.CIRCLE_SDF + SHADER_CIRCLE_MAIN
|
||||
private const val ROUNDED_BOX_SHADER = SHADER_UNIFORMS +
|
||||
RippleShaderUtilLibrary.SHADER_LIB + SdfShaderLibrary.ROUNDED_BOX_SDF +
|
||||
SHADER_ROUNDED_BOX_MAIN
|
||||
|
||||
private fun buildShader(rippleShape: RippleShape): String =
|
||||
if (rippleShape == RippleShape.CIRCLE) CIRCLE_SHADER else ROUNDED_BOX_SHADER
|
||||
|
||||
private fun subProgress(start: Float, end: Float, progress: Float): Float {
|
||||
val min = Math.min(start, end)
|
||||
@@ -116,22 +107,18 @@ class RippleShader internal constructor() : RuntimeShader(SHADER) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum radius of the ripple.
|
||||
* Sets the center position of the ripple.
|
||||
*/
|
||||
var radius: Float = 0.0f
|
||||
set(value) {
|
||||
field = value
|
||||
setFloatUniform("in_maxRadius", value)
|
||||
}
|
||||
fun setCenter(x: Float, y: Float) {
|
||||
setFloatUniform("in_center", x, y)
|
||||
}
|
||||
|
||||
/**
|
||||
* Origin coordinate of the ripple.
|
||||
*/
|
||||
var origin: PointF = PointF()
|
||||
set(value) {
|
||||
field = value
|
||||
setFloatUniform("in_origin", value.x, value.y)
|
||||
}
|
||||
/** Max width of the ripple. */
|
||||
private var maxSize: PointF = PointF()
|
||||
fun setMaxSize(width: Float, height: Float) {
|
||||
maxSize.x = width
|
||||
maxSize.y = height
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress of the ripple. Float value between [0, 1].
|
||||
@@ -140,20 +127,27 @@ class RippleShader internal constructor() : RuntimeShader(SHADER) {
|
||||
set(value) {
|
||||
field = value
|
||||
setFloatUniform("in_progress", value)
|
||||
setFloatUniform("in_radius",
|
||||
(1 - (1 - value) * (1 - value) * (1 - value))* radius)
|
||||
val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
|
||||
|
||||
setFloatUniform("in_size", /* width= */ maxSize.x * curvedProg,
|
||||
/* height= */ maxSize.y * curvedProg)
|
||||
setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
|
||||
// radius should not exceed width and height values.
|
||||
setFloatUniform("in_cornerRadius",
|
||||
Math.min(maxSize.x, maxSize.y) * curvedProg)
|
||||
|
||||
setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value))
|
||||
|
||||
val fadeIn = subProgress(0f, 0.1f, value)
|
||||
val fadeOutNoise = subProgress(0.4f, 1f, value)
|
||||
var fadeOutRipple = 0f
|
||||
var fadeCircle = 0f
|
||||
var fadeFill = 0f
|
||||
if (!rippleFill) {
|
||||
fadeCircle = subProgress(0f, 0.2f, value)
|
||||
fadeFill = subProgress(0f, 0.6f, value)
|
||||
fadeOutRipple = subProgress(0.3f, 1f, value)
|
||||
}
|
||||
setFloatUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
|
||||
setFloatUniform("in_fadeCircle", 1 - fadeCircle)
|
||||
setFloatUniform("in_fadeFill", 1 - fadeFill)
|
||||
setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
|
||||
}
|
||||
|
||||
@@ -169,7 +163,7 @@ class RippleShader internal constructor() : RuntimeShader(SHADER) {
|
||||
/**
|
||||
* A hex value representing the ripple color, in the format of ARGB
|
||||
*/
|
||||
var color: Int = 0xffffff.toInt()
|
||||
var color: Int = 0xffffff
|
||||
set(value) {
|
||||
field = value
|
||||
setColorUniform("in_color", value)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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
|
||||
|
||||
/** A common utility functions that are used for computing [RippleShader]. */
|
||||
class RippleShaderUtilLibrary {
|
||||
companion object {
|
||||
const val SHADER_LIB = """float triangleNoise(vec2 n) {
|
||||
n = fract(n * vec2(5.3987, 5.4421));
|
||||
n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));
|
||||
float xy = n.x * n.y;
|
||||
return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;
|
||||
}
|
||||
const float PI = 3.1415926535897932384626;
|
||||
|
||||
float sparkles(vec2 uv, float t) {
|
||||
float n = triangleNoise(uv);
|
||||
float s = 0.0;
|
||||
for (float i = 0; i < 4; i += 1) {
|
||||
float l = i * 0.01;
|
||||
float h = l + 0.1;
|
||||
float o = smoothstep(n - l, h, n);
|
||||
o *= abs(sin(PI * o * (t + 0.55 * i)));
|
||||
s += o;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
vec2 distort(vec2 p, float time, float distort_amount_radial,
|
||||
float distort_amount_xy) {
|
||||
float angle = atan(p.y, p.x);
|
||||
return p + vec2(sin(angle * 8 + time * 0.003 + 1.641),
|
||||
cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial
|
||||
+ vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123),
|
||||
cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy;
|
||||
}"""
|
||||
}
|
||||
}
|
||||
@@ -23,39 +23,42 @@ import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
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. To trigger the ripple expansion, set [radius] and [origin],
|
||||
* then call [startRipple].
|
||||
* 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 val rippleShader = RippleShader()
|
||||
private val defaultColor: Int = 0xffffffff.toInt()
|
||||
|
||||
private lateinit var rippleShader: RippleShader
|
||||
private lateinit var rippleShape: RippleShape
|
||||
private val ripplePaint = Paint()
|
||||
|
||||
var rippleInProgress: Boolean = false
|
||||
var radius: Float = 0.0f
|
||||
set(value) {
|
||||
rippleShader.radius = value
|
||||
field = value
|
||||
}
|
||||
var origin: PointF = PointF()
|
||||
set(value) {
|
||||
rippleShader.origin = value
|
||||
field = value
|
||||
}
|
||||
var duration: Long = 1750
|
||||
|
||||
init {
|
||||
rippleShader.color = defaultColor
|
||||
rippleShader.progress = 0f
|
||||
rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
|
||||
ripplePaint.shader = rippleShader
|
||||
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?) {
|
||||
@@ -68,6 +71,18 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
|
||||
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
|
||||
|
||||
ripplePaint.shader = rippleShader
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun startRipple(onAnimationEnd: Runnable? = null) {
|
||||
if (rippleInProgress) {
|
||||
@@ -113,11 +128,24 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
|
||||
// if it's unsupported.
|
||||
return
|
||||
}
|
||||
// To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover
|
||||
// the active effect area. Values here should be kept in sync with the
|
||||
// animation implementation in the ripple shader.
|
||||
val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
|
||||
(1 - rippleShader.progress)) * radius * 2
|
||||
canvas.drawCircle(origin.x, origin.y, maskRadius, ripplePaint)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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
|
||||
|
||||
/** Library class that contains 2D signed distance functions. */
|
||||
class SdfShaderLibrary {
|
||||
companion object {
|
||||
const val CIRCLE_SDF = """
|
||||
float sdCircle(vec2 p, float r) {
|
||||
return (length(p)-r) / r;
|
||||
}
|
||||
|
||||
float softCircle(vec2 p, vec2 origin, float radius, float blur) {
|
||||
float d = sdCircle(p-origin, radius);
|
||||
float blurHalf = blur * 0.5;
|
||||
return smoothstep(-blurHalf, blurHalf, d);
|
||||
}
|
||||
|
||||
float softRing(vec2 p, vec2 origin, float radius, float blur) {
|
||||
float thicknessHalf = radius * 0.25;
|
||||
|
||||
float outerCircle = sdCircle(p-origin, radius + thicknessHalf);
|
||||
float innerCircle = sdCircle(p-origin, radius);
|
||||
|
||||
float d = max(outerCircle, -innerCircle);
|
||||
float blurHalf = blur * 0.5;
|
||||
|
||||
return smoothstep(-blurHalf, blurHalf, d);
|
||||
}
|
||||
"""
|
||||
|
||||
const val ROUNDED_BOX_SDF = """
|
||||
float sdRoundedBox(vec2 p, vec2 size, float cornerRadius) {
|
||||
size *= 0.5;
|
||||
cornerRadius *= 0.5;
|
||||
vec2 d = abs(p)-size+cornerRadius;
|
||||
|
||||
float outside = length(max(d, 0.0));
|
||||
float inside = min(max(d.x, d.y), 0.0);
|
||||
|
||||
return (outside+inside-cornerRadius)/size.y;
|
||||
}
|
||||
|
||||
float softRoundedBox(vec2 p, vec2 origin, vec2 size, float cornerRadius, float blur) {
|
||||
float d = sdRoundedBox(p-origin, size, cornerRadius);
|
||||
float blurHalf = blur * 0.5;
|
||||
return smoothstep(-blurHalf, blurHalf, d);
|
||||
}
|
||||
|
||||
float softRoundedBoxRing(vec2 p, vec2 origin, vec2 size, float cornerRadius,
|
||||
float borderThickness, float blur) {
|
||||
|
||||
float outerRoundBox = sdRoundedBox(p-origin, size, cornerRadius);
|
||||
float innerRoundBox = sdRoundedBox(p-origin, size - vec2(borderThickness),
|
||||
cornerRadius - borderThickness);
|
||||
|
||||
float d = max(outerRoundBox, -innerRoundBox);
|
||||
float blurHalf = blur * 0.5;
|
||||
|
||||
return smoothstep(-blurHalf, blurHalf, d);
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,12 @@ import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.eq
|
||||
import org.mockito.Mockito.reset
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.reset
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@@ -63,6 +63,7 @@ class WiredChargingRippleControllerTest : SysuiTestCase() {
|
||||
controller = WiredChargingRippleController(
|
||||
commandRegistry, batteryController, configurationController,
|
||||
featureFlags, context, windowManager, systemClock, uiEventLogger)
|
||||
rippleView.setupShader()
|
||||
controller.rippleView = rippleView // Replace the real ripple view with a mock instance
|
||||
controller.registerCallbacks()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class RippleViewTest : SysuiTestCase() {
|
||||
@Mock
|
||||
private lateinit var rippleView: RippleView
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
rippleView = RippleView(context, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetupShader_compilesCircle() {
|
||||
rippleView.setupShader(RippleShader.RippleShape.CIRCLE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetupShader_compilesRoundedBox() {
|
||||
rippleView.setupShader(RippleShader.RippleShape.ROUNDED_BOX)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user