Merge "Show ripple in a window when phone is unlocked." into sc-dev

This commit is contained in:
Shan Huang
2021-04-07 13:05:28 +00:00
committed by Android (Google) Code Review
4 changed files with 71 additions and 75 deletions

View File

@@ -33,11 +33,11 @@ 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()
var rippleInProgress: Boolean = false
var radius: Float = 0.0f
set(value) { rippleShader.radius = value }
var origin: PointF = PointF()
@@ -62,7 +62,8 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context
super.onAttachedToWindow()
}
fun startRipple() {
@JvmOverloads
fun startRipple(onAnimationEnd: Runnable? = null) {
if (rippleInProgress) {
return // Ignore if ripple effect is already playing
}
@@ -80,6 +81,7 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context
override fun onAnimationEnd(animation: Animator?) {
rippleInProgress = false
visibility = View.GONE
onAnimationEnd?.run()
}
})
animator.start()

View File

@@ -17,11 +17,11 @@
package com.android.systemui.statusbar.charging
import android.content.Context
import android.content.res.Configuration
import android.graphics.PixelFormat
import android.graphics.PointF
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroupOverlay
import android.view.WindowManager
import com.android.internal.annotations.VisibleForTesting
import com.android.settingslib.Utils
import com.android.systemui.dagger.SysUISingleton
@@ -30,9 +30,7 @@ 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 java.lang.Integer.max
import javax.inject.Inject
/***
@@ -45,11 +43,22 @@ class WiredChargingRippleController @Inject constructor(
batteryController: BatteryController,
configurationController: ConfigurationController,
featureFlags: FeatureFlags,
private val context: Context,
private val keyguardStateController: KeyguardStateController
private val context: Context
) {
private var charging: Boolean? = null
private val rippleEnabled: Boolean = featureFlags.isChargingRippleEnabled
private val windowLayoutParams = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.MATCH_PARENT
height = WindowManager.LayoutParams.MATCH_PARENT
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
format = PixelFormat.TRANSLUCENT
type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
fitInsetsTypes = 0 // Ignore insets from all system bars
title = "Wired Charging Animation"
flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
}
@VisibleForTesting
var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null)
@@ -68,8 +77,8 @@ class WiredChargingRippleController @Inject constructor(
val wasCharging = charging
charging = nowCharging
// Only triggers when the keyguard is active and the device is just plugged in.
if (wasCharging == false && nowCharging && keyguardStateController.isShowing) {
rippleView.startRipple()
if ((wasCharging == null || !wasCharging) && nowCharging) {
startRipple()
}
}
}
@@ -85,46 +94,41 @@ class WiredChargingRippleController @Inject constructor(
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.
fun startRipple() {
if (rippleView.rippleInProgress) {
return
}
val mWM = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowLayoutParams.packageName = context.opPackageName
rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(view: View?) {}
override fun onViewAttachedToWindow(view: View?) {
layoutRipple()
rippleView.startRipple(Runnable {
mWM.removeView(rippleView)
})
rippleView.removeOnAttachStateChangeListener(this)
}
})
mWM.addView(rippleView, windowLayoutParams)
}
private fun layoutRipple() {
// TODO(shanh): Set origin base on phone orientation.
val displayMetrics = DisplayMetrics()
context.display.getRealMetrics(displayMetrics)
val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels
if (width != rippleView.width || height != rippleView.height) {
rippleView.apply {
measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))
layout(0, 0, width, height)
origin = PointF(width / 2f, height.toFloat())
radius = max(width, height).toFloat()
}
}
rippleView.origin = PointF(width / 2f, height.toFloat())
rippleView.radius = Integer.max(width, height).toFloat()
}
private fun updateRippleColor() {
@@ -134,7 +138,7 @@ class WiredChargingRippleController @Inject constructor(
inner class ChargingRippleCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) {
rippleView.startRipple()
startRipple()
}
override fun help(pw: PrintWriter) {

View File

@@ -1228,7 +1228,6 @@ public class StatusBar extends SystemUI implements DemoMode,
mScrimController.attachViews(scrimBehind, scrimInFront, scrimForBubble);
mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim);
mChargingRippleAnimationController.setViewHost(mNotificationShadeWindowView);
updateLightRevealScrimVisibility();
mNotificationPanelViewController.initDependencies(

View File

@@ -16,17 +16,17 @@
package com.android.systemui.statusbar.charging
import android.content.Context
import android.testing.AndroidTestingRunner
import android.view.View
import android.view.ViewGroupOverlay
import android.view.ViewRootImpl
import android.view.WindowManager
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.FeatureFlags
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 com.android.systemui.util.mockito.capture
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,7 +34,8 @@ import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.never
import org.mockito.Mockito.any
import org.mockito.Mockito.eq
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -47,55 +48,45 @@ class WiredChargingRippleControllerTest : SysuiTestCase() {
@Mock private lateinit var batteryController: BatteryController
@Mock private lateinit var featureFlags: FeatureFlags
@Mock private lateinit var configurationController: ConfigurationController
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var rippleView: ChargingRippleView
@Mock private lateinit var viewHost: View
@Mock private lateinit var viewHostRootImpl: ViewRootImpl
@Mock private lateinit var viewGroupOverlay: ViewGroupOverlay
@Mock private lateinit var windowManager: WindowManager
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
`when`(viewHost.viewRootImpl).thenReturn(viewHostRootImpl)
`when`(viewHostRootImpl.view).thenReturn(viewHost)
`when`(viewHost.overlay).thenReturn(viewGroupOverlay)
`when`(featureFlags.isChargingRippleEnabled).thenReturn(true)
`when`(keyguardStateController.isShowing).thenReturn(true)
controller = WiredChargingRippleController(
commandRegistry, batteryController, configurationController,
featureFlags, context, keyguardStateController)
featureFlags, context)
controller.rippleView = rippleView // Replace the real ripple view with a mock instance
controller.setViewHost(viewHost)
context.addMockSystemService(Context.WINDOW_SERVICE, windowManager)
}
@Test
fun testSetRippleViewAsOverlay() {
val listenerCaptor = ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
verify(viewHost).addOnAttachStateChangeListener(listenerCaptor.capture())
// Fake attach to window
listenerCaptor.value.onViewAttachedToWindow(viewHost)
verify(viewGroupOverlay).add(rippleView)
}
@Test
fun testTriggerRipple() {
fun testTriggerRipple_UnlockedState() {
val captor = ArgumentCaptor
.forClass(BatteryController.BatteryStateChangeCallback::class.java)
verify(batteryController).addCallback(captor.capture())
val unusedBatteryLevel = 0
// Verify ripple added to window manager.
captor.value.onBatteryLevelChanged(
unusedBatteryLevel,
false /* plugged in */,
false /* charging */)
verify(rippleView, never()).startRipple()
captor.value.onBatteryLevelChanged(
unusedBatteryLevel,
0 /* unusedBatteryLevel */,
false /* plugged in */,
true /* charging */)
verify(rippleView).startRipple()
val attachListenerCaptor =
ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture())
verify(windowManager).addView(eq(rippleView), any<WindowManager.LayoutParams>())
// Verify ripple started
val runnableCaptor =
ArgumentCaptor.forClass(Runnable::class.java)
attachListenerCaptor.value.onViewAttachedToWindow(rippleView)
verify(rippleView).startRipple(runnableCaptor.capture())
// Verify ripple removed
runnableCaptor.value.run()
verify(windowManager).removeView(rippleView)
}
@Test