Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 27988dee authored by Jamie Garside's avatar Jamie Garside Committed by Android (Google) Code Review
Browse files

Merge "Add large screen clock motion - method 2."

parents 94cc29a1 82cec883
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -82,6 +82,14 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) {
        /** Mutable Y coordinate of the glyph position relative from the baseline. */
        var y: Float = 0f

        /**
         * The current line of text being drawn, in a multi-line TextView.
         */
        var lineNo: Int = 0

        /**
         * Mutable text size of the glyph in pixels.
         */
        /** Mutable text size of the glyph in pixels. */
        var textSize: Float = 0f

+6 −2
Original line number Diff line number Diff line
@@ -231,7 +231,9 @@ class TextInterpolator(layout: Layout) {
                    val origin = layout.getDrawOrigin(lineNo)
                    canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat())

                    run.fontRuns.forEach { fontRun -> drawFontRun(canvas, run, fontRun, tmpPaint) }
                    run.fontRuns.forEach { fontRun ->
                        drawFontRun(canvas, run, fontRun, lineNo, tmpPaint)
                    }
                } finally {
                    canvas.restore()
                }
@@ -341,7 +343,7 @@ class TextInterpolator(layout: Layout) {
    var glyphFilter: GlyphCallback? = null

    // Draws single font run.
    private fun drawFontRun(c: Canvas, line: Run, run: FontRun, paint: Paint) {
    private fun drawFontRun(c: Canvas, line: Run, run: FontRun, lineNo: Int, paint: Paint) {
        var arrayIndex = 0
        val font = fontInterpolator.lerp(run.baseFont, run.targetFont, progress)

@@ -360,11 +362,13 @@ class TextInterpolator(layout: Layout) {
        tmpGlyph.font = font
        tmpGlyph.runStart = run.start
        tmpGlyph.runLength = run.end - run.start
        tmpGlyph.lineNo = lineNo

        tmpPaintForGlyph.set(paint)
        var prevStart = run.start

        for (i in run.start until run.end) {
            tmpGlyph.glyphIndex = i
            tmpGlyph.glyphId = line.glyphIds[i]
            tmpGlyph.x = MathUtils.lerp(line.baseX[i], line.targetX[i], progress)
            tmpGlyph.y = MathUtils.lerp(line.baseY[i], line.targetY[i], progress)
+12 −0
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@
package com.android.systemui.plugins

import android.content.res.Resources
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import com.android.systemui.plugins.annotations.ProvidesInterface
@@ -114,6 +115,17 @@ 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) { }

    /**
     * Whether this clock has a custom position update animation. If true, the keyguard will call
     * `onPositionUpdated` to notify the clock of a position update animation. If false, a default
     * animation will be used (e.g. a simple translation).
     */
    val hasCustomPositionUpdatedAnimation
        get() = false
}

/** Events that have specific data about the related face */
+1 −0
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="@dimen/keyguard_large_clock_top_margin"
        android:clipChildren="false"
        android:visibility="gone" />

    <!-- Not quite optimal but needed to translate these items as a group. The
+171 −4
Original line number Diff line number Diff line
@@ -20,16 +20,15 @@ import android.annotation.ColorInt
import android.annotation.FloatRange
import android.annotation.IntRange
import android.annotation.SuppressLint
import android.app.compat.ChangeIdStateCache.invalidate
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.widget.TextView
import com.android.internal.R.attr.contentDescription
import com.android.internal.R.attr.format
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.animation.GlyphCallback
import com.android.systemui.animation.Interpolators
@@ -39,6 +38,8 @@ 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)
@@ -311,7 +312,24 @@ class AnimatableClockView @JvmOverloads constructor(
        )
    }

    private val glyphFilter: GlyphCallback? = null // Add text animation tweak here.
    // The offset of each glyph from where it should be.
    private var glyphOffsets = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f)

    private var lastSeenAnimationProgress = 1.0f

    // If the animation is being reversed, the target offset for each glyph for the "stop".
    private var animationCancelStartPosition = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f)
    private var animationCancelStopPosition = 0.0f

    // Whether the currently playing animation needed a stop (and thus, is shortened).
    private var currentAnimationNeededStop = false

    private val glyphFilter: GlyphCallback = { positionedGlyph, _ ->
        val offset = positionedGlyph.lineNo * DIGITS_PER_LINE + positionedGlyph.glyphIndex
        if (offset < glyphOffsets.size) {
            positionedGlyph.x += glyphOffsets[offset]
        }
    }

    /**
     * Set text style with an optional animation.
@@ -421,6 +439,124 @@ 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

            // We assume that the current glyph offsets would be relative to the "from" position.
            val moveAmount = toRect.left - fromRect.left

            // 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
                    )
                    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])
            }
        }

        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.
    // This is an optimization to ensure we only recompute the patterns when the inputs change.
    private object Patterns {
@@ -458,5 +594,36 @@ class AnimatableClockView @JvmOverloads constructor(
        private const val APPEAR_ANIM_DURATION: Long = 350
        private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
        private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000

        // Constants for the animation
        private val MOVE_INTERPOLATOR = Interpolators.STANDARD

        // 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
        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 = 0.4f

        // 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).
        private const val MOVE_DIGIT_STEP = 0.1f

        // 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)
    }
}
Loading