Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit dbf31b5f authored by Shan Huang's avatar Shan Huang Committed by Automerger Merge Worker
Browse files

Merge "Add the charging ripple effect, currently guarded by...

Merge "Add the charging ripple effect, currently guarded by flag_charging_ripple." into sc-dev am: b2e08e97

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/13774840

Change-Id: Ibc0bb012ad9c4b8eac18a61deb5eff7054d9bf52
parents 76e101a5 b2e08e97
Loading
Loading
Loading
Loading
+2 −0
Original line number Original line Diff line number Diff line
@@ -50,4 +50,6 @@
    <bool name="flag_pm_lite">false</bool>
    <bool name="flag_pm_lite">false</bool>


    <bool name="flag_alarm_tile">false</bool>
    <bool name="flag_alarm_tile">false</bool>

    <bool name="flag_charging_ripple">false</bool>
</resources>
</resources>
+4 −0
Original line number Original line Diff line number Diff line
@@ -98,4 +98,8 @@ public class FeatureFlags {
    public boolean isAlarmTileAvailable() {
    public boolean isAlarmTileAvailable() {
        return mFlagReader.isEnabled(R.bool.flag_alarm_tile);
        return mFlagReader.isEnabled(R.bool.flag_alarm_tile);
    }
    }

    public boolean isChargingRippleEnabled() {
        return mFlagReader.isEnabled(R.bool.flag_charging_ripple);
    }
}
}
+86 −0
Original line number Original line Diff line number Diff line
/*
 * 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.statusbar.charging

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PointF
import android.util.AttributeSet
import android.view.View
import kotlin.math.max

private const val RIPPLE_ANIMATION_DURATION: Long = 1500
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f

/**
 * Expanding ripple effect that shows when charging begins.
 */
class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    private var rippleInProgress: Boolean = false
    private val rippleShader = RippleShader()
    private val defaultColor: Int = 0xffffffff.toInt()
    private val ripplePaint = Paint()

    init {
        rippleShader.color = defaultColor
        rippleShader.progress = 0f
        rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
        ripplePaint.shader = rippleShader
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        rippleShader.origin = PointF(measuredWidth / 2f, measuredHeight.toFloat())
        rippleShader.radius = max(measuredWidth, measuredHeight).toFloat()
        super.onLayout(changed, left, top, right, bottom)
    }

    fun startRipple() {
        if (rippleInProgress) {
            return // Ignore if ripple effect is already playing
        }
        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.duration = RIPPLE_ANIMATION_DURATION
        animator.addUpdateListener { animator ->
            val now = animator.currentPlayTime
            val phase = now / 30000f
            rippleShader.progress = animator.animatedValue as Float
            rippleShader.noisePhase = phase
            invalidate()
        }
        animator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator?) {
                rippleInProgress = false
                visibility = View.GONE
            }
        })
        animator.start()
        visibility = View.VISIBLE
        rippleInProgress = true
    }

    fun setColor(color: Int) {
        rippleShader.color = color
    }

    override fun onDraw(canvas: Canvas?) {
        canvas?.drawRect(0f, 0f, width.toFloat(), height.toFloat(), ripplePaint)
    }
}
+147 −0
Original line number Original line Diff line number Diff line
/*
 * 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.statusbar.charging

import android.graphics.Color
import android.graphics.PointF
import android.graphics.RuntimeShader

/**
 * 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
 * 2. an expanding ring that appears throughout the effect
 * 3. an expanding ring-shaped area that reveals noise over #2.
 *
 * Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java.
 */
class RippleShader internal constructor() : RuntimeShader(SHADER, false) {
    companion object {
        private const val SHADER_UNIFORMS = """uniform vec2 in_origin;
                uniform float in_progress;
                uniform float in_maxRadius;
                uniform float in_noisePhase;
                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));
                }

                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.25;
                    float h = l + 0.025;
                    float o = abs(sin(0.1 * PI * (t + i)));
                    s += threshold(n + o, l, h);
                  }
                  return saturate(s);
                }

                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 softRing(vec2 uv, vec2 xy, float radius, float blur) {
                  float thickness = 0.4;
                  float circle_outer = softCircle(uv, xy,
                      radius + thickness * radius * 0.5, blur);
                  float circle_inner = softCircle(uv, xy,
                      radius - thickness * radius * 0.5, blur);
                  return circle_outer - circle_inner;
                }

                float subProgress(float start, float end, float progress) {
                    float sub = clamp(progress, start, end);
                    return (sub - start) / (end - start);
                }

                float smoothstop2(float t) {
                  return 1 - (1 - t) * (1 - t);
                }"""
        private const val SHADER_MAIN = """vec4 main(vec2 p) {
                    float fadeIn = subProgress(0., 0.1, in_progress);
                    float fadeOutNoise = subProgress(0.8, 1., in_progress);
                    float fadeOutRipple = subProgress(0.7, 1., in_progress);
                    float fadeCircle = subProgress(0., 0.5, in_progress);
                    float radius = smoothstop2(in_progress) * in_maxRadius;
                    float sparkleRing = softRing(p, in_origin, radius, 0.5);
                    float sparkleAlpha = min(fadeIn, 1. - fadeOutNoise);
                    float sparkle = sparkles(p, in_noisePhase) * sparkleRing * sparkleAlpha;
                    float circle = softCircle(p, in_origin, radius * 1.2, 0.5)
                        * (1 - fadeCircle);
                    float fadeRipple = min(fadeIn, 1.-fadeOutRipple);
                    float rippleAlpha = softRing(p, in_origin, radius, 0.5)
                        * fadeRipple * in_color.a;
                    vec4 ripple = in_color * max(circle, rippleAlpha) * 0.4;
                    return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
                }"""
        private const val SHADER = SHADER_UNIFORMS + SHADER_LIB + SHADER_MAIN
    }

