Loading packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt +50 −56 Original line number Diff line number Diff line Loading @@ -33,7 +33,9 @@ private const val FONT_ITALIC_MIN = 0f private const val FONT_ITALIC_ANIMATION_STEP = 0.1f private const val FONT_ITALIC_DEFAULT_VALUE = 0f /** Provide interpolation of two fonts by adjusting font variation settings. */ /** * Provide interpolation of two fonts by adjusting font variation settings. */ class FontInterpolator { /** Loading @@ -59,11 +61,8 @@ class FontInterpolator { var index: Int, val sortedAxes: MutableList<FontVariationAxis> ) { constructor( font: Font, axes: List<FontVariationAxis> ) : this( font.sourceIdentifier, constructor(font: Font, axes: List<FontVariationAxis>) : this(font.sourceIdentifier, font.ttcIndex, axes.toMutableList().apply { sortBy { it.tag } } ) Loading @@ -87,7 +86,9 @@ class FontInterpolator { private val tmpInterpKey = InterpKey(null, null, 0f) private val tmpVarFontKey = VarFontKey(0, 0, mutableListOf()) /** Linear interpolate the font variation settings. */ /** * Linear interpolate the font variation settings. */ fun lerp(start: Font, end: Font, progress: Float): Font { if (progress == 0f) { return start Loading @@ -114,26 +115,19 @@ class FontInterpolator { // this doesn't take much time since the variation axes is usually up to 5. If we need to // support more number of axes, we may want to preprocess the font and store the sorted axes // and also pre-fill the missing axes value with default value from 'fvar' table. val newAxes = lerp(startAxes, endAxes) { tag, startValue, endValue -> val newAxes = lerp(startAxes, endAxes) { tag, startValue, endValue -> when (tag) { // TODO: Good to parse 'fvar' table for retrieving default value. TAG_WGHT -> adjustWeight( TAG_WGHT -> adjustWeight( MathUtils.lerp( startValue ?: FONT_WEIGHT_DEFAULT_VALUE, endValue ?: FONT_WEIGHT_DEFAULT_VALUE, progress ) ) TAG_ITAL -> adjustItalic( progress)) TAG_ITAL -> adjustItalic( MathUtils.lerp( startValue ?: FONT_ITALIC_DEFAULT_VALUE, endValue ?: FONT_ITALIC_DEFAULT_VALUE, progress ) ) progress)) else -> { require(startValue != null && endValue != null) { "Unable to interpolate due to unknown default axes value : $tag" Loading @@ -155,7 +149,9 @@ class FontInterpolator { // This is the first time to make the font for the axes. Build and store it to the cache. // Font.Builder#build won't throw IOException since creating fonts from existing fonts will // not do any IO work. val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build() val newFont = Font.Builder(start) .setFontVariationSettings(newAxes.toTypedArray()) .build() interpCache[InterpKey(start, end, progress)] = newFont verFontCache[VarFontKey(start, newAxes)] = newFont return newFont Loading @@ -177,15 +173,13 @@ class FontInterpolator { val tagA = if (i < start.size) start[i].tag else null val tagB = if (j < end.size) end[j].tag else null val comp = when { val comp = when { tagA == null -> 1 tagB == null -> -1 else -> tagA.compareTo(tagB) } val axis = when { val axis = when { comp == 0 -> { val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue) FontVariationAxis(tagA, v) Loading packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +84 −71 Original line number Diff line number Diff line Loading @@ -36,7 +36,8 @@ typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit * 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(...) * Loading @@ -52,34 +53,39 @@ typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit * animator.setTextStyle(-1 /* unchanged weight */, sizePx, animate) * } * } * ``` * </code> * </pre> */ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { 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 { public var animator: ValueAnimator = ValueAnimator.ofFloat(1f).apply { duration = DEFAULT_ANIMATION_DURATION addUpdateListener { textInterpolator.progress = it.animatedValue as Float invalidateCallback() } addListener( object : AnimatorListenerAdapter() { 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. */ /** * 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. */ /** * Mutable Y coordinate of the glyph position relative from the baseline. */ var y: Float = 0f /** Loading @@ -90,29 +96,40 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { /** * Mutable text size of the glyph in pixels. */ /** Mutable text size of the glyph in pixels. */ var textSize: Float = 0f /** Mutable color of the glyph. */ /** * Mutable color of the glyph. */ var color: Int = 0 /** Immutable character offset in the text that the current font run start. */ /** * 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. */ /** * Immutable run length of the font run. */ abstract var runLength: Int protected set /** Immutable glyph index of the font run. */ /** * Immutable glyph index of the font run. */ abstract var glyphIndex: Int protected set /** Immutable font instance for this font run. */ /** * Immutable font instance for this font run. */ abstract var font: Font protected set /** Immutable glyph ID for this glyph. */ /** * Immutable glyph ID for this glyph. */ abstract var glyphId: Int protected set } Loading @@ -130,30 +147,30 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { /** * 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. * 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 * 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. Loading @@ -176,23 +193,22 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { */ var glyphFilter: GlyphCallback? get() = textInterpolator.glyphFilter set(value) { textInterpolator.glyphFilter = value } 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. * 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 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 Loading Loading @@ -221,8 +237,7 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { 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.typeface = typefaceCache.getOrElse(weight) { textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight" textInterpolator.targetPaint.typeface } Loading @@ -234,16 +249,14 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { if (animate) { animator.startDelay = delay animator.duration = if (duration == -1L) { animator.duration = if (duration == -1L) { DEFAULT_ANIMATION_DURATION } else { duration } interpolator?.let { animator.interpolator = it } if (onAnimationEnd != null) { val listener = object : AnimatorListenerAdapter() { val listener = object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { onAnimationEnd.run() animator.removeListener(this) Loading packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +119 −113 Original line number Diff line number Diff line Loading @@ -26,8 +26,12 @@ import android.util.MathUtils import com.android.internal.graphics.ColorUtils import java.lang.Math.max /** Provide text style linear interpolation for plain text. */ class TextInterpolator(layout: Layout) { /** * Provide text style linear interpolation for plain text. */ class TextInterpolator( layout: Layout ) { /** * Returns base paint used for interpolation. Loading Loading @@ -60,11 +64,12 @@ class TextInterpolator(layout: Layout) { var baseFont: Font, var targetFont: Font ) { val length: Int get() = end - start val length: Int get() = end - start } /** A class represents text layout of a single run. */ /** * A class represents text layout of a single run. */ private class Run( val glyphIds: IntArray, val baseX: FloatArray, // same length as glyphIds Loading @@ -74,8 +79,12 @@ class TextInterpolator(layout: Layout) { val fontRuns: List<FontRun> ) /** A class represents text layout of a single line. */ private class Line(val runs: List<Run>) /** * A class represents text layout of a single line. */ private class Line( val runs: List<Run> ) private var lines = listOf<Line>() private val fontInterpolator = FontInterpolator() Loading @@ -97,8 +106,8 @@ class TextInterpolator(layout: Layout) { /** * The layout used for drawing text. * * Only non-styled text is supported. Even if the given layout is created from Spanned, the span * information is not used. * Only non-styled text is supported. Even if the given layout is created from Spanned, the * span information is not used. * * The paint objects used for interpolation are not changed by this method call. * Loading @@ -121,8 +130,8 @@ class TextInterpolator(layout: Layout) { /** * Recalculate internal text layout for interpolation. * * Whenever the target paint is modified, call this method to recalculate internal text layout * used for interpolation. * Whenever the target paint is modified, call this method to recalculate internal * text layout used for interpolation. */ fun onTargetPaintModified() { updatePositionsAndFonts(shapeText(layout, targetPaint), updateBase = false) Loading @@ -131,8 +140,8 @@ class TextInterpolator(layout: Layout) { /** * Recalculate internal text layout for interpolation. * * Whenever the base paint is modified, call this method to recalculate internal text layout * used for interpolation. * Whenever the base paint is modified, call this method to recalculate internal * text layout used for interpolation. */ fun onBasePaintModified() { updatePositionsAndFonts(shapeText(layout, basePaint), updateBase = true) Loading @@ -143,11 +152,11 @@ class TextInterpolator(layout: Layout) { * * The text interpolator does not calculate all the text position by text shaper due to * performance reasons. Instead, the text interpolator shape the start and end state and * calculate text position of the middle state by linear interpolation. Due to this trick, the * text positions of the middle state is likely different from the text shaper result. So, if * you want to start animation from the middle state, you will see the glyph jumps due to this * trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different from * text shape result of weight 550. * calculate text position of the middle state by linear interpolation. Due to this trick, * the text positions of the middle state is likely different from the text shaper result. * So, if you want to start animation from the middle state, you will see the glyph jumps due to * this trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different * from text shape result of weight 550. * * After calling this method, do not call onBasePaintModified() since it reshape the text and * update the base state. As in above notice, the text shaping result at current progress is Loading @@ -159,7 +168,8 @@ class TextInterpolator(layout: Layout) { * animate weight from 200 to 400, then if you want to move back to 200 at the half of the * animation, it will look like * * ``` * <pre> * <code> * val interp = TextInterpolator(layout) * * // Interpolate between weight 200 to 400. Loading Loading @@ -189,7 +199,9 @@ class TextInterpolator(layout: Layout) { * // progress is 0.5 * animator.start() * } * ``` * </code> * </pre> * */ fun rebase() { if (progress == 0f) { Loading Loading @@ -251,20 +263,17 @@ class TextInterpolator(layout: Layout) { } var maxRunLength = 0 lines = baseLayout.zip(targetLayout) { baseLine, targetLine -> val runs = baseLine.zip(targetLine) { base, target -> lines = baseLayout.zip(targetLayout) { baseLine, targetLine -> val runs = baseLine.zip(targetLine) { base, target -> require(base.glyphCount() == target.glyphCount()) { "Inconsistent glyph count at line ${lines.size}" } val glyphCount = base.glyphCount() // Good to recycle the array if the existing array can hold the new layout // result. val glyphIds = IntArray(glyphCount) { // Good to recycle the array if the existing array can hold the new layout result. val glyphIds = IntArray(glyphCount) { base.getGlyphId(it).also { baseGlyphId -> require(baseGlyphId == target.getGlyphId(it)) { "Inconsistent glyph ID at $it in line ${lines.size}" Loading Loading @@ -293,8 +302,7 @@ class TextInterpolator(layout: Layout) { if (baseFont !== nextBaseFont) { require(targetFont !== nextTargetFont) { "Base font has changed at $i but target font has not " + "changed." "Base font has changed at $i but target font has not changed." } // Font transition point. push run and reset context. fontRun.add(FontRun(start, i, baseFont, targetFont)) Loading @@ -303,13 +311,11 @@ class TextInterpolator(layout: Layout) { targetFont = nextTargetFont start = i require(FontInterpolator.canInterpolate(baseFont, targetFont)) { "Cannot interpolate font at $start ($baseFont vs " + "$targetFont)" "Cannot interpolate font at $start ($baseFont vs $targetFont)" } } else { // baseFont === nextBaseFont require(targetFont === nextTargetFont) { "Base font has not changed at $i but target font has " + "changed." "Base font has not changed at $i but target font has changed." } } } Loading Loading @@ -388,8 +394,7 @@ class TextInterpolator(layout: Layout) { 0, i - prevStart, font, tmpPaintForGlyph ) tmpPaintForGlyph) prevStart = i arrayIndex = 0 } Loading @@ -405,8 +410,7 @@ class TextInterpolator(layout: Layout) { 0, run.end - prevStart, font, tmpPaintForGlyph ) tmpPaintForGlyph) } private fun updatePositionsAndFonts( Loading @@ -414,7 +418,9 @@ class TextInterpolator(layout: Layout) { updateBase: Boolean ) { // Update target positions with newly calculated text layout. check(layoutResult.size == lines.size) { "The new layout result has different line count." } check(layoutResult.size == lines.size) { "The new layout result has different line count." } lines.zip(layoutResult) { line, runs -> line.runs.zip(runs) { lineRun, newGlyphs -> Loading Loading @@ -474,7 +480,10 @@ class TextInterpolator(layout: Layout) { } // Shape the text and stores the result to out argument. private fun shapeText(layout: Layout, paint: TextPaint): List<List<PositionedGlyphs>> { private fun shapeText( layout: Layout, paint: TextPaint ): List<List<PositionedGlyphs>> { val out = mutableListOf<List<PositionedGlyphs>>() for (lineNo in 0 until layout.lineCount) { // Shape all lines. val lineStart = layout.getLineStart(lineNo) Loading @@ -486,13 +495,10 @@ class TextInterpolator(layout: Layout) { } val runs = mutableListOf<PositionedGlyphs>() TextShaper.shapeText( layout.text, lineStart, count, layout.textDirectionHeuristic, paint ) { _, _, glyphs, _ -> runs.add(glyphs) } TextShaper.shapeText(layout.text, lineStart, count, layout.textDirectionHeuristic, paint) { _, _, glyphs, _ -> runs.add(glyphs) } out.add(runs) } return out Loading Loading
packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt +50 −56 Original line number Diff line number Diff line Loading @@ -33,7 +33,9 @@ private const val FONT_ITALIC_MIN = 0f private const val FONT_ITALIC_ANIMATION_STEP = 0.1f private const val FONT_ITALIC_DEFAULT_VALUE = 0f /** Provide interpolation of two fonts by adjusting font variation settings. */ /** * Provide interpolation of two fonts by adjusting font variation settings. */ class FontInterpolator { /** Loading @@ -59,11 +61,8 @@ class FontInterpolator { var index: Int, val sortedAxes: MutableList<FontVariationAxis> ) { constructor( font: Font, axes: List<FontVariationAxis> ) : this( font.sourceIdentifier, constructor(font: Font, axes: List<FontVariationAxis>) : this(font.sourceIdentifier, font.ttcIndex, axes.toMutableList().apply { sortBy { it.tag } } ) Loading @@ -87,7 +86,9 @@ class FontInterpolator { private val tmpInterpKey = InterpKey(null, null, 0f) private val tmpVarFontKey = VarFontKey(0, 0, mutableListOf()) /** Linear interpolate the font variation settings. */ /** * Linear interpolate the font variation settings. */ fun lerp(start: Font, end: Font, progress: Float): Font { if (progress == 0f) { return start Loading @@ -114,26 +115,19 @@ class FontInterpolator { // this doesn't take much time since the variation axes is usually up to 5. If we need to // support more number of axes, we may want to preprocess the font and store the sorted axes // and also pre-fill the missing axes value with default value from 'fvar' table. val newAxes = lerp(startAxes, endAxes) { tag, startValue, endValue -> val newAxes = lerp(startAxes, endAxes) { tag, startValue, endValue -> when (tag) { // TODO: Good to parse 'fvar' table for retrieving default value. TAG_WGHT -> adjustWeight( TAG_WGHT -> adjustWeight( MathUtils.lerp( startValue ?: FONT_WEIGHT_DEFAULT_VALUE, endValue ?: FONT_WEIGHT_DEFAULT_VALUE, progress ) ) TAG_ITAL -> adjustItalic( progress)) TAG_ITAL -> adjustItalic( MathUtils.lerp( startValue ?: FONT_ITALIC_DEFAULT_VALUE, endValue ?: FONT_ITALIC_DEFAULT_VALUE, progress ) ) progress)) else -> { require(startValue != null && endValue != null) { "Unable to interpolate due to unknown default axes value : $tag" Loading @@ -155,7 +149,9 @@ class FontInterpolator { // This is the first time to make the font for the axes. Build and store it to the cache. // Font.Builder#build won't throw IOException since creating fonts from existing fonts will // not do any IO work. val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build() val newFont = Font.Builder(start) .setFontVariationSettings(newAxes.toTypedArray()) .build() interpCache[InterpKey(start, end, progress)] = newFont verFontCache[VarFontKey(start, newAxes)] = newFont return newFont Loading @@ -177,15 +173,13 @@ class FontInterpolator { val tagA = if (i < start.size) start[i].tag else null val tagB = if (j < end.size) end[j].tag else null val comp = when { val comp = when { tagA == null -> 1 tagB == null -> -1 else -> tagA.compareTo(tagB) } val axis = when { val axis = when { comp == 0 -> { val v = filter(tagA!!, start[i++].styleValue, end[j++].styleValue) FontVariationAxis(tagA, v) Loading
packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +84 −71 Original line number Diff line number Diff line Loading @@ -36,7 +36,8 @@ typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit * 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(...) * Loading @@ -52,34 +53,39 @@ typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit * animator.setTextStyle(-1 /* unchanged weight */, sizePx, animate) * } * } * ``` * </code> * </pre> */ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { 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 { public var animator: ValueAnimator = ValueAnimator.ofFloat(1f).apply { duration = DEFAULT_ANIMATION_DURATION addUpdateListener { textInterpolator.progress = it.animatedValue as Float invalidateCallback() } addListener( object : AnimatorListenerAdapter() { 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. */ /** * 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. */ /** * Mutable Y coordinate of the glyph position relative from the baseline. */ var y: Float = 0f /** Loading @@ -90,29 +96,40 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { /** * Mutable text size of the glyph in pixels. */ /** Mutable text size of the glyph in pixels. */ var textSize: Float = 0f /** Mutable color of the glyph. */ /** * Mutable color of the glyph. */ var color: Int = 0 /** Immutable character offset in the text that the current font run start. */ /** * 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. */ /** * Immutable run length of the font run. */ abstract var runLength: Int protected set /** Immutable glyph index of the font run. */ /** * Immutable glyph index of the font run. */ abstract var glyphIndex: Int protected set /** Immutable font instance for this font run. */ /** * Immutable font instance for this font run. */ abstract var font: Font protected set /** Immutable glyph ID for this glyph. */ /** * Immutable glyph ID for this glyph. */ abstract var glyphId: Int protected set } Loading @@ -130,30 +147,30 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { /** * 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. * 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 * 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. Loading @@ -176,23 +193,22 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { */ var glyphFilter: GlyphCallback? get() = textInterpolator.glyphFilter set(value) { textInterpolator.glyphFilter = value } 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. * 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 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 Loading Loading @@ -221,8 +237,7 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { 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.typeface = typefaceCache.getOrElse(weight) { textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight" textInterpolator.targetPaint.typeface } Loading @@ -234,16 +249,14 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { if (animate) { animator.startDelay = delay animator.duration = if (duration == -1L) { animator.duration = if (duration == -1L) { DEFAULT_ANIMATION_DURATION } else { duration } interpolator?.let { animator.interpolator = it } if (onAnimationEnd != null) { val listener = object : AnimatorListenerAdapter() { val listener = object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { onAnimationEnd.run() animator.removeListener(this) Loading
packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +119 −113 Original line number Diff line number Diff line Loading @@ -26,8 +26,12 @@ import android.util.MathUtils import com.android.internal.graphics.ColorUtils import java.lang.Math.max /** Provide text style linear interpolation for plain text. */ class TextInterpolator(layout: Layout) { /** * Provide text style linear interpolation for plain text. */ class TextInterpolator( layout: Layout ) { /** * Returns base paint used for interpolation. Loading Loading @@ -60,11 +64,12 @@ class TextInterpolator(layout: Layout) { var baseFont: Font, var targetFont: Font ) { val length: Int get() = end - start val length: Int get() = end - start } /** A class represents text layout of a single run. */ /** * A class represents text layout of a single run. */ private class Run( val glyphIds: IntArray, val baseX: FloatArray, // same length as glyphIds Loading @@ -74,8 +79,12 @@ class TextInterpolator(layout: Layout) { val fontRuns: List<FontRun> ) /** A class represents text layout of a single line. */ private class Line(val runs: List<Run>) /** * A class represents text layout of a single line. */ private class Line( val runs: List<Run> ) private var lines = listOf<Line>() private val fontInterpolator = FontInterpolator() Loading @@ -97,8 +106,8 @@ class TextInterpolator(layout: Layout) { /** * The layout used for drawing text. * * Only non-styled text is supported. Even if the given layout is created from Spanned, the span * information is not used. * Only non-styled text is supported. Even if the given layout is created from Spanned, the * span information is not used. * * The paint objects used for interpolation are not changed by this method call. * Loading @@ -121,8 +130,8 @@ class TextInterpolator(layout: Layout) { /** * Recalculate internal text layout for interpolation. * * Whenever the target paint is modified, call this method to recalculate internal text layout * used for interpolation. * Whenever the target paint is modified, call this method to recalculate internal * text layout used for interpolation. */ fun onTargetPaintModified() { updatePositionsAndFonts(shapeText(layout, targetPaint), updateBase = false) Loading @@ -131,8 +140,8 @@ class TextInterpolator(layout: Layout) { /** * Recalculate internal text layout for interpolation. * * Whenever the base paint is modified, call this method to recalculate internal text layout * used for interpolation. * Whenever the base paint is modified, call this method to recalculate internal * text layout used for interpolation. */ fun onBasePaintModified() { updatePositionsAndFonts(shapeText(layout, basePaint), updateBase = true) Loading @@ -143,11 +152,11 @@ class TextInterpolator(layout: Layout) { * * The text interpolator does not calculate all the text position by text shaper due to * performance reasons. Instead, the text interpolator shape the start and end state and * calculate text position of the middle state by linear interpolation. Due to this trick, the * text positions of the middle state is likely different from the text shaper result. So, if * you want to start animation from the middle state, you will see the glyph jumps due to this * trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different from * text shape result of weight 550. * calculate text position of the middle state by linear interpolation. Due to this trick, * the text positions of the middle state is likely different from the text shaper result. * So, if you want to start animation from the middle state, you will see the glyph jumps due to * this trick, i.e. the progress 0.5 of interpolation between weight 400 and 700 is different * from text shape result of weight 550. * * After calling this method, do not call onBasePaintModified() since it reshape the text and * update the base state. As in above notice, the text shaping result at current progress is Loading @@ -159,7 +168,8 @@ class TextInterpolator(layout: Layout) { * animate weight from 200 to 400, then if you want to move back to 200 at the half of the * animation, it will look like * * ``` * <pre> * <code> * val interp = TextInterpolator(layout) * * // Interpolate between weight 200 to 400. Loading Loading @@ -189,7 +199,9 @@ class TextInterpolator(layout: Layout) { * // progress is 0.5 * animator.start() * } * ``` * </code> * </pre> * */ fun rebase() { if (progress == 0f) { Loading Loading @@ -251,20 +263,17 @@ class TextInterpolator(layout: Layout) { } var maxRunLength = 0 lines = baseLayout.zip(targetLayout) { baseLine, targetLine -> val runs = baseLine.zip(targetLine) { base, target -> lines = baseLayout.zip(targetLayout) { baseLine, targetLine -> val runs = baseLine.zip(targetLine) { base, target -> require(base.glyphCount() == target.glyphCount()) { "Inconsistent glyph count at line ${lines.size}" } val glyphCount = base.glyphCount() // Good to recycle the array if the existing array can hold the new layout // result. val glyphIds = IntArray(glyphCount) { // Good to recycle the array if the existing array can hold the new layout result. val glyphIds = IntArray(glyphCount) { base.getGlyphId(it).also { baseGlyphId -> require(baseGlyphId == target.getGlyphId(it)) { "Inconsistent glyph ID at $it in line ${lines.size}" Loading Loading @@ -293,8 +302,7 @@ class TextInterpolator(layout: Layout) { if (baseFont !== nextBaseFont) { require(targetFont !== nextTargetFont) { "Base font has changed at $i but target font has not " + "changed." "Base font has changed at $i but target font has not changed." } // Font transition point. push run and reset context. fontRun.add(FontRun(start, i, baseFont, targetFont)) Loading @@ -303,13 +311,11 @@ class TextInterpolator(layout: Layout) { targetFont = nextTargetFont start = i require(FontInterpolator.canInterpolate(baseFont, targetFont)) { "Cannot interpolate font at $start ($baseFont vs " + "$targetFont)" "Cannot interpolate font at $start ($baseFont vs $targetFont)" } } else { // baseFont === nextBaseFont require(targetFont === nextTargetFont) { "Base font has not changed at $i but target font has " + "changed." "Base font has not changed at $i but target font has changed." } } } Loading Loading @@ -388,8 +394,7 @@ class TextInterpolator(layout: Layout) { 0, i - prevStart, font, tmpPaintForGlyph ) tmpPaintForGlyph) prevStart = i arrayIndex = 0 } Loading @@ -405,8 +410,7 @@ class TextInterpolator(layout: Layout) { 0, run.end - prevStart, font, tmpPaintForGlyph ) tmpPaintForGlyph) } private fun updatePositionsAndFonts( Loading @@ -414,7 +418,9 @@ class TextInterpolator(layout: Layout) { updateBase: Boolean ) { // Update target positions with newly calculated text layout. check(layoutResult.size == lines.size) { "The new layout result has different line count." } check(layoutResult.size == lines.size) { "The new layout result has different line count." } lines.zip(layoutResult) { line, runs -> line.runs.zip(runs) { lineRun, newGlyphs -> Loading Loading @@ -474,7 +480,10 @@ class TextInterpolator(layout: Layout) { } // Shape the text and stores the result to out argument. private fun shapeText(layout: Layout, paint: TextPaint): List<List<PositionedGlyphs>> { private fun shapeText( layout: Layout, paint: TextPaint ): List<List<PositionedGlyphs>> { val out = mutableListOf<List<PositionedGlyphs>>() for (lineNo in 0 until layout.lineCount) { // Shape all lines. val lineStart = layout.getLineStart(lineNo) Loading @@ -486,13 +495,10 @@ class TextInterpolator(layout: Layout) { } val runs = mutableListOf<PositionedGlyphs>() TextShaper.shapeText( layout.text, lineStart, count, layout.textDirectionHeuristic, paint ) { _, _, glyphs, _ -> runs.add(glyphs) } TextShaper.shapeText(layout.text, lineStart, count, layout.textDirectionHeuristic, paint) { _, _, glyphs, _ -> runs.add(glyphs) } out.add(runs) } return out Loading