1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.keyguard
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.TimeInterpolator
22 import android.animation.ValueAnimator
23 import android.graphics.Canvas
24 import android.graphics.Typeface
25 import android.text.Layout
26 import android.util.SparseArray
27 
28 private const val TAG_WGHT = "wght"
29 private const val DEFAULT_ANIMATION_DURATION: Long = 300
30 
31 /**
32  * This class provides text animation between two styles.
33  *
34  * Currently this class can provide text style animation for text weight and text size. For example
35  * the simple view that draws text with animating text size is like as follows:
36  *
37  * <pre>
38  * <code>
39  *     class SimpleTextAnimation : View {
40  *         @JvmOverloads constructor(...)
41  *
42  *         private val layout: Layout = ... // Text layout, e.g. StaticLayout.
43  *
44  *         // TextAnimator tells us when needs to be invalidate.
45  *         private val animator = TextAnimator(layout) { invalidate() }
46  *
47  *         override fun onDraw(canvas: Canvas) = animator.draw(canvas)
48  *
49  *         // Change the text size with animation.
50  *         fun setTextSize(sizePx: Float, animate: Boolean) {
51  *             animator.setTextStyle(-1 /* unchanged weight */, sizePx, animate)
52  *         }
53  *     }
54  * </code>
55  * </pre>
56  */
57 class TextAnimator(
58     layout: Layout,
59     private val invalidateCallback: () -> Unit
60 ) {
61     // Following two members are for mutable for testing purposes.
62     internal var textInterpolator: TextInterpolator = TextInterpolator(layout)
63     internal var animator: ValueAnimator = ValueAnimator.ofFloat(1f).apply {
64         duration = DEFAULT_ANIMATION_DURATION
65         addUpdateListener {
66             textInterpolator.progress = it.animatedValue as Float
67             invalidateCallback()
68         }
69         addListener(object : AnimatorListenerAdapter() {
70             override fun onAnimationEnd(animation: Animator?) {
71                 textInterpolator.rebase()
72             }
73             override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase()
74         })
75     }
76 
77     private val typefaceCache = SparseArray<Typeface?>()
78 
79     fun updateLayout(layout: Layout) {
80         textInterpolator.layout = layout
81     }
82 
83     fun isRunning(): Boolean {
84         return animator.isRunning
85     }
86 
87     fun draw(c: Canvas) = textInterpolator.draw(c)
88 
89     /**
90      * Set text style with animation.
91      *
92      * By passing -1 to weight, the view preserve the current weight.
93      * By passing -1 to textSize, the view preserve the current text size.
94      * Bu passing -1 to duration, the default text animation, 1000ms, is used.
95      * By passing false to animate, the text will be updated without animation.
96      *
97      * @param weight an optional text weight.
98      * @param textSize an optional font size.
99      * @param colors an optional colors array that must be the same size as numLines passed to
100      *  the TextInterpolator
101      * @param animate an optional boolean indicating true for showing style transition as animation,
102      *                false for immediate style transition. True by default.
103      * @param duration an optional animation duration in milliseconds. This is ignored if animate is
104      *                 false.
105      * @param interpolator an optional time interpolator. If null is passed, last set interpolator
106      *                     will be used. This is ignored if animate is false.
107      */
108     fun setTextStyle(
109         weight: Int = -1,
110         textSize: Float = -1f,
111         color: Int? = null,
112         animate: Boolean = true,
113         duration: Long = -1L,
114         interpolator: TimeInterpolator? = null,
115         delay: Long = 0,
116         onAnimationEnd: Runnable? = null
117     ) {
118         if (animate) {
119             animator.cancel()
120             textInterpolator.rebase()
121         }
122 
123         if (textSize >= 0) {
124             textInterpolator.targetPaint.textSize = textSize
125         }
126         if (weight >= 0) {
127             // Paint#setFontVariationSettings creates Typeface instance from scratch. To reduce the
128             // memory impact, cache the typeface result.
129             textInterpolator.targetPaint.typeface = typefaceCache.getOrElse(weight) {
130                 textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight"
131                 textInterpolator.targetPaint.typeface
132             }
133         }
134         if (color != null) {
135             textInterpolator.targetPaint.color = color
136         }
137         textInterpolator.onTargetPaintModified()
138 
139         if (animate) {
140             animator.startDelay = delay
141             animator.duration = if (duration == -1L) {
142                 DEFAULT_ANIMATION_DURATION
143             } else {
144                 duration
145             }
146             interpolator?.let { animator.interpolator = it }
147             if (onAnimationEnd != null) {
148                 val listener = object : AnimatorListenerAdapter() {
149                     override fun onAnimationEnd(animation: Animator?) {
150                         onAnimationEnd.run()
151                         animator.removeListener(this)
152                     }
153                     override fun onAnimationCancel(animation: Animator?) {
154                         animator.removeListener(this)
155                     }
156                 }
157                 animator.addListener(listener)
158             }
159             animator.start()
160         } else {
161             // No animation is requested, thus set base and target state to the same state.
162             textInterpolator.progress = 1f
163             textInterpolator.rebase()
164         }
165     }
166 }
167 
168 private fun <V> SparseArray<V>.getOrElse(key: Int, defaultValue: () -> V): V {
169     var v = get(key)
170     if (v == null) {
171         v = defaultValue()
172         put(key, v)
173     }
174     return v
175 }