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