| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.animation |
| |
| import android.animation.Animator |
| import android.animation.AnimatorListenerAdapter |
| import android.animation.TimeInterpolator |
| import android.animation.ValueAnimator |
| import android.graphics.Canvas |
| import android.graphics.Typeface |
| import android.graphics.fonts.Font |
| import android.text.Layout |
| import android.util.SparseArray |
| |
| private const val TAG_WGHT = "wght" |
| private const val DEFAULT_ANIMATION_DURATION: Long = 300 |
| |
| typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit |
| /** |
| * This class provides text animation between two styles. |
| * |
| * 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: |
| * |
| * <pre> |
| * <code> |
| * class SimpleTextAnimation : View { |
| * @JvmOverloads constructor(...) |
| * |
| * private val layout: Layout = ... // Text layout, e.g. StaticLayout. |
| * |
| * // TextAnimator tells us when needs to be invalidate. |
| * private val animator = TextAnimator(layout) { invalidate() } |
| * |
| * override fun onDraw(canvas: Canvas) = animator.draw(canvas) |
| * |
| * // Change the text size with animation. |
| * fun setTextSize(sizePx: Float, animate: Boolean) { |
| * animator.setTextStyle(-1 /* unchanged weight */, sizePx, animate) |
| * } |
| * } |
| * </code> |
| * </pre> |
| */ |
| class TextAnimator( |
| layout: Layout, |
| private val invalidateCallback: () -> Unit |
| ) { |
| // Following two members are for mutable for testing purposes. |
| public var textInterpolator: TextInterpolator = TextInterpolator(layout) |
| public var animator: ValueAnimator = ValueAnimator.ofFloat(1f).apply { |
| duration = DEFAULT_ANIMATION_DURATION |
| addUpdateListener { |
| textInterpolator.progress = it.animatedValue as Float |
| invalidateCallback() |
| } |
| addListener(object : AnimatorListenerAdapter() { |
| override fun onAnimationEnd(animation: Animator?) { |
| textInterpolator.rebase() |
| } |
| override fun onAnimationCancel(animation: Animator?) = textInterpolator.rebase() |
| }) |
| } |
| |
| sealed class PositionedGlyph { |
| |
| /** |
| * Mutable X coordinate of the glyph position relative from drawing offset. |
| */ |
| var x: Float = 0f |
| |
| /** |
| * Mutable Y coordinate of the glyph position relative from the baseline. |
| */ |
| var y: Float = 0f |
| |
| /** |
| * Mutable text size of the glyph in pixels. |
| */ |
| var textSize: Float = 0f |
| |
| /** |
| * Mutable color of the glyph. |
| */ |
| var color: Int = 0 |
| |
| /** |
| * Immutable character offset in the text that the current font run start. |
| */ |
| abstract var runStart: Int |
| protected set |
| |
| /** |
| * Immutable run length of the font run. |
| */ |
| abstract var runLength: Int |
| protected set |
| |
| /** |
| * Immutable glyph index of the font run. |
| */ |
| abstract var glyphIndex: Int |
| protected set |
| |
| /** |
| * Immutable font instance for this font run. |
| */ |
| abstract var font: Font |
| protected set |
| |
| /** |
| * Immutable glyph ID for this glyph. |
| */ |
| abstract var glyphId: Int |
| protected set |
| } |
| |
| private val typefaceCache = SparseArray<Typeface?>() |
| |
| fun updateLayout(layout: Layout) { |
| textInterpolator.layout = layout |
| } |
| |
| fun isRunning(): Boolean { |
| return animator.isRunning |
| } |
| |
| /** |
| * GlyphFilter applied just before drawing to canvas for tweaking positions and text size. |
| * |
| * This callback is called for each glyphs just before drawing the glyphs. This function will |
| * be called with the intrinsic position, size, color, glyph ID and font instance. You can |
| * mutate the position, size and color for tweaking animations. |
| * Do not keep the reference of passed glyph object. The interpolator reuses that object for |
| * avoiding object allocations. |
| * |
| * Details: |
| * The text is drawn with font run units. The font run is a text segment that draws with the |
| * same font. The {@code runStart} and {@code runLimit} is a range of the font run in the text |
| * that current glyph is in. Once the font run is determined, the system will convert characters |
| * into glyph IDs. The {@code glyphId} is the glyph identifier in the font and |
| * {@code glyphIndex} is the offset of the converted glyph array. Please note that the |
| * {@code glyphIndex} is not a character index, because the character will not be converted to |
| * glyph one-by-one. If there are ligatures including emoji sequence, etc, the glyph ID may be |
| * composed from multiple characters. |
| * |
| * Here is an example of font runs: "fin. 終わり" |
| * |
| * Characters : f i n . _ 終 わ り |
| * Code Points: \u0066 \u0069 \u006E \u002E \u0020 \u7D42 \u308F \u308A |
| * Font Runs : <-- Roboto-Regular.ttf --><-- NotoSans-CJK.otf --> |
| * runStart = 0, runLength = 5 runStart = 5, runLength = 3 |
| * Glyph IDs : 194 48 7 8 4367 1039 1002 |
| * Glyph Index: 0 1 2 3 0 1 2 |
| * |
| * In this example, the "fi" is converted into ligature form, thus the single glyph ID is |
| * assigned for two characters, f and i. |
| * |
| * Example: |
| * ``` |
| * private val glyphFilter: GlyphCallback = { glyph, progress -> |
| * val index = glyph.runStart |
| * val i = glyph.glyphIndex |
| * val moveAmount = 1.3f |
| * val sign = (-1 + 2 * ((i + index) % 2)) |
| * val turnProgress = if (progress < .5f) progress / 0.5f else (1.0f - progress) / 0.5f |
| * |
| * // You can modify (x, y) coordinates, textSize and color during animation. |
| * glyph.textSize += glyph.textSize * sign * moveAmount * turnProgress |
| * glyph.y += glyph.y * sign * moveAmount * turnProgress |
| * glyph.x += glyph.x * sign * moveAmount * turnProgress |
| * } |
| * ``` |
| */ |
| var glyphFilter: GlyphCallback? |
| get() = textInterpolator.glyphFilter |
| set(value) { textInterpolator.glyphFilter = value } |
| |
| fun draw(c: Canvas) = textInterpolator.draw(c) |
| |
| /** |
| * Set text style with animation. |
| * |
| * By passing -1 to weight, the view preserve the current weight. |
| * By passing -1 to textSize, the view preserve the current text size. |
| * Bu passing -1 to duration, the default text animation, 1000ms, is used. |
| * By passing false to animate, the text will be updated without animation. |
| * |
| * @param weight an optional text weight. |
| * @param textSize an optional font size. |
| * @param colors an optional colors array that must be the same size as numLines passed to |
| * the TextInterpolator |
| * @param animate an optional boolean indicating true for showing style transition as animation, |
| * false for immediate style transition. True by default. |
| * @param duration an optional animation duration in milliseconds. This is ignored if animate is |
| * false. |
| * @param interpolator an optional time interpolator. If null is passed, last set interpolator |
| * will be used. This is ignored if animate is false. |
| */ |
| fun setTextStyle( |
| weight: Int = -1, |
| textSize: Float = -1f, |
| color: Int? = null, |
| animate: Boolean = true, |
| duration: Long = -1L, |
| interpolator: TimeInterpolator? = null, |
| delay: Long = 0, |
| onAnimationEnd: Runnable? = null |
| ) { |
| if (animate) { |
| animator.cancel() |
| textInterpolator.rebase() |
| } |
| |
| if (textSize >= 0) { |
| textInterpolator.targetPaint.textSize = textSize |
| } |
| if (weight >= 0) { |
| // Paint#setFontVariationSettings creates Typeface instance from scratch. To reduce the |
| // memory impact, cache the typeface result. |
| textInterpolator.targetPaint.typeface = typefaceCache.getOrElse(weight) { |
| textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight" |
| textInterpolator.targetPaint.typeface |
| } |
| } |
| if (color != null) { |
| textInterpolator.targetPaint.color = color |
| } |
| textInterpolator.onTargetPaintModified() |
| |
| if (animate) { |
| animator.startDelay = delay |
| animator.duration = if (duration == -1L) { |
| DEFAULT_ANIMATION_DURATION |
| } else { |
| duration |
| } |
| interpolator?.let { animator.interpolator = it } |
| if (onAnimationEnd != null) { |
| val listener = object : AnimatorListenerAdapter() { |
| override fun onAnimationEnd(animation: Animator?) { |
| onAnimationEnd.run() |
| animator.removeListener(this) |
| } |
| override fun onAnimationCancel(animation: Animator?) { |
| animator.removeListener(this) |
| } |
| } |
| animator.addListener(listener) |
| } |
| animator.start() |
| } else { |
| // No animation is requested, thus set base and target state to the same state. |
| textInterpolator.progress = 1f |
| textInterpolator.rebase() |
| invalidateCallback() |
| } |
| } |
| } |
| |
| private fun <V> SparseArray<V>.getOrElse(key: Int, defaultValue: () -> V): V { |
| var v = get(key) |
| if (v == null) { |
| v = defaultValue() |
| put(key, v) |
| } |
| return v |
| } |