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:
Christian Göllner
2023-04-21 17:00:15 +02:00
parent a8d94a19c0
commit f5bc9b73cc
5 changed files with 94 additions and 141 deletions

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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(