    /**
     * Maximum radius of the ripple.
     */
    var radius: Float = 0.0f
        set(value) { setUniform("in_maxRadius", value) }

    /**
     * Origin coordinate of the ripple.
     */
    var origin: PointF = PointF()
        set(value) { setUniform("in_origin", floatArrayOf(value.x, value.y)) }

    /**
     * Progress of the ripple. Float value between [0, 1].
     */
    var progress: Float = 0.0f
        set(value) { setUniform("in_progress", value) }

    /**
     * Continuous offset used as noise phase.
     */
    var noisePhase: Float = 0.0f
        set(value) { setUniform("in_noisePhase", value) }

    /**
     * A hex value representing the ripple color, in the format of ARGB
     */
    var color: Int = 0xffffff.toInt()
        set(value) {
            val color = Color.valueOf(value)
            setUniform("in_color", floatArrayOf(color.red(),
                    color.green(), color.blue(), color.alpha()))
        }

    /**
     * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus
     * with strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1
     * it's opaque white and looks the most grainy.
     */
    var sparkleStrength: Float = 0.0f
        set(value) { setUniform("in_sparkle_strength", value) }
}
+137 −0
Original line number Original line Diff line number Diff line
/*
 * 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.statusbar.charging

import android.content.Context
import android.content.res.Configuration
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroupOverlay
import com.android.internal.annotations.VisibleForTesting
import com.android.settingslib.Utils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.FeatureFlags
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import java.io.PrintWriter
import javax.inject.Inject

/***
 * Controls the ripple effect that shows when wired charging begins.
 * The ripple uses the accent color of the current theme.
 */
@SysUISingleton
class WiredChargingRippleController @Inject constructor(
    commandRegistry: CommandRegistry,
    batteryController: BatteryController,
    configurationController: ConfigurationController,
    featureFlags: FeatureFlags,
    private val context: Context,
    private val keyguardStateController: KeyguardStateController
) {
    private var pluggedIn: Boolean? = null
    private val rippleEnabled: Boolean = featureFlags.isChargingRippleEnabled
    @VisibleForTesting
    var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null)

    init {
        val batteryStateChangeCallback = object : BatteryController.BatteryStateChangeCallback {
            override fun onBatteryLevelChanged(
                level: Int,
                nowPluggedIn: Boolean,
                charging: Boolean
            ) {
                if (!rippleEnabled) {
                    return
                }
                val wasPluggedIn = pluggedIn
                pluggedIn = nowPluggedIn
                // Only triggers when the keyguard is active and the device is just plugged in.
                if (wasPluggedIn == false && nowPluggedIn && keyguardStateController.isShowing) {
                    rippleView.startRipple()
                }
            }
        }
        batteryController.addCallback(batteryStateChangeCallback)

        val configurationChangedListener = object : ConfigurationController.ConfigurationListener {
            override fun onUiModeChanged() {
                updateRippleColor()
            }
            override fun onThemeChanged() {
                updateRippleColor()
            }
            override fun onOverlayChanged() {
                updateRippleColor()
            }
            override fun onConfigChanged(newConfig: Configuration?) {
                layoutRippleView()
            }
        }
        configurationController.addCallback(configurationChangedListener)

        commandRegistry.registerCommand("charging-ripple") { ChargingRippleCommand() }
    }

    fun setViewHost(viewHost: View) {
        // Add the ripple view as an overlay of the root view so that it always
        // shows on top.
        viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
            override fun onViewDetachedFromWindow(view: View?) {}

            override fun onViewAttachedToWindow(view: View?) {
                (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay).add(rippleView)
                layoutRippleView()
                viewHost.removeOnAttachStateChangeListener(this)
            }
        })

        updateRippleColor()
    }

    private fun layoutRippleView() {
        // Overlays are not auto measured and laid out so we do it manually here.
        val displayMetrics = DisplayMetrics()
        context.display.getRealMetrics(displayMetrics)
        val width = displayMetrics.widthPixels
        val height = displayMetrics.heightPixels
        if (width != rippleView.width || height != rippleView.height) {
            rippleView.measure(
                    View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
                    View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))
            rippleView.layout(0, 0, width, height)
        }
    }

    private fun updateRippleColor() {
        rippleView.setColor(
                Utils.getColorAttr(context, android.R.attr.colorAccent).defaultColor)
    }

    inner class ChargingRippleCommand : Command {
        override fun execute(pw: PrintWriter, args: List<String>) {
            rippleView.startRipple()
        }

        override fun help(pw: PrintWriter) {
            pw.println("Usage: adb shell cmd statusbar charging-ripple")
        }
    }
}
Loading