Merge "Make RippleShader support circle and rounded box shapes." into tm-qpr-dev
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
|
||||
@@ -202,10 +201,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