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 }