Loading packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +5 −0 Original line number Diff line number Diff line Loading @@ -88,6 +88,11 @@ class TextAnimator( */ 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. */ Loading packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +4 −2 Original line number Diff line number Diff line Loading @@ -244,7 +244,7 @@ class TextInterpolator( canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat()) run.fontRuns.forEach { fontRun -> drawFontRun(canvas, run, fontRun, tmpPaint) drawFontRun(canvas, run, fontRun, lineNo, tmpPaint) } } finally { canvas.restore() Loading Loading @@ -349,7 +349,7 @@ class TextInterpolator( 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) Loading @@ -368,11 +368,13 @@ class TextInterpolator( 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) Loading packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +12 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 */ Loading packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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 Loading packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +171 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading Loading @@ -316,7 +317,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. Loading Loading @@ -432,6 +450,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 { Loading Loading @@ -469,5 +605,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
packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +5 −0 Original line number Diff line number Diff line Loading @@ -88,6 +88,11 @@ class TextAnimator( */ 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. */ Loading
packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +4 −2 Original line number Diff line number Diff line Loading @@ -244,7 +244,7 @@ class TextInterpolator( canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat()) run.fontRuns.forEach { fontRun -> drawFontRun(canvas, run, fontRun, tmpPaint) drawFontRun(canvas, run, fontRun, lineNo, tmpPaint) } } finally { canvas.restore() Loading Loading @@ -349,7 +349,7 @@ class TextInterpolator( 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) Loading @@ -368,11 +368,13 @@ class TextInterpolator( 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) Loading
packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +12 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 */ Loading
packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +171 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading Loading @@ -316,7 +317,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. Loading Loading @@ -432,6 +450,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 { Loading Loading @@ -469,5 +605,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) } }