Merge "Port stepping animation to FlexClockView" into main
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index a4782ac..ee21ea6 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -55,10 +55,7 @@
     override val view: View
         get() = layerController.view
 
-    override val config =
-        ClockFaceConfig(
-            hasCustomPositionUpdatedAnimation = false // TODO(b/364673982)
-        )
+    override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true)
 
     override var theme = ThemeConfig(true, assets.seedColor)
 
@@ -96,6 +93,19 @@
         layerController.view.layoutParams = lp
     }
 
+    /** See documentation at [FlexClockView.offsetGlyphsForStepClockAnimation]. */
+    private fun offsetGlyphsForStepClockAnimation(
+        clockStartLeft: Int,
+        direction: Int,
+        fraction: Float
+    ) {
+        (view as? FlexClockView)?.offsetGlyphsForStepClockAnimation(
+            clockStartLeft,
+            direction,
+            fraction,
+        )
+    }
+
     override val layout: ClockFaceLayout =
         DefaultClockFaceLayout(view).apply {
             views[0].id =
@@ -248,10 +258,12 @@
 
             override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
                 layerController.animations.onPositionUpdated(fromLeft, direction, fraction)
+                if (isLargeClock) offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
             }
 
             override fun onPositionUpdated(distance: Float, fraction: Float) {
                 layerController.animations.onPositionUpdated(distance, fraction)
+                // TODO(b/378128811) port stepping animation
             }
         }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index d86c0d6..593eba9 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Point
+import android.util.MathUtils.constrainedMap
 import android.view.View
 import android.view.ViewGroup
 import android.widget.RelativeLayout
@@ -50,6 +51,8 @@
             )
     }
 
+    private val digitOffsets = mutableMapOf<Int, Float>()
+
     override fun addView(child: View?) {
         super.addView(child)
         (child as SimpleDigitalClockTextView).digitTranslateAnimator =
@@ -76,7 +79,7 @@
         digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0)
         digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y)
         digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitSize)
-        digitLeftTopMap.forEach { _, point ->
+        digitLeftTopMap.forEach { (_, point) ->
             point.x += abs(aodTranslate.x)
             point.y += abs(aodTranslate.y)
         }
@@ -89,11 +92,17 @@
 
     override fun onDraw(canvas: Canvas) {
         super.onDraw(canvas)
-        digitalClockTextViewMap.forEach { (id, _) ->
-            val textView = digitalClockTextViewMap[id]!!
-            canvas.translate(digitLeftTopMap[id]!!.x.toFloat(), digitLeftTopMap[id]!!.y.toFloat())
+        digitalClockTextViewMap.forEach { (id, textView) ->
+            // save canvas location in anticipation of restoration later
+            canvas.save()
+            val xTranslateAmount =
+                digitOffsets.getOrDefault(id, 0f) + digitLeftTopMap[id]!!.x.toFloat()
+            // move canvas to location that the textView would like
+            canvas.translate(xTranslateAmount, digitLeftTopMap[id]!!.y.toFloat())
+            // draw the textView at the location of the canvas above
             textView.draw(canvas)
-            canvas.translate(-digitLeftTopMap[id]!!.x.toFloat(), -digitLeftTopMap[id]!!.y.toFloat())
+            // reset the canvas location back to 0 without drawing
+            canvas.restore()
         }
     }
 
@@ -157,10 +166,108 @@
         }
     }
 
+    /**
+     * Offsets the textViews of the clock for the step clock animation.
+     *
+     * The animation makes the textViews 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
+        // The sign of moveAmountDeltaForDigit is already set here
+        // we can interpret (left - clockStartLeft) as (destinationPosition - originPosition)
+        // so we no longer need to multiply direct sign to moveAmountDeltaForDigit
+        val currentMoveAmount = left - clockStartLeft
+        for (i in 0 until NUM_DIGITS) {
+            val mapIndexToId =
+                when (i) {
+                    0 -> R.id.HOUR_FIRST_DIGIT
+                    1 -> R.id.HOUR_SECOND_DIGIT
+                    2 -> R.id.MINUTE_FIRST_DIGIT
+                    3 -> R.id.MINUTE_SECOND_DIGIT
+                    else -> -1
+                }
+            val digitFraction =
+                getDigitFraction(
+                    digit = i,
+                    isMovingToCenter = isMovingToCenter,
+                    fraction = moveFraction,
+                )
+            // left here is the final left position after the animation is done
+            val moveAmountForDigit = currentMoveAmount * digitFraction
+            var moveAmountDeltaForDigit = moveAmountForDigit - currentMoveAmount
+            if (isMovingToCenter && moveAmountForDigit < 0) moveAmountDeltaForDigit *= -1
+            digitOffsets[mapIndexToId] = moveAmountDeltaForDigit
+            invalidate()
+        }
+    }
+
+    private val moveToCenterDelays: List<Int>
+        get() = if (isLayoutRtl) MOVE_LEFT_DELAYS else MOVE_RIGHT_DELAYS
+
+    private val moveToSideDelays: List<Int>
+        get() = if (isLayoutRtl) MOVE_RIGHT_DELAYS else MOVE_LEFT_DELAYS
+
+    private fun getDigitFraction(digit: Int, isMovingToCenter: Boolean, fraction: Float): Float {
+        // The delay for the digit, in terms of fraction.
+        // (i.e. the digit should not move during 0.0 - 0.1).
+        val delays = if (isMovingToCenter) moveToCenterDelays else moveToSideDelays
+        val digitInitialDelay = delays[digit] * MOVE_DIGIT_STEP
+        return MOVE_INTERPOLATOR.getInterpolation(
+            constrainedMap(
+                /* rangeMin= */ 0.0f,
+                /* rangeMax= */ 1.0f,
+                /* valueMin= */ digitInitialDelay,
+                /* valueMax= */ digitInitialDelay + AVAILABLE_ANIMATION_TIME,
+                /* value= */ fraction,
+            )
+        )
+    }
+
     companion object {
         val AOD_TRANSITION_DURATION = 750L
         val CHARGING_TRANSITION_DURATION = 300L
 
+        // Calculate the positions of all of the digits...
+        // Offset each digit by, say, 0.1
+        // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should
+        // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3
+        // from 0.3 - 1.0.
+        private const val NUM_DIGITS = 4
+
+        // 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
+        // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied
+        // by delayMultiplier.
+        private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3)
+        private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2)
+
+        // How much delay to apply to each subsequent digit. This is measured in terms of "fraction"
+        // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc
+        // before moving).
+        //
+        // The current specs dictate that each digit should have a 33ms gap between them. The
+        // overall time is 1s right now.
+        private const val MOVE_DIGIT_STEP = 0.033f
+
+        // Constants for the animation
+        private val MOVE_INTERPOLATOR = Interpolators.EMPHASIZED
+
+        // 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 const val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1)
+
         // Use the sign of targetTranslation to control the direction of digit translation
         fun updateDirectionalTargetTranslate(id: Int, targetTranslation: Point): Point {
             val outPoint = Point(targetTranslation)
@@ -169,17 +276,14 @@
                     outPoint.x *= -1
                     outPoint.y *= -1
                 }
-
                 R.id.HOUR_SECOND_DIGIT -> {
                     outPoint.x *= 1
                     outPoint.y *= -1
                 }
-
                 R.id.MINUTE_FIRST_DIGIT -> {
                     outPoint.x *= -1
                     outPoint.y *= 1
                 }
-
                 R.id.MINUTE_SECOND_DIGIT -> {
                     outPoint.x *= 1
                     outPoint.y *= 1