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. * Expanding ripple effect that shows when charging begins.
*/ */
class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private var rippleInProgress: Boolean = false
private val rippleShader = RippleShader() private val rippleShader = RippleShader()
private val defaultColor: Int = 0xffffffff.toInt() private val defaultColor: Int = 0xffffffff.toInt()
private val ripplePaint = Paint() private val ripplePaint = Paint()
var rippleInProgress: Boolean = false
var radius: Float = 0.0f var radius: Float = 0.0f
set(value) { rippleShader.radius = value } set(value) { rippleShader.radius = value }
var origin: PointF = PointF() var origin: PointF = PointF()
@@ -62,7 +62,8 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context
super.onAttachedToWindow() super.onAttachedToWindow()
} }
fun startRipple() { @JvmOverloads
fun startRipple(onAnimationEnd: Runnable? = null) {
if (rippleInProgress) { if (rippleInProgress) {
return // Ignore if ripple effect is already playing 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?) { override fun onAnimationEnd(animation: Animator?) {
rippleInProgress = false rippleInProgress = false
visibility = View.GONE visibility = View.GONE
onAnimationEnd?.run()
} }
}) })
animator.start() animator.start()

View File

@@ -17,11 +17,11 @@
package com.android.systemui.statusbar.charging package com.android.systemui.statusbar.charging
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.graphics.PixelFormat
import android.graphics.PointF import android.graphics.PointF
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.view.ViewGroupOverlay import android.view.WindowManager
import com.android.internal.annotations.VisibleForTesting import com.android.internal.annotations.VisibleForTesting
import com.android.settingslib.Utils import com.android.settingslib.Utils
import com.android.systemui.dagger.SysUISingleton 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.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import java.io.PrintWriter import java.io.PrintWriter
import java.lang.Integer.max
import javax.inject.Inject import javax.inject.Inject
/*** /***
@@ -45,11 +43,22 @@ class WiredChargingRippleController @Inject constructor(
batteryController: BatteryController, batteryController: BatteryController,
configurationController: ConfigurationController, configurationController: ConfigurationController,
featureFlags: FeatureFlags, featureFlags: FeatureFlags,
private val context: Context, private val context: Context
private val keyguardStateController: KeyguardStateController
) { ) {
private var charging: Boolean? = null private var charging: Boolean? = null
private val rippleEnabled: Boolean = featureFlags.isChargingRippleEnabled 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 @VisibleForTesting
var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null) var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null)
@@ -68,8 +77,8 @@ class WiredChargingRippleController @Inject constructor(
val wasCharging = charging val wasCharging = charging
charging = nowCharging charging = nowCharging
// Only triggers when the keyguard is active and the device is just plugged in. // Only triggers when the keyguard is active and the device is just plugged in.
if (wasCharging == false && nowCharging && keyguardStateController.isShowing) { if ((wasCharging == null || !wasCharging) && nowCharging) {
rippleView.startRipple() startRipple()
} }
} }
} }
@@ -85,46 +94,41 @@ class WiredChargingRippleController @Inject constructor(
override fun onOverlayChanged() { override fun onOverlayChanged() {
updateRippleColor() updateRippleColor()
} }
override fun onConfigChanged(newConfig: Configuration?) {
layoutRippleView()
}
} }
configurationController.addCallback(configurationChangedListener) configurationController.addCallback(configurationChangedListener)
commandRegistry.registerCommand("charging-ripple") { ChargingRippleCommand() } 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() updateRippleColor()
} }
private fun layoutRippleView() { fun startRipple() {
// Overlays are not auto measured and laid out so we do it manually here. 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() val displayMetrics = DisplayMetrics()
context.display.getRealMetrics(displayMetrics) context.display.getRealMetrics(displayMetrics)
val width = displayMetrics.widthPixels val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels val height = displayMetrics.heightPixels
if (width != rippleView.width || height != rippleView.height) { rippleView.origin = PointF(width / 2f, height.toFloat())
rippleView.apply { rippleView.radius = Integer.max(width, height).toFloat()
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()
}
}
} }
private fun updateRippleColor() { private fun updateRippleColor() {
@@ -134,7 +138,7 @@ class WiredChargingRippleController @Inject constructor(
inner class ChargingRippleCommand : Command { inner class ChargingRippleCommand : Command {
override fun execute(pw: PrintWriter, args: List<String>) { override fun execute(pw: PrintWriter, args: List<String>) {
rippleView.startRipple() startRipple()
} }
override fun help(pw: PrintWriter) { override fun help(pw: PrintWriter) {

View File

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

View File

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