Animate LockScreen and AOD clock text for charging events
Test: Manual
Bug: 182719493
Change-Id: I57f7cd834a08d8846a35cdf731611fc7a239b85a
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
index e025e27..00c27bf 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
@@ -42,6 +42,7 @@
android:typeface="monospace"
android:elegantTextHeight="false"
android:singleLine="true"
+ chargeAnimationDelay="350"
dozeWeight="200"
lockScreenWeight="400"
/>
@@ -62,6 +63,7 @@
android:fontFamily="@font/clock"
android:typeface="monospace"
android:elegantTextHeight="false"
+ chargeAnimationDelay="200"
dozeWeight="200"
lockScreenWeight="400"
/>
diff --git a/packages/SystemUI/res-keyguard/values/attrs.xml b/packages/SystemUI/res-keyguard/values/attrs.xml
index eb7a1f7..25be37a 100644
--- a/packages/SystemUI/res-keyguard/values/attrs.xml
+++ b/packages/SystemUI/res-keyguard/values/attrs.xml
@@ -45,5 +45,6 @@
<declare-styleable name="AnimatableClockView">
<attr name="dozeWeight" format="integer" />
<attr name="lockScreenWeight" format="integer" />
+ <attr name="chargeAnimationDelay" format="integer" />
</declare-styleable>
</resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
index ab219f3..60b677a 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
@@ -27,6 +27,7 @@
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.util.ViewController;
import java.util.Locale;
@@ -45,6 +46,7 @@
private int mLockScreenColor;
private boolean mIsDozing;
+ private boolean mIsCharging;
private float mDozeAmount;
private Locale mLocale;
@@ -56,7 +58,8 @@
public AnimatableClockController(
AnimatableClockView view,
StatusBarStateController statusBarStateController,
- BroadcastDispatcher broadcastDispatcher) {
+ BroadcastDispatcher broadcastDispatcher,
+ BatteryController batteryController) {
super(view);
mStatusBarStateController = statusBarStateController;
mIsDozing = mStatusBarStateController.isDozing();
@@ -68,6 +71,16 @@
R.dimen.keyguard_clock_line_spacing_scale_burmese);
mDefaultLineSpacing = getContext().getResources().getFloat(
R.dimen.keyguard_clock_line_spacing_scale);
+
+ batteryController.addCallback(new BatteryController.BatteryStateChangeCallback() {
+ @Override
+ public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
+ if (!mIsCharging && charging) {
+ mView.animateCharge(mIsDozing);
+ }
+ mIsCharging = charging;
+ }
+ });
}
private BroadcastReceiver mLocaleBroadcastReceiver = new BroadcastReceiver() {
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java
index c918d98..0d6f64f 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.java
@@ -42,7 +42,9 @@
private static final CharSequence DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm";
private static final CharSequence SINGLE_LINE_FORMAT_12_HOUR = "h:mm";
private static final CharSequence SINGLE_LINE_FORMAT_24_HOUR = "H:mm";
- private static final long ANIM_DURATION = 300;
+ private static final long DOZE_ANIM_DURATION = 300;
+ private static final long CHARGE_ANIM_DURATION_PHASE_0 = 500;
+ private static final long CHARGE_ANIM_DURATION_PHASE_1 = 1000;
private final Calendar mTime = Calendar.getInstance();
@@ -53,6 +55,7 @@
private int mDozingColor;
private int mLockScreenColor;
private float mLineSpacingScale = 1f;
+ private int mChargeAnimationDelay = 0;
private TextAnimator mTextAnimator = null;
private Runnable mOnTextAnimatorInitialized;
@@ -79,6 +82,8 @@
try {
mDozingWeight = ta.getInt(R.styleable.AnimatableClockView_dozeWeight, 100);
mLockScreenWeight = ta.getInt(R.styleable.AnimatableClockView_lockScreenWeight, 300);
+ mChargeAnimationDelay = ta.getInt(
+ R.styleable.AnimatableClockView_chargeAnimationDelay, 200);
} finally {
ta.recycle();
}
@@ -150,11 +155,36 @@
mLockScreenColor = lockScreenColor;
}
+ void animateCharge(boolean isDozing) {
+ if (mTextAnimator == null || mTextAnimator.isRunning()) {
+ // Skip charge animation if dozing animation is already playing.
+ return;
+ }
+ Runnable startAnimPhase2 = () -> setTextStyle(
+ isDozing ? mDozingWeight : mLockScreenWeight/* weight */,
+ -1,
+ null,
+ true /* animate */,
+ CHARGE_ANIM_DURATION_PHASE_1,
+ 0 /* delay */,
+ null /* onAnimationEnd */);
+ setTextStyle(isDozing ? mLockScreenWeight : mDozingWeight/* weight */,
+ -1,
+ null,
+ true /* animate */,
+ CHARGE_ANIM_DURATION_PHASE_0,
+ mChargeAnimationDelay,
+ startAnimPhase2);
+ }
+
void animateDoze(boolean isDozing, boolean animate) {
setTextStyle(isDozing ? mDozingWeight : mLockScreenWeight /* weight */,
-1,
isDozing ? mDozingColor : mLockScreenColor,
- animate);
+ animate,
+ DOZE_ANIM_DURATION,
+ 0 /* delay */,
+ null /* onAnimationEnd */);
}
/**
@@ -170,15 +200,20 @@
private void setTextStyle(
@IntRange(from = 0, to = 1000) int weight,
@FloatRange(from = 0) float textSize,
- int color,
- boolean animate) {
+ Integer color,
+ boolean animate,
+ long duration,
+ long delay,
+ Runnable onAnimationEnd) {
if (mTextAnimator != null) {
- mTextAnimator.setTextStyle(weight, textSize, color, animate, ANIM_DURATION, null);
+ mTextAnimator.setTextStyle(weight, textSize, color, animate, duration, null,
+ delay, onAnimationEnd);
} else {
// when the text animator is set, update its start values
mOnTextAnimatorInitialized =
() -> mTextAnimator.setTextStyle(
- weight, textSize, color, false, ANIM_DURATION, null);
+ weight, textSize, color, false, duration, null,
+ delay, onAnimationEnd);
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 874b4d9..032ed7d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -49,6 +49,7 @@
import com.android.systemui.statusbar.notification.stack.AnimationProperties;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
import com.android.systemui.statusbar.phone.NotificationIconContainer;
+import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.util.ViewController;
import java.util.Locale;
@@ -70,6 +71,7 @@
private final KeyguardSliceViewController mKeyguardSliceViewController;
private final NotificationIconAreaController mNotificationIconAreaController;
private final BroadcastDispatcher mBroadcastDispatcher;
+ private final BatteryController mBatteryController;
/**
* Clock for both small and large sizes
@@ -118,7 +120,8 @@
BroadcastDispatcher broadcastDispatcher,
PluginManager pluginManager,
FeatureFlags featureFlags,
- @Main Executor uiExecutor) {
+ @Main Executor uiExecutor,
+ BatteryController batteryController) {
super(keyguardClockSwitch);
mResources = resources;
mStatusBarStateController = statusBarStateController;
@@ -130,6 +133,7 @@
mPluginManager = pluginManager;
mIsSmartspaceEnabled = featureFlags.isSmartspaceEnabled();
mUiExecutor = uiExecutor;
+ mBatteryController = batteryController;
}
/**
@@ -156,14 +160,16 @@
new AnimatableClockController(
mView.findViewById(R.id.animatable_clock_view),
mStatusBarStateController,
- mBroadcastDispatcher);
+ mBroadcastDispatcher,
+ mBatteryController);
mClockViewController.init();
mLargeClockViewController =
new AnimatableClockController(
mView.findViewById(R.id.animatable_clock_view_large),
mStatusBarStateController,
- mBroadcastDispatcher);
+ mBroadcastDispatcher,
+ mBatteryController);
mLargeClockViewController.init();
// If a smartspace plugin is detected, replace the existing smartspace
diff --git a/packages/SystemUI/src/com/android/keyguard/TextAnimator.kt b/packages/SystemUI/src/com/android/keyguard/TextAnimator.kt
index 5735a4f..cdb39ef 100644
--- a/packages/SystemUI/src/com/android/keyguard/TextAnimator.kt
+++ b/packages/SystemUI/src/com/android/keyguard/TextAnimator.kt
@@ -65,7 +65,9 @@
invalidateCallback()
}
addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) = textInterpolator.rebase()
+ override fun onAnimationEnd(animation: Animator?) {
+ textInterpolator.rebase()
+ }
override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase()
})
}
@@ -74,6 +76,10 @@
textInterpolator.layout = layout
}
+ fun isRunning(): Boolean {
+ return animator.isRunning
+ }
+
fun draw(c: Canvas) = textInterpolator.draw(c)
/**
@@ -101,7 +107,9 @@
color: Int? = null,
animate: Boolean = true,
duration: Long = -1L,
- interpolator: TimeInterpolator? = null
+ interpolator: TimeInterpolator? = null,
+ delay: Long = 0,
+ onAnimationEnd: Runnable? = null
) {
if (animate) {
animator.cancel()
@@ -120,12 +128,25 @@
textInterpolator.onTargetPaintModified()
if (animate) {
+ animator.startDelay = delay
animator.duration = if (duration == -1L) {
DEFAULT_ANIMATION_DURATION
} else {
duration
}
interpolator?.let { animator.interpolator = it }
+ if (onAnimationEnd != null) {
+ val listener = object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ onAnimationEnd.run()
+ animator.removeListener(this)
+ }
+ override fun onAnimationCancel(animation: Animator?) {
+ animator.removeListener(this)
+ }
+ }
+ animator.addListener(listener)
+ }
animator.start()
} else {
// No animation is requested, thus set base and target state to the same state.
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 4e5502d..9017dd2 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -47,6 +47,7 @@
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
import com.android.systemui.statusbar.phone.NotificationIconContainer;
+import com.android.systemui.statusbar.policy.BatteryController;
import org.junit.Before;
import org.junit.Test;
@@ -96,6 +97,8 @@
private AnimatableClockView mClockView;
@Mock
private AnimatableClockView mLargeClockView;
+ @Mock
+ BatteryController mBatteryController;
private KeyguardClockSwitchController mController;
@@ -127,7 +130,8 @@
mBroadcastDispatcher,
mPluginManager,
mFeatureFlags,
- mExecutor);
+ mExecutor,
+ mBatteryController);
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
when(mColorExtractor.getColors(anyInt())).thenReturn(mGradientColors);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt
index 7b4f14d..ad7f0cb 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/TextAnimatorTest.kt
@@ -16,6 +16,7 @@
package com.android.keyguard
+import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.testing.AndroidTestingRunner
import android.text.Layout
@@ -25,7 +26,9 @@
import com.android.systemui.SysuiTestCase
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
import org.mockito.Mockito.`when`
+import org.mockito.Mockito.eq
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
@@ -107,4 +110,34 @@
// Then, animation start should not be called.
verify(valueAnimator, never()).start()
}
+
+ @Test
+ fun testAnimationEnded() {
+ val layout = makeLayout("Hello, World", PAINT)
+ val valueAnimator = mock(ValueAnimator::class.java)
+ val textInterpolator = mock(TextInterpolator::class.java)
+ val paint = mock(TextPaint::class.java)
+ `when`(textInterpolator.targetPaint).thenReturn(paint)
+ val animationEndCallback = mock(Runnable::class.java)
+
+ val textAnimator = TextAnimator(layout, {}).apply {
+ this.textInterpolator = textInterpolator
+ this.animator = valueAnimator
+ }
+
+ textAnimator.setTextStyle(
+ weight = 400,
+ animate = true,
+ onAnimationEnd = animationEndCallback
+ )
+
+ // Verify animationEnd callback has been added.
+ val captor = ArgumentCaptor.forClass(AnimatorListenerAdapter::class.java)
+ verify(valueAnimator).addListener(captor.capture())
+ captor.value.onAnimationEnd(valueAnimator)
+
+ // Verify animationEnd callback has been invoked and removed.
+ verify(animationEndCallback).run()
+ verify(valueAnimator).removeListener(eq(captor.value))
+ }
}