Step clock animation: fix RTL issues and clean-up a little
- Leaves the bounds change animation be taken care of, by a
`ChangeBounds` transition. This takes care of the clock moving
correctly in RTL, and works well together with the burn-in animation
(offset + scale) that happens when going from/to AOD.
- Changes the step clock animation logic to only update glyph offsets,
instead of bounds + offsets.
- Removes the cancellation logic as it wasn't being used now, and was
making the code more complicated to read.
- Renames a few methods, adds documentation, and tries to simplify the
code a little bit.
Fixes: 274892412
Test: Manually - Going from/to AOD and posting/clearing notifications
on lockscreen.
Also took before/after videos and compared side by side.
Change-Id: I92f5c822a6b45ef5e4c5db680f9c795b3d8a5042
This commit is contained in:
@@ -22,12 +22,11 @@ import android.annotation.IntRange
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.text.Layout
|
||||
import android.text.TextUtils
|
||||
import android.text.format.DateFormat
|
||||
import android.util.AttributeSet
|
||||
import android.util.MathUtils
|
||||
import android.util.MathUtils.constrainedMap
|
||||
import android.widget.TextView
|
||||
import com.android.app.animation.Interpolators
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
@@ -40,8 +39,6 @@ import java.io.PrintWriter
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
|
||||
@@ -478,122 +475,56 @@ class AnimatableClockView @JvmOverloads constructor(
|
||||
pw.println(" time=$time")
|
||||
}
|
||||
|
||||
fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) {
|
||||
// Do we need to cancel an in-flight animation?
|
||||
// Need to also check against 0.0f here; we can sometimes get two calls with fraction == 0,
|
||||
// which trips up the check otherwise.
|
||||
if (lastSeenAnimationProgress != 1.0f &&
|
||||
lastSeenAnimationProgress != 0.0f &&
|
||||
fraction == 0.0f) {
|
||||
// New animation, but need to stop the old one. Figure out where each glyph currently
|
||||
// is in relation to the box position. After that, use the leading digit's current
|
||||
// position as the stop target.
|
||||
currentAnimationNeededStop = true
|
||||
private val moveToCenterDelays
|
||||
get() = if (isLayoutRtl) MOVE_LEFT_DELAYS else MOVE_RIGHT_DELAYS
|
||||
|
||||
// We assume that the current glyph offsets would be relative to the "from" position.
|
||||
val moveAmount = toRect.left - fromRect.left
|
||||
private val moveToSideDelays
|
||||
get() = if (isLayoutRtl) MOVE_RIGHT_DELAYS else MOVE_LEFT_DELAYS
|
||||
|
||||
// Remap the current glyph offsets to be relative to the new "end" position, and figure
|
||||
// out the start/end positions for the stop animation.
|
||||
for (i in 0 until NUM_DIGITS) {
|
||||
glyphOffsets[i] = -moveAmount + glyphOffsets[i]
|
||||
animationCancelStartPosition[i] = glyphOffsets[i]
|
||||
}
|
||||
|
||||
// Use the leading digit's offset as the stop position.
|
||||
if (toRect.left > fromRect.left) {
|
||||
// It _was_ moving left
|
||||
animationCancelStopPosition = glyphOffsets[0]
|
||||
} else {
|
||||
// It was moving right
|
||||
animationCancelStopPosition = glyphOffsets[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Is there a cancellation in progress?
|
||||
if (currentAnimationNeededStop && fraction < ANIMATION_CANCELLATION_TIME) {
|
||||
val animationStopProgress = MathUtils.constrainedMap(
|
||||
0.0f, 1.0f, 0.0f, ANIMATION_CANCELLATION_TIME, fraction
|
||||
)
|
||||
|
||||
// One of the digits has already stopped.
|
||||
val animationStopStep = 1.0f / (NUM_DIGITS - 1)
|
||||
|
||||
for (i in 0 until NUM_DIGITS) {
|
||||
val stopAmount = if (toRect.left > fromRect.left) {
|
||||
// It was moving left (before flipping)
|
||||
MOVE_LEFT_DELAYS[i] * animationStopStep
|
||||
} else {
|
||||
// It was moving right (before flipping)
|
||||
MOVE_RIGHT_DELAYS[i] * animationStopStep
|
||||
}
|
||||
|
||||
// Leading digit stops immediately.
|
||||
if (stopAmount == 0.0f) {
|
||||
glyphOffsets[i] = animationCancelStopPosition
|
||||
} else {
|
||||
val actualStopAmount = MathUtils.constrainedMap(
|
||||
0.0f, 1.0f, 0.0f, stopAmount, animationStopProgress
|
||||
/**
|
||||
* Offsets the glyphs of the clock for the step clock animation.
|
||||
*
|
||||
* The animation makes the glyphs of the clock move at different speeds, when the clock is
|
||||
* moving horizontally.
|
||||
*
|
||||
* @param clockStartLeft the [getLeft] position of the clock, before it started moving.
|
||||
* @param clockMoveDirection the direction in which it is moving. A positive number means right,
|
||||
* and negative means left.
|
||||
* @param moveFraction fraction of the clock movement. 0 means it is at the beginning, and 1
|
||||
* means it finished moving.
|
||||
*/
|
||||
fun offsetGlyphsForStepClockAnimation(
|
||||
clockStartLeft: Int,
|
||||
clockMoveDirection: Int,
|
||||
moveFraction: Float
|
||||
) {
|
||||
val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0
|
||||
val currentMoveAmount = left - clockStartLeft
|
||||
val digitOffsetDirection = if (isLayoutRtl) -1 else 1
|
||||
for (i in 0 until NUM_DIGITS) {
|
||||
// The delay for the digit, in terms of fraction (i.e. the digit should not move
|
||||
// during 0.0 - 0.1).
|
||||
val digitInitialDelay =
|
||||
if (isMovingToCenter) {
|
||||
moveToCenterDelays[i] * MOVE_DIGIT_STEP
|
||||
} else {
|
||||
moveToSideDelays[i] * MOVE_DIGIT_STEP
|
||||
}
|
||||
val digitFraction =
|
||||
MOVE_INTERPOLATOR.getInterpolation(
|
||||
constrainedMap(
|
||||
0.0f,
|
||||
1.0f,
|
||||
digitInitialDelay,
|
||||
digitInitialDelay + AVAILABLE_ANIMATION_TIME,
|
||||
moveFraction
|
||||
)
|
||||
)
|
||||
val easedProgress = MOVE_INTERPOLATOR.getInterpolation(actualStopAmount)
|
||||
val glyphMoveAmount =
|
||||
animationCancelStopPosition - animationCancelStartPosition[i]
|
||||
glyphOffsets[i] =
|
||||
animationCancelStartPosition[i] + glyphMoveAmount * easedProgress
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal part of the animation.
|
||||
// Do we need to remap the animation progress to take account of the cancellation?
|
||||
val actualFraction = if (currentAnimationNeededStop) {
|
||||
MathUtils.constrainedMap(
|
||||
0.0f, 1.0f, ANIMATION_CANCELLATION_TIME, 1.0f, fraction
|
||||
)
|
||||
} else {
|
||||
fraction
|
||||
}
|
||||
|
||||
val digitFractions = (0 until NUM_DIGITS).map {
|
||||
// The delay for each digit, in terms of fraction (i.e. the digit should not move
|
||||
// during 0.0 - 0.1).
|
||||
val initialDelay = if (toRect.left > fromRect.left) {
|
||||
MOVE_RIGHT_DELAYS[it] * MOVE_DIGIT_STEP
|
||||
} else {
|
||||
MOVE_LEFT_DELAYS[it] * MOVE_DIGIT_STEP
|
||||
}
|
||||
|
||||
val f = MathUtils.constrainedMap(
|
||||
0.0f, 1.0f,
|
||||
initialDelay, initialDelay + AVAILABLE_ANIMATION_TIME,
|
||||
actualFraction
|
||||
)
|
||||
MOVE_INTERPOLATOR.getInterpolation(max(min(f, 1.0f), 0.0f))
|
||||
}
|
||||
|
||||
// Was there an animation halt?
|
||||
val moveAmount = if (currentAnimationNeededStop) {
|
||||
// Only need to animate over the remaining space if the animation was aborted.
|
||||
-animationCancelStopPosition
|
||||
} else {
|
||||
toRect.left.toFloat() - fromRect.left.toFloat()
|
||||
}
|
||||
|
||||
for (i in 0 until NUM_DIGITS) {
|
||||
glyphOffsets[i] = -moveAmount + (moveAmount * digitFractions[i])
|
||||
}
|
||||
val moveAmountForDigit = currentMoveAmount * digitFraction
|
||||
val moveAmountDeltaForDigit = moveAmountForDigit - currentMoveAmount
|
||||
glyphOffsets[i] = digitOffsetDirection * moveAmountDeltaForDigit
|
||||
}
|
||||
|
||||
invalidate()
|
||||
|
||||
if (fraction == 1.0f) {
|
||||
// Reset
|
||||
currentAnimationNeededStop = false
|
||||
}
|
||||
|
||||
lastSeenAnimationProgress = fraction
|
||||
|
||||
// Ensure that the actual clock container is always in the "end" position.
|
||||
this.setLeftTopRightBottom(toRect.left, toRect.top, toRect.right, toRect.bottom)
|
||||
}
|
||||
|
||||
// DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
|
||||
@@ -647,9 +578,6 @@ class AnimatableClockView @JvmOverloads constructor(
|
||||
private const val NUM_DIGITS = 4
|
||||
private const val DIGITS_PER_LINE = 2
|
||||
|
||||
// How much of "fraction" to spend on canceling the animation, if needed
|
||||
private const val ANIMATION_CANCELLATION_TIME = 0f
|
||||
|
||||
// Delays. Each digit's animation should have a slight delay, so we get a nice
|
||||
// "stepping" effect. When moving right, the second digit of the hour should move first.
|
||||
// When moving left, the first digit of the hour should move first. The lists encode
|
||||
@@ -668,6 +596,6 @@ class AnimatableClockView @JvmOverloads constructor(
|
||||
|
||||
// Total available transition time for each digit, taking into account the step. If step is
|
||||
// 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7.
|
||||
private val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1)
|
||||
private const val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,8 +189,9 @@ class DefaultClockController(
|
||||
view.setLayoutParams(lp)
|
||||
}
|
||||
|
||||
fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) {
|
||||
view.moveForSplitShade(fromRect, toRect, fraction)
|
||||
/** See documentation at [AnimatableClockView.offsetGlyphsForStepClockAnimation]. */
|
||||
fun offsetGlyphsForStepClockAnimation(fromLeft: Int, direction: Int, fraction: Float) {
|
||||
view.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,8 +278,8 @@ class DefaultClockController(
|
||||
dozeFraction: Float,
|
||||
foldFraction: Float,
|
||||
) : DefaultClockAnimations(view, dozeFraction, foldFraction) {
|
||||
override fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {
|
||||
largeClock.moveForSplitShade(fromRect, toRect, fraction)
|
||||
override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
|
||||
largeClock.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -141,8 +141,16 @@ interface ClockAnimations {
|
||||
/** Runs the battery animation (if any). */
|
||||
fun charge() {}
|
||||
|
||||
/** Move the clock, for example, if the notification tray appears in split-shade mode. */
|
||||
fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {}
|
||||
/**
|
||||
* Runs when the clock's position changed during the move animation.
|
||||
*
|
||||
* @param fromLeft the [View.getLeft] position of the clock, before it started moving.
|
||||
* @param direction the direction in which it is moving. A positive number means right, and
|
||||
* negative means left.
|
||||
* @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
|
||||
* it finished moving.
|
||||
*/
|
||||
fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
|
||||
|
||||
/**
|
||||
* Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview,
|
||||
|
||||
@@ -363,8 +363,6 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
|
||||
} else {
|
||||
View clockView = clockContainerView.getChildAt(0);
|
||||
|
||||
transition.excludeTarget(clockView, /* exclude= */ true);
|
||||
|
||||
TransitionSet set = new TransitionSet();
|
||||
set.addTransition(transition);
|
||||
|
||||
@@ -389,8 +387,9 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
|
||||
|
||||
@VisibleForTesting
|
||||
static class SplitShadeTransitionAdapter extends Transition {
|
||||
private static final String PROP_BOUNDS = "splitShadeTransitionAdapter:bounds";
|
||||
private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS };
|
||||
private static final String PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft";
|
||||
private static final String PROP_X_IN_WINDOW = "splitShadeTransitionAdapter:xInWindow";
|
||||
private static final String[] TRANSITION_PROPERTIES = { PROP_BOUNDS_LEFT, PROP_X_IN_WINDOW};
|
||||
|
||||
private final KeyguardClockSwitchController mController;
|
||||
|
||||
@@ -400,12 +399,10 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
|
||||
}
|
||||
|
||||
private void captureValues(TransitionValues transitionValues) {
|
||||
Rect boundsRect = new Rect();
|
||||
boundsRect.left = transitionValues.view.getLeft();
|
||||
boundsRect.top = transitionValues.view.getTop();
|
||||
boundsRect.right = transitionValues.view.getRight();
|
||||
boundsRect.bottom = transitionValues.view.getBottom();
|
||||
transitionValues.values.put(PROP_BOUNDS, boundsRect);
|
||||
transitionValues.values.put(PROP_BOUNDS_LEFT, transitionValues.view.getLeft());
|
||||
int[] locationInWindowTmp = new int[2];
|
||||
transitionValues.view.getLocationInWindow(locationInWindowTmp);
|
||||
transitionValues.values.put(PROP_X_IN_WINDOW, locationInWindowTmp[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -427,8 +424,12 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
|
||||
}
|
||||
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
|
||||
|
||||
Rect from = (Rect) startValues.values.get(PROP_BOUNDS);
|
||||
Rect to = (Rect) endValues.values.get(PROP_BOUNDS);
|
||||
int fromLeft = (int) startValues.values.get(PROP_BOUNDS_LEFT);
|
||||
int fromWindowX = (int) startValues.values.get(PROP_X_IN_WINDOW);
|
||||
int toWindowX = (int) endValues.values.get(PROP_X_IN_WINDOW);
|
||||
// Using windowX, to determine direction, instead of left, as in RTL the difference of
|
||||
// toLeft - fromLeft is always positive, even when moving left.
|
||||
int direction = toWindowX - fromWindowX > 0 ? 1 : -1;
|
||||
|
||||
anim.addUpdateListener(animation -> {
|
||||
ClockController clock = mController.getClock();
|
||||
@@ -437,7 +438,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
|
||||
}
|
||||
|
||||
clock.getLargeClock().getAnimations()
|
||||
.onPositionUpdated(from, to, animation.getAnimatedFraction());
|
||||
.onPositionUpdated(fromLeft, direction, animation.getAnimatedFraction());
|
||||
});
|
||||
|
||||
return anim;
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.android.keyguard
|
||||
import android.animation.Animator
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.transition.TransitionValues
|
||||
import android.view.View
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.keyguard.KeyguardStatusViewController.SplitShadeTransitionAdapter
|
||||
import com.android.systemui.SysuiTestCase
|
||||
@@ -44,14 +45,16 @@ class SplitShadeTransitionAdapterTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun createAnimator_nullStartValues_returnsNull() {
|
||||
val animator = adapter.createAnimator(startValues = null, endValues = TransitionValues())
|
||||
val endValues = createEndValues()
|
||||
|
||||
val animator = adapter.createAnimator(startValues = null, endValues = endValues)
|
||||
|
||||
assertThat(animator).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createAnimator_nullEndValues_returnsNull() {
|
||||
val animator = adapter.createAnimator(startValues = TransitionValues(), endValues = null)
|
||||
val animator = adapter.createAnimator(startValues = createStartValues(), endValues = null)
|
||||
|
||||
assertThat(animator).isNull()
|
||||
}
|
||||
@@ -59,10 +62,22 @@ class SplitShadeTransitionAdapterTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun createAnimator_nonNullStartAndEndValues_returnsAnimator() {
|
||||
val animator =
|
||||
adapter.createAnimator(startValues = TransitionValues(), endValues = TransitionValues())
|
||||
adapter.createAnimator(startValues = createStartValues(), endValues = createEndValues())
|
||||
|
||||
assertThat(animator).isNotNull()
|
||||
}
|
||||
|
||||
private fun createStartValues() =
|
||||
TransitionValues().also { values ->
|
||||
values.view = View(context)
|
||||
adapter.captureStartValues(values)
|
||||
}
|
||||
|
||||
private fun createEndValues() =
|
||||
TransitionValues().also { values ->
|
||||
values.view = View(context)
|
||||
adapter.captureEndValues(values)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SplitShadeTransitionAdapter.createAnimator(
|
||||
|
||||
Reference in New Issue
Block a user