• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.systemui.animation
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.graphics.fonts.Font
26 import android.text.Layout
27 import android.util.LruCache
28 
29 private const val DEFAULT_ANIMATION_DURATION: Long = 300
30 private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
31 
32 typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit
33 /**
34  * This class provides text animation between two styles.
35  *
36  * Currently this class can provide text style animation for text weight and text size. For example
37  * the simple view that draws text with animating text size is like as follows:
38  *
39  * <pre> <code>
40  * ```
41  *     class SimpleTextAnimation : View {
42  *         @JvmOverloads constructor(...)
43  *
44  *         private val layout: Layout = ... // Text layout, e.g. StaticLayout.
45  *
46  *         // TextAnimator tells us when needs to be invalidate.
47  *         private val animator = TextAnimator(layout) { invalidate() }
48  *
49  *         override fun onDraw(canvas: Canvas) = animator.draw(canvas)
50  *
51  *         // Change the text size with animation.
52  *         fun setTextSize(sizePx: Float, animate: Boolean) {
53  *             animator.setTextStyle("" /* unchanged fvar... */, sizePx, animate)
54  *         }
55  *     }
56  * ```
57  * </code> </pre>
58  */
59 class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) {
60     // Following two members are for mutable for testing purposes.
61     public var textInterpolator: TextInterpolator = TextInterpolator(layout)
62     public var animator: ValueAnimator =
<lambda>null63         ValueAnimator.ofFloat(1f).apply {
64             duration = DEFAULT_ANIMATION_DURATION
65             addUpdateListener {
66                 textInterpolator.progress = it.animatedValue as Float
67                 invalidateCallback()
68             }
69             addListener(
70                 object : AnimatorListenerAdapter() {
71                     override fun onAnimationEnd(animation: Animator?) {
72                         textInterpolator.rebase()
73                     }
74                     override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase()
75                 }
76             )
77         }
78 
79     sealed class PositionedGlyph {
80 
81         /** Mutable X coordinate of the glyph position relative from drawing offset. */
82         var x: Float = 0f
83 
84         /** Mutable Y coordinate of the glyph position relative from the baseline. */
85         var y: Float = 0f
86 
87         /** The current line of text being drawn, in a multi-line TextView. */
88         var lineNo: Int = 0
89 
90         /** Mutable text size of the glyph in pixels. */
91         var textSize: Float = 0f
92 
93         /** Mutable color of the glyph. */
94         var color: Int = 0
95 
96         /** Immutable character offset in the text that the current font run start. */
97         abstract var runStart: Int
98             protected set
99 
100         /** Immutable run length of the font run. */
101         abstract var runLength: Int
102             protected set
103 
104         /** Immutable glyph index of the font run. */
105         abstract var glyphIndex: Int
106             protected set
107 
108         /** Immutable font instance for this font run. */
109         abstract var font: Font
110             protected set
111 
112         /** Immutable glyph ID for this glyph. */
113         abstract var glyphId: Int
114             protected set
115     }
116 
117     private val fontVariationUtils = FontVariationUtils()
118 
119     private val typefaceCache = LruCache<String, Typeface>(TYPEFACE_CACHE_MAX_ENTRIES)
120 
updateLayoutnull121     fun updateLayout(layout: Layout) {
122         textInterpolator.layout = layout
123     }
124 
isRunningnull125     fun isRunning(): Boolean {
126         return animator.isRunning
127     }
128 
129     /**
130      * GlyphFilter applied just before drawing to canvas for tweaking positions and text size.
131      *
132      * This callback is called for each glyphs just before drawing the glyphs. This function will be
133      * called with the intrinsic position, size, color, glyph ID and font instance. You can mutate
134      * the position, size and color for tweaking animations. Do not keep the reference of passed
135      * glyph object. The interpolator reuses that object for avoiding object allocations.
136      *
137      * Details: The text is drawn with font run units. The font run is a text segment that draws
138      * with the same font. The {@code runStart} and {@code runLimit} is a range of the font run in
139      * the text that current glyph is in. Once the font run is determined, the system will convert
140      * characters into glyph IDs. The {@code glyphId} is the glyph identifier in the font and {@code
141      * glyphIndex} is the offset of the converted glyph array. Please note that the {@code
142      * glyphIndex} is not a character index, because the character will not be converted to glyph
143      * one-by-one. If there are ligatures including emoji sequence, etc, the glyph ID may be
144      * composed from multiple characters.
145      *
146      * Here is an example of font runs: "fin. 終わり"
147      *
148      * Characters :    f      i      n      .      _      終     わ     り
149      * Code Points: \u0066 \u0069 \u006E \u002E \u0020 \u7D42 \u308F \u308A
150      * Font Runs  : <-- Roboto-Regular.ttf          --><-- NotoSans-CJK.otf -->
151      *                  runStart = 0, runLength = 5        runStart = 5, runLength = 3
152      * Glyph IDs  :      194        48     7      8     4367   1039   1002
153      * Glyph Index:       0          1     2      3       0      1      2
154      *
155      * In this example, the "fi" is converted into ligature form, thus the single glyph ID is
156      * assigned for two characters, f and i.
157      *
158      * Example:
159      * ```
160      * private val glyphFilter: GlyphCallback = { glyph, progress ->
161      *     val index = glyph.runStart
162      *     val i = glyph.glyphIndex
163      *     val moveAmount = 1.3f
164      *     val sign = (-1 + 2 * ((i + index) % 2))
165      *     val turnProgress = if (progress < .5f) progress / 0.5f else (1.0f - progress) / 0.5f
166      *
167      *     // You can modify (x, y) coordinates, textSize and color during animation.
168      *     glyph.textSize += glyph.textSize * sign * moveAmount * turnProgress
169      *     glyph.y += glyph.y * sign * moveAmount * turnProgress
170      *     glyph.x += glyph.x * sign * moveAmount * turnProgress
171      * }
172      * ```
173      */
174     var glyphFilter: GlyphCallback?
175         get() = textInterpolator.glyphFilter
176         set(value) {
177             textInterpolator.glyphFilter = value
178         }
179 
drawnull180     fun draw(c: Canvas) = textInterpolator.draw(c)
181 
182     /**
183      * Set text style with animation.
184      *
185      * By passing -1 to weight, the view preserve the current weight.
186      * By passing -1 to textSize, the view preserve the current text size.
187      * Bu passing -1 to duration, the default text animation, 1000ms, is used.
188      * By passing false to animate, the text will be updated without animation.
189      *
190      * @param fvar an optional text fontVariationSettings.
191      * @param textSize an optional font size.
192      * @param colors an optional colors array that must be the same size as numLines passed to
193      *               the TextInterpolator
194      * @param strokeWidth an optional paint stroke width
195      * @param animate an optional boolean indicating true for showing style transition as animation,
196      *                false for immediate style transition. True by default.
197      * @param duration an optional animation duration in milliseconds. This is ignored if animate is
198      *                 false.
199      * @param interpolator an optional time interpolator. If null is passed, last set interpolator
200      *                     will be used. This is ignored if animate is false.
201      */
202     fun setTextStyle(
203         fvar: String? = "",
204         textSize: Float = -1f,
205         color: Int? = null,
206         strokeWidth: Float = -1f,
207         animate: Boolean = true,
208         duration: Long = -1L,
209         interpolator: TimeInterpolator? = null,
210         delay: Long = 0,
211         onAnimationEnd: Runnable? = null
212     ) {
213         if (animate) {
214             animator.cancel()
215             textInterpolator.rebase()
216         }
217 
218         if (textSize >= 0) {
219             textInterpolator.targetPaint.textSize = textSize
220         }
221 
222         if (!fvar.isNullOrBlank()) {
223             textInterpolator.targetPaint.typeface = typefaceCache.get(fvar) ?: run {
224                 textInterpolator.targetPaint.fontVariationSettings = fvar
225                 textInterpolator.targetPaint.typeface?.also {
226                     typefaceCache.put(fvar, textInterpolator.targetPaint.typeface)
227                 }
228             }
229         }
230 
231         if (color != null) {
232             textInterpolator.targetPaint.color = color
233         }
234         if (strokeWidth >= 0F) {
235             textInterpolator.targetPaint.strokeWidth = strokeWidth
236         }
237         textInterpolator.onTargetPaintModified()
238 
239         if (animate) {
240             animator.startDelay = delay
241             animator.duration =
242                 if (duration == -1L) {
243                     DEFAULT_ANIMATION_DURATION
244                 } else {
245                     duration
246                 }
247             interpolator?.let { animator.interpolator = it }
248             if (onAnimationEnd != null) {
249                 val listener =
250                     object : AnimatorListenerAdapter() {
251                         override fun onAnimationEnd(animation: Animator?) {
252                             onAnimationEnd.run()
253                             animator.removeListener(this)
254                         }
255                         override fun onAnimationCancel(animation: Animator?) {
256                             animator.removeListener(this)
257                         }
258                     }
259                 animator.addListener(listener)
260             }
261             animator.start()
262         } else {
263             // No animation is requested, thus set base and target state to the same state.
264             textInterpolator.progress = 1f
265             textInterpolator.rebase()
266             invalidateCallback()
267         }
268     }
269 
270     /**
271      * Set text style with animation. Similar as
272      * fun setTextStyle(
273      *      fvar: String? = "",
274      *      textSize: Float = -1f,
275      *      color: Int? = null,
276      *      strokeWidth: Float = -1f,
277      *      animate: Boolean = true,
278      *      duration: Long = -1L,
279      *      interpolator: TimeInterpolator? = null,
280      *      delay: Long = 0,
281      *      onAnimationEnd: Runnable? = null
282      * )
283      *
284      * @param weight an optional style value for `wght` in fontVariationSettings.
285      * @param width an optional style value for `wdth` in fontVariationSettings.
286      * @param opticalSize an optional style value for `opsz` in fontVariationSettings.
287      * @param roundness an optional style value for `ROND` in fontVariationSettings.
288      */
setTextStylenull289     fun setTextStyle(
290         weight: Int = -1,
291         width: Int = -1,
292         opticalSize: Int = -1,
293         roundness: Int = -1,
294         textSize: Float = -1f,
295         color: Int? = null,
296         strokeWidth: Float = -1f,
297         animate: Boolean = true,
298         duration: Long = -1L,
299         interpolator: TimeInterpolator? = null,
300         delay: Long = 0,
301         onAnimationEnd: Runnable? = null
302     ) {
303         val fvar = fontVariationUtils.updateFontVariation(
304             weight = weight,
305             width = width,
306             opticalSize = opticalSize,
307             roundness = roundness,)
308         setTextStyle(
309             fvar = fvar,
310             textSize = textSize,
311             color = color,
312             strokeWidth = strokeWidth,
313             animate = animate,
314             duration = duration,
315             interpolator = interpolator,
316             delay = delay,
317             onAnimationEnd = onAnimationEnd,
318         )
319     }
320 }
321 
322