Merge "Animate clock fonts with fewer discrete steps" into udc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
3c4b404c50
@@ -18,6 +18,7 @@ package com.android.systemui.animation
|
|||||||
|
|
||||||
import android.graphics.fonts.Font
|
import android.graphics.fonts.Font
|
||||||
import android.graphics.fonts.FontVariationAxis
|
import android.graphics.fonts.FontVariationAxis
|
||||||
|
import android.util.Log
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import android.util.MathUtils
|
import android.util.MathUtils
|
||||||
import android.util.MathUtils.abs
|
import android.util.MathUtils.abs
|
||||||
@@ -114,6 +115,9 @@ class FontInterpolator {
|
|||||||
tmpInterpKey.set(start, end, progress)
|
tmpInterpKey.set(start, end, progress)
|
||||||
val cachedFont = interpCache[tmpInterpKey]
|
val cachedFont = interpCache[tmpInterpKey]
|
||||||
if (cachedFont != null) {
|
if (cachedFont != null) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "[$progress] Interp. cache hit for $tmpInterpKey")
|
||||||
|
}
|
||||||
return cachedFont
|
return cachedFont
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +163,9 @@ class FontInterpolator {
|
|||||||
val axesCachedFont = verFontCache[tmpVarFontKey]
|
val axesCachedFont = verFontCache[tmpVarFontKey]
|
||||||
if (axesCachedFont != null) {
|
if (axesCachedFont != null) {
|
||||||
interpCache.put(InterpKey(start, end, progress), axesCachedFont)
|
interpCache.put(InterpKey(start, end, progress), axesCachedFont)
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "[$progress] Axis cache hit for $tmpVarFontKey")
|
||||||
|
}
|
||||||
return axesCachedFont
|
return axesCachedFont
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +175,9 @@ class FontInterpolator {
|
|||||||
val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build()
|
val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build()
|
||||||
interpCache.put(InterpKey(start, end, progress), newFont)
|
interpCache.put(InterpKey(start, end, progress), newFont)
|
||||||
verFontCache.put(VarFontKey(start, newAxes), newFont)
|
verFontCache.put(VarFontKey(start, newAxes), newFont)
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "[$progress] Cache MISS for $tmpInterpKey / $tmpVarFontKey")
|
||||||
|
}
|
||||||
return newFont
|
return newFont
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +243,8 @@ class FontInterpolator {
|
|||||||
(v.coerceIn(min, max) / step).toInt() * step
|
(v.coerceIn(min, max) / step).toInt() * step
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val LOG_TAG = "FontInterpolator"
|
||||||
|
private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG)
|
||||||
private val EMPTY_AXES = arrayOf<FontVariationAxis>()
|
private val EMPTY_AXES = arrayOf<FontVariationAxis>()
|
||||||
|
|
||||||
// Returns true if given two font instance can be interpolated.
|
// Returns true if given two font instance can be interpolated.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import android.graphics.fonts.Font
|
|||||||
import android.graphics.fonts.FontVariationAxis
|
import android.graphics.fonts.FontVariationAxis
|
||||||
import android.text.Layout
|
import android.text.Layout
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private const val DEFAULT_ANIMATION_DURATION: Long = 300
|
private const val DEFAULT_ANIMATION_DURATION: Long = 300
|
||||||
private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
|
private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
|
||||||
@@ -63,9 +64,9 @@ class TypefaceVariantCacheImpl(
|
|||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
return TypefaceVariantCache
|
return TypefaceVariantCache.createVariantTypeface(baseTypeface, fvar).also {
|
||||||
.createVariantTypeface(baseTypeface, fvar)
|
cache.put(fvar, it)
|
||||||
.also { cache.put(fvar, it) }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,6 @@ class TypefaceVariantCacheImpl(
|
|||||||
*
|
*
|
||||||
* Currently this class can provide text style animation for text weight and text size. For example
|
* Currently this class can provide text style animation for text weight and text size. For example
|
||||||
* the simple view that draws text with animating text size is like as follows:
|
* the simple view that draws text with animating text size is like as follows:
|
||||||
*
|
|
||||||
* <pre> <code>
|
* <pre> <code>
|
||||||
* ```
|
* ```
|
||||||
* class SimpleTextAnimation : View {
|
* class SimpleTextAnimation : View {
|
||||||
@@ -97,6 +97,7 @@ class TypefaceVariantCacheImpl(
|
|||||||
*/
|
*/
|
||||||
class TextAnimator(
|
class TextAnimator(
|
||||||
layout: Layout,
|
layout: Layout,
|
||||||
|
numberOfAnimationSteps: Int? = null, // Only do this number of discrete animation steps.
|
||||||
private val invalidateCallback: () -> Unit,
|
private val invalidateCallback: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var typefaceCache: TypefaceVariantCache = TypefaceVariantCacheImpl(layout.paint.typeface)
|
var typefaceCache: TypefaceVariantCache = TypefaceVariantCacheImpl(layout.paint.typeface)
|
||||||
@@ -112,7 +113,8 @@ class TextAnimator(
|
|||||||
ValueAnimator.ofFloat(1f).apply {
|
ValueAnimator.ofFloat(1f).apply {
|
||||||
duration = DEFAULT_ANIMATION_DURATION
|
duration = DEFAULT_ANIMATION_DURATION
|
||||||
addUpdateListener {
|
addUpdateListener {
|
||||||
textInterpolator.progress = it.animatedValue as Float
|
textInterpolator.progress =
|
||||||
|
calculateProgress(it.animatedValue as Float, numberOfAnimationSteps)
|
||||||
invalidateCallback()
|
invalidateCallback()
|
||||||
}
|
}
|
||||||
addListener(
|
addListener(
|
||||||
@@ -123,6 +125,17 @@ class TextAnimator(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun calculateProgress(animProgress: Float, numberOfAnimationSteps: Int?): Float {
|
||||||
|
if (numberOfAnimationSteps != null) {
|
||||||
|
// This clamps the progress to the nearest value of "numberOfAnimationSteps"
|
||||||
|
// discrete values between 0 and 1f.
|
||||||
|
return (animProgress * numberOfAnimationSteps).roundToInt() /
|
||||||
|
numberOfAnimationSteps.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
return animProgress
|
||||||
|
}
|
||||||
|
|
||||||
sealed class PositionedGlyph {
|
sealed class PositionedGlyph {
|
||||||
|
|
||||||
/** Mutable X coordinate of the glyph position relative from drawing offset. */
|
/** Mutable X coordinate of the glyph position relative from drawing offset. */
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ class AnimatableClockView @JvmOverloads constructor(
|
|||||||
private var onTextAnimatorInitialized: Runnable? = null
|
private var onTextAnimatorInitialized: Runnable? = null
|
||||||
|
|
||||||
@VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator =
|
@VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator =
|
||||||
{ layout, invalidateCb -> TextAnimator(layout, invalidateCb) }
|
{ layout, invalidateCb ->
|
||||||
|
TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb) }
|
||||||
@VisibleForTesting var isAnimationEnabled: Boolean = true
|
@VisibleForTesting var isAnimationEnabled: Boolean = true
|
||||||
@VisibleForTesting var timeOverrideInMillis: Long? = null
|
@VisibleForTesting var timeOverrideInMillis: Long? = null
|
||||||
|
|
||||||
@@ -567,6 +568,7 @@ class AnimatableClockView @JvmOverloads constructor(
|
|||||||
private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
|
private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
|
||||||
private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
|
private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
|
||||||
private const val COLOR_ANIM_DURATION: Long = 400
|
private const val COLOR_ANIM_DURATION: Long = 400
|
||||||
|
private const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30
|
||||||
|
|
||||||
// Constants for the animation
|
// Constants for the animation
|
||||||
private val MOVE_INTERPOLATOR = Interpolators.EMPHASIZED
|
private val MOVE_INTERPOLATOR = Interpolators.EMPHASIZED
|
||||||
|
|||||||
@@ -26,18 +26,17 @@ import android.text.TextPaint
|
|||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import com.android.systemui.SysuiTestCase
|
import com.android.systemui.SysuiTestCase
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlin.math.ceil
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.ArgumentCaptor
|
import org.mockito.ArgumentCaptor
|
||||||
import org.mockito.Mockito.`when`
|
|
||||||
import org.mockito.Mockito.eq
|
import org.mockito.Mockito.eq
|
||||||
import org.mockito.Mockito.inOrder
|
import org.mockito.Mockito.inOrder
|
||||||
import org.mockito.Mockito.mock
|
import org.mockito.Mockito.mock
|
||||||
import org.mockito.Mockito.never
|
import org.mockito.Mockito.never
|
||||||
import org.mockito.Mockito.times
|
import org.mockito.Mockito.times
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.`when`
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
@RunWith(AndroidTestingRunner::class)
|
@RunWith(AndroidTestingRunner::class)
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@@ -56,15 +55,13 @@ class TextAnimatorTest : SysuiTestCase() {
|
|||||||
val paint = mock(TextPaint::class.java)
|
val paint = mock(TextPaint::class.java)
|
||||||
`when`(textInterpolator.targetPaint).thenReturn(paint)
|
`when`(textInterpolator.targetPaint).thenReturn(paint)
|
||||||
|
|
||||||
val textAnimator = TextAnimator(layout, {}).apply {
|
val textAnimator =
|
||||||
|
TextAnimator(layout, null, {}).apply {
|
||||||
this.textInterpolator = textInterpolator
|
this.textInterpolator = textInterpolator
|
||||||
this.animator = valueAnimator
|
this.animator = valueAnimator
|
||||||
}
|
}
|
||||||
|
|
||||||
textAnimator.setTextStyle(
|
textAnimator.setTextStyle(weight = 400, animate = true)
|
||||||
weight = 400,
|
|
||||||
animate = true
|
|
||||||
)
|
|
||||||
|
|
||||||
// If animation is requested, the base state should be rebased and the target state should
|
// If animation is requested, the base state should be rebased and the target state should
|
||||||
// be updated.
|
// be updated.
|
||||||
@@ -88,15 +85,13 @@ class TextAnimatorTest : SysuiTestCase() {
|
|||||||
val paint = mock(TextPaint::class.java)
|
val paint = mock(TextPaint::class.java)
|
||||||
`when`(textInterpolator.targetPaint).thenReturn(paint)
|
`when`(textInterpolator.targetPaint).thenReturn(paint)
|
||||||
|
|
||||||
val textAnimator = TextAnimator(layout, {}).apply {
|
val textAnimator =
|
||||||
|
TextAnimator(layout, null, {}).apply {
|
||||||
this.textInterpolator = textInterpolator
|
this.textInterpolator = textInterpolator
|
||||||
this.animator = valueAnimator
|
this.animator = valueAnimator
|
||||||
}
|
}
|
||||||
|
|
||||||
textAnimator.setTextStyle(
|
textAnimator.setTextStyle(weight = 400, animate = false)
|
||||||
weight = 400,
|
|
||||||
animate = false
|
|
||||||
)
|
|
||||||
|
|
||||||
// If animation is not requested, the progress should be 1 which is end of animation and the
|
// If animation is not requested, the progress should be 1 which is end of animation and the
|
||||||
// base state is rebased to target state by calling rebase.
|
// base state is rebased to target state by calling rebase.
|
||||||
@@ -118,7 +113,8 @@ class TextAnimatorTest : SysuiTestCase() {
|
|||||||
`when`(textInterpolator.targetPaint).thenReturn(paint)
|
`when`(textInterpolator.targetPaint).thenReturn(paint)
|
||||||
val animationEndCallback = mock(Runnable::class.java)
|
val animationEndCallback = mock(Runnable::class.java)
|
||||||
|
|
||||||
val textAnimator = TextAnimator(layout, {}).apply {
|
val textAnimator =
|
||||||
|
TextAnimator(layout, null, {}).apply {
|
||||||
this.textInterpolator = textInterpolator
|
this.textInterpolator = textInterpolator
|
||||||
this.animator = valueAnimator
|
this.animator = valueAnimator
|
||||||
}
|
}
|
||||||
@@ -144,34 +140,27 @@ class TextAnimatorTest : SysuiTestCase() {
|
|||||||
val layout = makeLayout("Hello, World", PAINT)
|
val layout = makeLayout("Hello, World", PAINT)
|
||||||
val valueAnimator = mock(ValueAnimator::class.java)
|
val valueAnimator = mock(ValueAnimator::class.java)
|
||||||
val textInterpolator = mock(TextInterpolator::class.java)
|
val textInterpolator = mock(TextInterpolator::class.java)
|
||||||
val paint = TextPaint().apply {
|
val paint =
|
||||||
|
TextPaint().apply {
|
||||||
typeface = Typeface.createFromFile("/system/fonts/Roboto-Regular.ttf")
|
typeface = Typeface.createFromFile("/system/fonts/Roboto-Regular.ttf")
|
||||||
}
|
}
|
||||||
`when`(textInterpolator.targetPaint).thenReturn(paint)
|
`when`(textInterpolator.targetPaint).thenReturn(paint)
|
||||||
|
|
||||||
val textAnimator = TextAnimator(layout, {}).apply {
|
val textAnimator =
|
||||||
|
TextAnimator(layout, null, {}).apply {
|
||||||
this.textInterpolator = textInterpolator
|
this.textInterpolator = textInterpolator
|
||||||
this.animator = valueAnimator
|
this.animator = valueAnimator
|
||||||
}
|
}
|
||||||
|
|
||||||
textAnimator.setTextStyle(
|
textAnimator.setTextStyle(weight = 400, animate = true)
|
||||||
weight = 400,
|
|
||||||
animate = true
|
|
||||||
)
|
|
||||||
|
|
||||||
val prevTypeface = paint.typeface
|
val prevTypeface = paint.typeface
|
||||||
|
|
||||||
textAnimator.setTextStyle(
|
textAnimator.setTextStyle(weight = 700, animate = true)
|
||||||
weight = 700,
|
|
||||||
animate = true
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(paint.typeface).isNotSameInstanceAs(prevTypeface)
|
assertThat(paint.typeface).isNotSameInstanceAs(prevTypeface)
|
||||||
|
|
||||||
textAnimator.setTextStyle(
|
textAnimator.setTextStyle(weight = 400, animate = true)
|
||||||
weight = 400,
|
|
||||||
animate = true
|
|
||||||
)
|
|
||||||
|
|
||||||
assertThat(paint.typeface).isSameInstanceAs(prevTypeface)
|
assertThat(paint.typeface).isSameInstanceAs(prevTypeface)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user