Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 06eda17d authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add BiDi support to TextInterpolator"

parents af69f9d7 f4aef967
Loading
Loading
Loading
Loading
+143 −122
Original line number Diff line number Diff line
@@ -19,8 +19,9 @@ import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.fonts.Font
import android.graphics.text.PositionedGlyphs
import android.graphics.text.TextRunShaper
import android.text.Layout
import android.text.TextPaint
import android.text.TextShaper
import android.util.MathUtils
import com.android.internal.graphics.ColorUtils
import java.lang.Math.max
@@ -57,10 +58,10 @@ class TextInterpolator(
     */
    val targetPaint = createDefaultPaint(layout.paint, lines)

    private fun createDefaultPaint(paint: Paint, lines: Int): ArrayList<Paint> {
        val paintList = ArrayList<Paint>()
    private fun createDefaultPaint(paint: TextPaint, lines: Int): ArrayList<TextPaint> {
        val paintList = ArrayList<TextPaint>()
        for (i in 0 until lines)
            paintList.add(Paint(paint))
            paintList.add(TextPaint(paint))
        return paintList
    }

@@ -79,9 +80,9 @@ class TextInterpolator(
    }

    /**
     * A class represents text layout of a single line.
     * A class represents text layout of a single run.
     */
    private class Line(
    private class Run(
        val glyphIds: IntArray,
        val baseX: FloatArray, // same length as glyphIds
        val baseY: FloatArray, // same length as glyphIds
@@ -90,11 +91,18 @@ class TextInterpolator(
        val fontRuns: List<FontRun>
    )

    /**
     * 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()

    // Recycling object for glyph drawing. Will be extended for the longest font run if needed.
    private val tmpDrawPaints = ArrayList<Paint>()
    private val tmpDrawPaints = ArrayList<TextPaint>()
    private var tmpPositionArray = FloatArray(20)

    /**
@@ -215,23 +223,25 @@ class TextInterpolator(
        }

        lines.forEach { line ->
            for (i in line.baseX.indices) {
                line.baseX[i] = MathUtils.lerp(line.baseX[i], line.targetX[i], progress)
                line.baseY[i] = MathUtils.lerp(line.baseY[i], line.targetY[i], progress)
            line.runs.forEach { run ->
                for (i in run.baseX.indices) {
                    run.baseX[i] = MathUtils.lerp(run.baseX[i], run.targetX[i], progress)
                    run.baseY[i] = MathUtils.lerp(run.baseY[i], run.targetY[i], progress)
                }
            line.fontRuns.forEach {
                run.fontRuns.forEach {
                    it.baseFont = fontInterpolator.lerp(it.baseFont, it.targetFont, progress)
                }
            }
        }

        progress = 0f
    }

    companion object {
        fun updatePaint(toUpdate: ArrayList<Paint>, newValues: ArrayList<Paint>) {
        fun updatePaint(toUpdate: ArrayList<TextPaint>, newValues: ArrayList<TextPaint>) {
            toUpdate.clear()
            for (paint in newValues)
                toUpdate.add(Paint(paint))
                toUpdate.add(TextPaint(paint))
        }
    }

@@ -243,23 +253,25 @@ class TextInterpolator(
    fun draw(canvas: Canvas) {
        lerp(basePaint, targetPaint, progress, tmpDrawPaints)
        lines.forEachIndexed { lineNo, line ->
            line.runs.forEach { run ->
                canvas.save()
                try {
                    // Move to drawing origin.
                    val origin = layout.getDrawOrigin(lineNo)
                    canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat())

                line.fontRuns.forEach { run ->
                    run.fontRuns.forEach { fontRun ->
                        if (lineNo >= tmpDrawPaints.size)
                        drawFontRun(canvas, line, run, tmpDrawPaints[0])
                            drawFontRun(canvas, run, fontRun, tmpDrawPaints[0])
                        else
                        drawFontRun(canvas, line, run, tmpDrawPaints[lineNo])
                            drawFontRun(canvas, run, fontRun, tmpDrawPaints[lineNo])
                    }
                } finally {
                    canvas.restore()
                }
            }
        }
    }

    // Shape text with current paint parameters.
    private fun shapeText(layout: Layout) {
@@ -271,7 +283,9 @@ class TextInterpolator(
        }

        var maxRunLength = 0
        lines = baseLayout.zip(targetLayout) { 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}"
                }
@@ -328,7 +342,9 @@ class TextInterpolator(
                    fontRun.add(FontRun(start, glyphCount, baseFont, targetFont))
                    maxRunLength = max(maxRunLength, glyphCount - start)
                }
            Line(glyphIds, baseX, baseY, targetX, targetY, fontRun)
                Run(glyphIds, baseX, baseY, targetX, targetY, fontRun)
            }
            Line(runs)
        }

        // Update float array used for drawing.
@@ -338,7 +354,7 @@ class TextInterpolator(
    }

    // Draws single font run.
    private fun drawFontRun(c: Canvas, line: Line, run: FontRun, paint: Paint) {
    private fun drawFontRun(c: Canvas, line: Run, run: FontRun, paint: Paint) {
        var arrayIndex = 0
        for (i in run.start until run.end) {
            tmpPositionArray[arrayIndex++] =
@@ -358,7 +374,7 @@ class TextInterpolator(
    }

    private fun updatePositionsAndFonts(
        layoutResult: List<PositionedGlyphs>,
        layoutResult: List<List<PositionedGlyphs>>,
        updateBase: Boolean
    ) {
        // Update target positions with newly calculated text layout.
@@ -366,15 +382,16 @@ class TextInterpolator(
            "The new layout result has different line count."
        }

        lines.zip(layoutResult) { line, newGlyphs ->
            require(newGlyphs.glyphCount() == line.glyphIds.size) {
        lines.zip(layoutResult) { line, runs ->
            line.runs.zip(runs) { lineRun, newGlyphs ->
                require(newGlyphs.glyphCount() == lineRun.glyphIds.size) {
                    "The new layout has different glyph count."
                }

            line.fontRuns.forEach { run ->
                lineRun.fontRuns.forEach { run ->
                    val newFont = newGlyphs.getFont(run.start)
                    for (i in run.start until run.end) {
                    require(newGlyphs.getGlyphId(run.start) == line.glyphIds[run.start]) {
                        require(newGlyphs.getGlyphId(run.start) == lineRun.glyphIds[run.start]) {
                            "The new layout has different glyph ID at ${run.start}"
                        }
                        require(newFont === newGlyphs.getFont(i)) {
@@ -383,10 +400,11 @@ class TextInterpolator(
                        }
                    }

                // The passing base font and target font is already interpolatable, so just check
                // new font can be interpolatable with base font.
                    // The passing base font and target font is already interpolatable, so just
                    // check new font can be interpolatable with base font.
                    require(FontInterpolator.canInterpolate(newFont, run.baseFont)) {
                    "New font cannot be interpolated with existing font. $newFont, ${run.baseFont}"
                        "New font cannot be interpolated with existing font. $newFont," +
                                " ${run.baseFont}"
                    }

                    if (updateBase) {
@@ -397,14 +415,15 @@ class TextInterpolator(
                }

                if (updateBase) {
                for (i in line.baseX.indices) {
                    line.baseX[i] = newGlyphs.getGlyphX(i)
                    line.baseY[i] = newGlyphs.getGlyphY(i)
                    for (i in lineRun.baseX.indices) {
                        lineRun.baseX[i] = newGlyphs.getGlyphX(i)
                        lineRun.baseY[i] = newGlyphs.getGlyphY(i)
                    }
                } else {
                for (i in line.baseX.indices) {
                    line.targetX[i] = newGlyphs.getGlyphX(i)
                    line.targetY[i] = newGlyphs.getGlyphY(i)
                    for (i in lineRun.baseX.indices) {
                        lineRun.targetX[i] = newGlyphs.getGlyphX(i)
                        lineRun.targetY[i] = newGlyphs.getGlyphY(i)
                    }
                }
            }
        }
@@ -412,16 +431,16 @@ class TextInterpolator(

    // Linear interpolate the paint.
    private fun lerp(
        from: ArrayList<Paint>,
        to: ArrayList<Paint>,
        from: ArrayList<TextPaint>,
        to: ArrayList<TextPaint>,
        progress: Float,
        out: ArrayList<Paint>
        out: ArrayList<TextPaint>
    ) {
        out.clear()
        // Currently only font size & colors are interpolated.
        // TODO(172943390): Add other interpolation or support custom interpolator.
        for (index in from.indices) {
            val paint = Paint(from[index])
            val paint = TextPaint(from[index])
            paint.textSize = MathUtils.lerp(from[index].textSize, to[index].textSize, progress)
            paint.color = ColorUtils.blendARGB(from[index].color, to[index].color, progress)
            out.add(paint)
@@ -429,18 +448,20 @@ class TextInterpolator(
    }

    // Shape the text and stores the result to out argument.
    private fun shapeText(layout: Layout, paints: ArrayList<Paint>): List<PositionedGlyphs> {
        val out = mutableListOf<PositionedGlyphs>()
    private fun shapeText(
        layout: Layout,
        paints: ArrayList<TextPaint>
    ): List<List<PositionedGlyphs>> {
        val out = mutableListOf<List<PositionedGlyphs>>()
        for (lineNo in 0 until layout.lineCount) { // Shape all lines.
            val lineStart = layout.getLineStart(lineNo)
            val count = layout.getLineEnd(lineNo) - lineStart
            out.add(TextRunShaper.shapeTextRun(
                    layout.text, // Styles are ignored.
                    lineStart, count, // shape range
                    lineStart, count, // shape context = shape range.
                    0f, 0f, // the layout offset. Not changed.
                    layout.getParagraphDirection(lineNo) == Layout.DIR_RIGHT_TO_LEFT,
                    paints[lineNo])) // Use given paint instead of layout's for style interpolation.
            val runs = mutableListOf<PositionedGlyphs>()
            TextShaper.shapeText(layout.text, lineStart, count, layout.textDirectionHeuristic,
                    paints[lineNo]) { _, _, glyphs, _ ->
                runs.add(glyphs)
            }
            out.add(runs)
        }
        return out
    }
+2 −3
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.keyguard

import android.animation.ValueAnimator
import android.graphics.Paint
import android.testing.AndroidTestingRunner
import android.text.Layout
import android.text.StaticLayout
@@ -53,7 +52,7 @@ class TextAnimatorTest : SysuiTestCase() {
        val layout = makeLayout("Hello, World", PAINT[0])
        val valueAnimator = mock(ValueAnimator::class.java)
        val textInterpolator = mock(TextInterpolator::class.java)
        val paint = arrayListOf(mock(Paint::class.java))
        val paint = arrayListOf(mock(TextPaint::class.java))
        `when`(textInterpolator.targetPaint).thenReturn(paint)

        val textAnimator = TextAnimator(layout, {}).apply {
@@ -85,7 +84,7 @@ class TextAnimatorTest : SysuiTestCase() {
        val layout = makeLayout("Hello, World", PAINT[0])
        val valueAnimator = mock(ValueAnimator::class.java)
        val textInterpolator = mock(TextInterpolator::class.java)
        val paint = arrayListOf(mock(Paint::class.java))
        val paint = arrayListOf(mock(TextPaint::class.java))
        `when`(textInterpolator.targetPaint).thenReturn(paint)

        val textAnimator = TextAnimator(layout, {}).apply {
+58 −11
Original line number Diff line number Diff line
@@ -18,11 +18,12 @@ package com.android.keyguard

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.testing.AndroidTestingRunner
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextDirectionHeuristic
import android.text.TextDirectionHeuristics
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
@@ -31,6 +32,7 @@ import org.junit.runner.RunWith
import kotlin.math.ceil

private const val TEXT = "Hello, World."
private const val BIDI_TEXT = "abc\u05D0\u05D1\u05D2"
private const val BMP_WIDTH = 400
private const val BMP_HEIGHT = 300

@@ -38,11 +40,11 @@ private val PAINT = TextPaint().apply {
    textSize = 32f
}

private val START_PAINT = arrayListOf<Paint>(TextPaint(PAINT).apply {
private val START_PAINT = arrayListOf(TextPaint(PAINT).apply {
    fontVariationSettings = "'wght' 400"
})

private val END_PAINT = arrayListOf<Paint>(TextPaint(PAINT).apply {
private val END_PAINT = arrayListOf(TextPaint(PAINT).apply {
    fontVariationSettings = "'wght' 700"
})

@@ -50,9 +52,14 @@ private val END_PAINT = arrayListOf<Paint>(TextPaint(PAINT).apply {
@SmallTest
class TextInterpolatorTest : SysuiTestCase() {

    private fun makeLayout(text: String, paint: TextPaint): Layout {
    private fun makeLayout(
        text: String,
        paint: TextPaint,
        dir: TextDirectionHeuristic = TextDirectionHeuristics.LTR
    ): Layout {
        val width = ceil(Layout.getDesiredWidth(text, 0, text.length, paint)).toInt()
        return StaticLayout.Builder.obtain(text, 0, text.length, paint, width).build()
        return StaticLayout.Builder.obtain(text, 0, text.length, paint, width)
                .setTextDirection(dir).build()
    }

    @Test
@@ -69,7 +76,7 @@ class TextInterpolatorTest : SysuiTestCase() {
        // Just after created TextInterpolator, it should have 0 progress.
        assertThat(interp.progress).isEqualTo(0f)
        val actual = interp.toBitmap(BMP_WIDTH, BMP_HEIGHT)
        val expected = makeLayout(TEXT, START_PAINT[0] as TextPaint).toBitmap(BMP_WIDTH, BMP_HEIGHT)
        val expected = makeLayout(TEXT, START_PAINT[0]).toBitmap(BMP_WIDTH, BMP_HEIGHT)

        assertThat(expected.sameAs(actual)).isTrue()
    }
@@ -87,7 +94,7 @@ class TextInterpolatorTest : SysuiTestCase() {

        interp.progress = 1f
        val actual = interp.toBitmap(BMP_WIDTH, BMP_HEIGHT)
        val expected = makeLayout(TEXT, END_PAINT[0] as TextPaint).toBitmap(BMP_WIDTH, BMP_HEIGHT)
        val expected = makeLayout(TEXT, END_PAINT[0]).toBitmap(BMP_WIDTH, BMP_HEIGHT)

        assertThat(expected.sameAs(actual)).isTrue()
    }
@@ -108,9 +115,9 @@ class TextInterpolatorTest : SysuiTestCase() {
        // end state.
        interp.progress = 0.5f
        val actual = interp.toBitmap(BMP_WIDTH, BMP_HEIGHT)
        assertThat(actual.sameAs(makeLayout(TEXT, START_PAINT[0] as TextPaint)
        assertThat(actual.sameAs(makeLayout(TEXT, START_PAINT[0])
            .toBitmap(BMP_WIDTH, BMP_HEIGHT))).isFalse()
        assertThat(actual.sameAs(makeLayout(TEXT, END_PAINT[0] as TextPaint)
        assertThat(actual.sameAs(makeLayout(TEXT, END_PAINT[0])
            .toBitmap(BMP_WIDTH, BMP_HEIGHT))).isFalse()
    }

@@ -135,10 +142,50 @@ class TextInterpolatorTest : SysuiTestCase() {

        assertThat(expected.sameAs(actual)).isTrue()
    }

    @Test
    fun testBidi_LTR() {
        val layout = makeLayout(BIDI_TEXT, PAINT, TextDirectionHeuristics.LTR)

        val interp = TextInterpolator(layout)
        TextInterpolator.updatePaint(interp.basePaint, START_PAINT)
        interp.onBasePaintModified()

        TextInterpolator.updatePaint(interp.targetPaint, END_PAINT)
        interp.onTargetPaintModified()

        // Just after created TextInterpolator, it should have 0 progress.
        assertThat(interp.progress).isEqualTo(0f)
        val actual = interp.toBitmap(BMP_WIDTH, BMP_HEIGHT)
        val expected = makeLayout(BIDI_TEXT, START_PAINT[0], TextDirectionHeuristics.LTR)
                .toBitmap(BMP_WIDTH, BMP_HEIGHT)

        assertThat(expected.sameAs(actual)).isTrue()
    }

    @Test
    fun testBidi_RTL() {
        val layout = makeLayout(BIDI_TEXT, PAINT, TextDirectionHeuristics.RTL)

        val interp = TextInterpolator(layout)
        TextInterpolator.updatePaint(interp.basePaint, START_PAINT)
        interp.onBasePaintModified()

        TextInterpolator.updatePaint(interp.targetPaint, END_PAINT)
        interp.onTargetPaintModified()

        // Just after created TextInterpolator, it should have 0 progress.
        assertThat(interp.progress).isEqualTo(0f)
        val actual = interp.toBitmap(BMP_WIDTH, BMP_HEIGHT)
        val expected = makeLayout(BIDI_TEXT, START_PAINT[0], TextDirectionHeuristics.RTL)
                .toBitmap(BMP_WIDTH, BMP_HEIGHT)

        assertThat(expected.sameAs(actual)).isTrue()
    }
}

private fun Layout.toBitmap(width: Int, height: Int) =
        Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8).also { draw(Canvas(it)) }!!
        Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { draw(Canvas(it)) }!!

private fun TextInterpolator.toBitmap(width: Int, height: Int) =
        Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8).also { draw(Canvas(it)) }
 No newline at end of file
        Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { draw(Canvas(it)) }
 No newline at end of file