Loading packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +149 −58 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.graphics.fonts.Font import android.graphics.fonts.FontVariationAxis import android.graphics.text.PositionedGlyphs import android.text.Layout import android.text.TextDirectionHeuristic import android.text.TextPaint import android.text.TextShaper import android.util.MathUtils Loading @@ -28,18 +29,33 @@ import com.android.internal.graphics.ColorUtils import java.lang.Math.max interface TextInterpolatorListener { fun onPaintModified() {} fun onPaintModified(paint: Paint) {} fun onRebased() {} fun onRebased(progress: Float) {} fun getCharWidthAdjustment(font: Font, char: Char, width: Float): Float = 0f fun onTotalAdjustmentComputed( paint: Paint, lineAdvance: Float, totalAdjustment: Float, ): Boolean = false } class ShapingResult( val text: String, val lines: List<List<ShapingRun>>, val textDirectionHeuristic: TextDirectionHeuristic, ) class ShapingRun(val text: String, val glyphs: PositionedGlyphs) /** Provide text style linear interpolation for plain text. */ class TextInterpolator( layout: Layout, var typefaceCache: TypefaceVariantCache, private val listener: TextInterpolatorListener? = null, ) { /** * Returns base paint used for interpolation. * Loading Loading @@ -147,7 +163,7 @@ class TextInterpolator( */ fun onTargetPaintModified() { updatePositionsAndFonts(shapeText(layout, targetPaint), updateBase = false) listener?.onPaintModified() listener?.onPaintModified(targetPaint) } /** Loading @@ -158,7 +174,7 @@ class TextInterpolator( */ fun onBasePaintModified() { updatePositionsAndFonts(shapeText(layout, basePaint), updateBase = true) listener?.onPaintModified() listener?.onPaintModified(basePaint) } /** Loading Loading @@ -217,7 +233,7 @@ class TextInterpolator( */ fun rebase() { if (progress == 0f) { listener?.onRebased() listener?.onRebased(progress) return } else if (progress == 1f) { basePaint.set(targetPaint) Loading Loading @@ -246,9 +262,9 @@ class TextInterpolator( } } progress = 0f listener?.onRebased(progress) linearProgress = 0f listener?.onRebased() progress = 0f } /** Loading Loading @@ -281,50 +297,67 @@ class TextInterpolator( val baseLayout = shapeText(layout, basePaint) val targetLayout = shapeText(layout, targetPaint) require(baseLayout.size == targetLayout.size) { require(baseLayout.lines.size == targetLayout.lines.size) { "The new layout result has different line count." } var maxRunLength = 0 lines = baseLayout.zip(targetLayout) { baseLine, targetLine -> baseLayout.lines.zip(targetLayout.lines) { baseLine, targetLine -> val runs = baseLine.zip(targetLine) { base, target -> require(base.glyphCount() == target.glyphCount()) { require(base.glyphs.glyphCount() == target.glyphs.glyphCount()) { "Inconsistent glyph count at line ${lines.size}" } val glyphCount = base.glyphCount() val glyphCount = base.glyphs.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)) { base.glyphs.getGlyphId(it).also { baseGlyphId -> require(baseGlyphId == target.glyphs.getGlyphId(it)) { "Inconsistent glyph ID at $it in line ${lines.size}" } } } val baseX = FloatArray(glyphCount) { base.getGlyphX(it) } val baseY = FloatArray(glyphCount) { base.getGlyphY(it) } val targetX = FloatArray(glyphCount) { target.getGlyphX(it) } val targetY = FloatArray(glyphCount) { target.getGlyphY(it) } val baseX = FloatArray(glyphCount) val baseY = FloatArray(glyphCount) populateGlyphPositions( basePaint, baseLayout.textDirectionHeuristic, base.glyphs, base.text, baseX, baseY, ) val targetX = FloatArray(glyphCount) val targetY = FloatArray(glyphCount) populateGlyphPositions( targetPaint, targetLayout.textDirectionHeuristic, target.glyphs, target.text, targetX, targetY, ) // Calculate font runs val fontRun = mutableListOf<FontRun>() if (glyphCount != 0) { var start = 0 var baseFont = base.getFont(start) var targetFont = target.getFont(start) var baseFont = base.glyphs.getFont(start) var targetFont = target.glyphs.getFont(start) require(FontInterpolator.canInterpolate(baseFont, targetFont)) { "Cannot interpolate font at $start ($baseFont vs $targetFont)" } for (i in 1 until glyphCount) { val nextBaseFont = base.getFont(i) val nextTargetFont = target.getFont(i) val nextBaseFont = base.glyphs.getFont(i) val nextTargetFont = target.glyphs.getFont(i) if (baseFont !== nextBaseFont) { require(targetFont !== nextTargetFont) { Loading Loading @@ -446,28 +479,29 @@ class TextInterpolator( ) } private fun updatePositionsAndFonts( layoutResult: List<List<PositionedGlyphs>>, updateBase: Boolean, ) { private fun updatePositionsAndFonts(layoutResult: ShapingResult, 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.lines.size == lines.size) { "The new layout result has different line count." } lines.zip(layoutResult) { line, runs -> line.runs.zip(runs) { lineRun, newGlyphs -> require(newGlyphs.glyphCount() == lineRun.glyphIds.size) { lines.zip(layoutResult.lines) { line, runs -> line.runs.zip(runs) { lineRun, newRun -> require(newRun.glyphs.glyphCount() == lineRun.glyphIds.size) { "The new layout has different glyph count." } lineRun.fontRuns.forEach { run -> val newFont = newGlyphs.getFont(run.start) val newFont = newRun.glyphs.getFont(run.start) for (i in run.start until run.end) { require(newGlyphs.getGlyphId(run.start) == lineRun.glyphIds[run.start]) { require( newRun.glyphs.getGlyphId(run.start) == lineRun.glyphIds[run.start] ) { "The new layout has different glyph ID at ${run.start}" } require(newFont === newGlyphs.getFont(i)) { require(newFont === newRun.glyphs.getFont(i)) { "The new layout has different font run." + " $newFont vs ${newGlyphs.getFont(i)} at $i" " $newFont vs ${newRun.glyphs.getFont(i)} at $i" } } Loading @@ -486,15 +520,23 @@ class TextInterpolator( } if (updateBase) { for (i in lineRun.baseX.indices) { lineRun.baseX[i] = newGlyphs.getGlyphX(i) lineRun.baseY[i] = newGlyphs.getGlyphY(i) } populateGlyphPositions( basePaint, layoutResult.textDirectionHeuristic, newRun.glyphs, newRun.text, lineRun.baseX, lineRun.baseY, ) } else { for (i in lineRun.baseX.indices) { lineRun.targetX[i] = newGlyphs.getGlyphX(i) lineRun.targetY[i] = newGlyphs.getGlyphY(i) } populateGlyphPositions( targetPaint, layoutResult.textDirectionHeuristic, newRun.glyphs, newRun.text, lineRun.targetX, lineRun.targetY, ) } } } Loading @@ -512,9 +554,9 @@ class TextInterpolator( } // 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): ShapingResult { var text = StringBuilder() val out = mutableListOf<List<PositionedGlyphs>>() val lines = mutableListOf<List<ShapingRun>>() for (lineNo in 0 until layout.lineCount) { // Shape all lines. val lineStart = layout.getLineStart(lineNo) val lineEnd = layout.getLineEnd(lineNo) Loading @@ -525,31 +567,80 @@ class TextInterpolator( count-- } val runs = mutableListOf<PositionedGlyphs>() val runs = mutableListOf<ShapingRun>() TextShaper.shapeText( layout.text, lineStart, count, layout.textDirectionHeuristic, paint, ) { _, _, glyphs, _ -> runs.add(glyphs) ) { start, count, glyphs, _ -> runs.add(ShapingRun(layout.text.substring(start, start + count), glyphs)) } out.add(runs) lines.add(runs) if (lineNo > 0) { text.append("\n") } if (lineNo > 0) text.append("\n") text.append(layout.text.substring(lineStart, lineEnd)) } shapedText = text.toString() return out return ShapingResult(shapedText, lines, layout.textDirectionHeuristic) } private fun populateGlyphPositions( paint: Paint, textDirectionHeuristic: TextDirectionHeuristic, glyphs: PositionedGlyphs, str: String, outX: FloatArray, outY: FloatArray, ) { val isRtl = textDirectionHeuristic.isRtl(str, 0, str.length) val range = (0 until glyphs.glyphCount()).let { if (isRtl) it.reversed() else it } val sign = if (isRtl) -1 else 1 var xAdjustment = 0f for (i in range) { val xPos = glyphs.getGlyphX(i) outX[i] = xPos + xAdjustment * sign outY[i] = glyphs.getGlyphY(i) // Characters are left-aligned so any modifications to width only effect the positioning // of later characters. As a result, all we need to do is track a cumulative total. The // last character is skipped as the view bounds don't include it's trailing spacing. if (i != range.last()) { val font = glyphs.getFont(i) val nextXPos = when { i + 1 < glyphs.glyphCount() -> glyphs.getGlyphX(i + 1) !isRtl -> glyphs.getAdvance() else -> 0f } xAdjustment += listener?.getCharWidthAdjustment(font, str[i], nextXPos - xPos) ?: 0f } } private fun Layout.getDrawOrigin(lineNo: Int) = val boundsUpdated = listener?.onTotalAdjustmentComputed(paint, glyphs.getAdvance(), xAdjustment) ?: false // RTL glyph positions are relative to zero on the right side, but do not invert the x axis. // and as a result are negative. They are still however drawn relative to the left side of // the view. This means when we shrink the view, they'll end up mispositioned unless we // account for the total adjustment and update each glyph position. For some reason that // isn't clear this misalginment is only present in production and not in robolectric tests. if (isRtl && boundsUpdated) { for (i in range) { outX[i] -= xAdjustment } } } companion object { private fun Layout.getDrawOrigin(lineNo: Int): Float { if (getParagraphDirection(lineNo) == Layout.DIR_LEFT_TO_RIGHT) { getLineLeft(lineNo) return getLineLeft(lineNo) } else { getLineRight(lineNo) return getLineRight(lineNo) } } } } packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +72 −13 Original line number Diff line number Diff line Loading @@ -23,11 +23,13 @@ import android.graphics.Paint import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.Rect import android.graphics.fonts.Font import android.os.VibrationEffect import android.text.TextPaint import android.util.AttributeSet import android.util.Log import android.util.MathUtils.lerp import android.util.MathUtils.lerpInvSat import android.util.TypedValue import android.view.View import android.view.View.MeasureSpec.EXACTLY Loading Loading @@ -59,6 +61,7 @@ import com.android.systemui.shared.clocks.DimensionParser import com.android.systemui.shared.clocks.FLEX_CLOCK_ID import com.android.systemui.shared.clocks.FontTextStyle import java.lang.Thread import kotlin.math.abs import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt Loading @@ -72,6 +75,10 @@ private fun Paint.getTextBounds(text: CharSequence): VRectF { return VRectF(tempRect) } private fun nearEqual(a: Float, b: Float, tolerance: Float): Boolean { return abs(a - b) < tolerance } enum class VerticalAlignment { TOP, BOTTOM, Loading Loading @@ -112,7 +119,7 @@ enum class XAlignment { @SuppressLint("AppCompatCustomView") open class SimpleDigitalClockTextView( val clockCtx: ClockContext, isLargeClock: Boolean, val isLargeClock: Boolean, attrs: AttributeSet? = null, ) : TextView(clockCtx.context, attrs) { val lockScreenPaint = TextPaint() Loading Loading @@ -198,6 +205,53 @@ open class SimpleDigitalClockTextView( var measuredBaseline = 0 var lockscreenColor = Color.WHITE var aodColor = Color.WHITE var baseWidthAdjustment = 0f var targetWidthAdjustment = 0f private val animatorListener = object : TextAnimatorListener { override fun onInvalidate() = invalidate() override fun onRebased(progress: Float) { baseWidthAdjustment = lerp(baseWidthAdjustment, targetWidthAdjustment, progress) updateAnimationTextBounds() } override fun onPaintModified(paint: Paint) { updateAnimationTextBounds() } override fun getCharWidthAdjustment(font: Font, char: Char, width: Float): Float { if (isLargeClock) return 0f val charMult = SPACING_ADJUSTMENT_GLYPH_MAP.get(char) ?: 1f val wdth = font.axes?.firstOrNull { it.tag == GSFAxes.WIDTH.tag }?.styleValue ?: 0f return width * SPACING_BASE_ADJUSTMENT * charMult * lerpInvSat(30f, 120f, wdth) } override fun onTotalAdjustmentComputed( paint: Paint, lineAdvance: Float, totalAdjustment: Float, ): Boolean { val isBasePaint = paint == textAnimator.textInterpolator.basePaint if (isBasePaint) { if (!nearEqual(baseWidthAdjustment, totalAdjustment, 0.1f)) { baseWidthAdjustment = totalAdjustment updateAnimationTextBounds() } } else { if (!nearEqual(targetWidthAdjustment, totalAdjustment, 0.1f)) { targetWidthAdjustment = totalAdjustment updateAnimationTextBounds() } } // If animation is disabled, then we don't want to adjust the glyph positions with // updated bounds as in the robolectric test environment we don't see the same // misalignment of RTL glyphs from the view bounds as we do in production. return isAnimationEnabled } } fun updateColor(lockscreenColor: Int, aodColor: Int = Color.WHITE) { this.lockscreenColor = lockscreenColor Loading Loading @@ -269,18 +323,7 @@ open class SimpleDigitalClockTextView( val layout = this.layout if (layout != null) { if (!this::textAnimator.isInitialized) { textAnimator = TextAnimator( layout, typefaceCache, object : TextAnimatorListener { override fun onInvalidate() = invalidate() override fun onRebased() = updateAnimationTextBounds() override fun onPaintModified() = updateAnimationTextBounds() }, ) textAnimator = TextAnimator(layout, typefaceCache, animatorListener) setInterpolatorPaint() } else { textAnimator.updateLayout(layout) Loading Loading @@ -649,6 +692,15 @@ open class SimpleDigitalClockTextView( updateAnimationTextBounds() } private fun adjustSpacingBounds(rect: VRectF, adjustment: Float): VRectF { return VRectF( top = rect.top, bottom = rect.bottom, left = rect.left - if (isLayoutRtl()) adjustment else 0f, right = rect.right + if (isLayoutRtl()) 0f else adjustment, ) } /** * Called after textAnimator.setTextStyle textAnimator.setTextStyle will update targetPaint, and * rebase if previous animator is canceled so basePaint will store the state we transition from Loading @@ -663,6 +715,9 @@ open class SimpleDigitalClockTextView( prevTextBounds = textBounds targetTextBounds = textBounds } prevTextBounds = adjustSpacingBounds(prevTextBounds, baseWidthAdjustment) targetTextBounds = adjustSpacingBounds(targetTextBounds, targetWidthAdjustment) } /** Loading Loading @@ -725,6 +780,10 @@ open class SimpleDigitalClockTextView( private val FLEX_AOD_WIDTH_AXIS = GSFAxes.WIDTH to 43f private val FLEX_ROUND_AXIS = GSFAxes.ROUND to 100f // Multipliers for glyphs that need specific spacing adjustment private val SPACING_ADJUSTMENT_GLYPH_MAP = mapOf(':' to 2.5f, '1' to 3.0f) private val SPACING_BASE_ADJUSTMENT = -0.08f private fun fromAxes(vararg axes: Pair<AxisDefinition, Float>): ClockAxisStyle { return ClockAxisStyle(axes.map { (def, value) -> def.tag to value }.toMap()) } Loading packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +1 −0 Original line number Diff line number Diff line Loading @@ -284,6 +284,7 @@ constructor( // Font axes width max cutoff // A font with a wider font axes than this is at risk of being pushed off screen // Value determined by the very robust and scientific process of eye-balling a few devices private const val FONT_WIDTH_MAX_CUTOFF = 110 } } Loading
packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +149 −58 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.graphics.fonts.Font import android.graphics.fonts.FontVariationAxis import android.graphics.text.PositionedGlyphs import android.text.Layout import android.text.TextDirectionHeuristic import android.text.TextPaint import android.text.TextShaper import android.util.MathUtils Loading @@ -28,18 +29,33 @@ import com.android.internal.graphics.ColorUtils import java.lang.Math.max interface TextInterpolatorListener { fun onPaintModified() {} fun onPaintModified(paint: Paint) {} fun onRebased() {} fun onRebased(progress: Float) {} fun getCharWidthAdjustment(font: Font, char: Char, width: Float): Float = 0f fun onTotalAdjustmentComputed( paint: Paint, lineAdvance: Float, totalAdjustment: Float, ): Boolean = false } class ShapingResult( val text: String, val lines: List<List<ShapingRun>>, val textDirectionHeuristic: TextDirectionHeuristic, ) class ShapingRun(val text: String, val glyphs: PositionedGlyphs) /** Provide text style linear interpolation for plain text. */ class TextInterpolator( layout: Layout, var typefaceCache: TypefaceVariantCache, private val listener: TextInterpolatorListener? = null, ) { /** * Returns base paint used for interpolation. * Loading Loading @@ -147,7 +163,7 @@ class TextInterpolator( */ fun onTargetPaintModified() { updatePositionsAndFonts(shapeText(layout, targetPaint), updateBase = false) listener?.onPaintModified() listener?.onPaintModified(targetPaint) } /** Loading @@ -158,7 +174,7 @@ class TextInterpolator( */ fun onBasePaintModified() { updatePositionsAndFonts(shapeText(layout, basePaint), updateBase = true) listener?.onPaintModified() listener?.onPaintModified(basePaint) } /** Loading Loading @@ -217,7 +233,7 @@ class TextInterpolator( */ fun rebase() { if (progress == 0f) { listener?.onRebased() listener?.onRebased(progress) return } else if (progress == 1f) { basePaint.set(targetPaint) Loading Loading @@ -246,9 +262,9 @@ class TextInterpolator( } } progress = 0f listener?.onRebased(progress) linearProgress = 0f listener?.onRebased() progress = 0f } /** Loading Loading @@ -281,50 +297,67 @@ class TextInterpolator( val baseLayout = shapeText(layout, basePaint) val targetLayout = shapeText(layout, targetPaint) require(baseLayout.size == targetLayout.size) { require(baseLayout.lines.size == targetLayout.lines.size) { "The new layout result has different line count." } var maxRunLength = 0 lines = baseLayout.zip(targetLayout) { baseLine, targetLine -> baseLayout.lines.zip(targetLayout.lines) { baseLine, targetLine -> val runs = baseLine.zip(targetLine) { base, target -> require(base.glyphCount() == target.glyphCount()) { require(base.glyphs.glyphCount() == target.glyphs.glyphCount()) { "Inconsistent glyph count at line ${lines.size}" } val glyphCount = base.glyphCount() val glyphCount = base.glyphs.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)) { base.glyphs.getGlyphId(it).also { baseGlyphId -> require(baseGlyphId == target.glyphs.getGlyphId(it)) { "Inconsistent glyph ID at $it in line ${lines.size}" } } } val baseX = FloatArray(glyphCount) { base.getGlyphX(it) } val baseY = FloatArray(glyphCount) { base.getGlyphY(it) } val targetX = FloatArray(glyphCount) { target.getGlyphX(it) } val targetY = FloatArray(glyphCount) { target.getGlyphY(it) } val baseX = FloatArray(glyphCount) val baseY = FloatArray(glyphCount) populateGlyphPositions( basePaint, baseLayout.textDirectionHeuristic, base.glyphs, base.text, baseX, baseY, ) val targetX = FloatArray(glyphCount) val targetY = FloatArray(glyphCount) populateGlyphPositions( targetPaint, targetLayout.textDirectionHeuristic, target.glyphs, target.text, targetX, targetY, ) // Calculate font runs val fontRun = mutableListOf<FontRun>() if (glyphCount != 0) { var start = 0 var baseFont = base.getFont(start) var targetFont = target.getFont(start) var baseFont = base.glyphs.getFont(start) var targetFont = target.glyphs.getFont(start) require(FontInterpolator.canInterpolate(baseFont, targetFont)) { "Cannot interpolate font at $start ($baseFont vs $targetFont)" } for (i in 1 until glyphCount) { val nextBaseFont = base.getFont(i) val nextTargetFont = target.getFont(i) val nextBaseFont = base.glyphs.getFont(i) val nextTargetFont = target.glyphs.getFont(i) if (baseFont !== nextBaseFont) { require(targetFont !== nextTargetFont) { Loading Loading @@ -446,28 +479,29 @@ class TextInterpolator( ) } private fun updatePositionsAndFonts( layoutResult: List<List<PositionedGlyphs>>, updateBase: Boolean, ) { private fun updatePositionsAndFonts(layoutResult: ShapingResult, 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.lines.size == lines.size) { "The new layout result has different line count." } lines.zip(layoutResult) { line, runs -> line.runs.zip(runs) { lineRun, newGlyphs -> require(newGlyphs.glyphCount() == lineRun.glyphIds.size) { lines.zip(layoutResult.lines) { line, runs -> line.runs.zip(runs) { lineRun, newRun -> require(newRun.glyphs.glyphCount() == lineRun.glyphIds.size) { "The new layout has different glyph count." } lineRun.fontRuns.forEach { run -> val newFont = newGlyphs.getFont(run.start) val newFont = newRun.glyphs.getFont(run.start) for (i in run.start until run.end) { require(newGlyphs.getGlyphId(run.start) == lineRun.glyphIds[run.start]) { require( newRun.glyphs.getGlyphId(run.start) == lineRun.glyphIds[run.start] ) { "The new layout has different glyph ID at ${run.start}" } require(newFont === newGlyphs.getFont(i)) { require(newFont === newRun.glyphs.getFont(i)) { "The new layout has different font run." + " $newFont vs ${newGlyphs.getFont(i)} at $i" " $newFont vs ${newRun.glyphs.getFont(i)} at $i" } } Loading @@ -486,15 +520,23 @@ class TextInterpolator( } if (updateBase) { for (i in lineRun.baseX.indices) { lineRun.baseX[i] = newGlyphs.getGlyphX(i) lineRun.baseY[i] = newGlyphs.getGlyphY(i) } populateGlyphPositions( basePaint, layoutResult.textDirectionHeuristic, newRun.glyphs, newRun.text, lineRun.baseX, lineRun.baseY, ) } else { for (i in lineRun.baseX.indices) { lineRun.targetX[i] = newGlyphs.getGlyphX(i) lineRun.targetY[i] = newGlyphs.getGlyphY(i) } populateGlyphPositions( targetPaint, layoutResult.textDirectionHeuristic, newRun.glyphs, newRun.text, lineRun.targetX, lineRun.targetY, ) } } } Loading @@ -512,9 +554,9 @@ class TextInterpolator( } // 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): ShapingResult { var text = StringBuilder() val out = mutableListOf<List<PositionedGlyphs>>() val lines = mutableListOf<List<ShapingRun>>() for (lineNo in 0 until layout.lineCount) { // Shape all lines. val lineStart = layout.getLineStart(lineNo) val lineEnd = layout.getLineEnd(lineNo) Loading @@ -525,31 +567,80 @@ class TextInterpolator( count-- } val runs = mutableListOf<PositionedGlyphs>() val runs = mutableListOf<ShapingRun>() TextShaper.shapeText( layout.text, lineStart, count, layout.textDirectionHeuristic, paint, ) { _, _, glyphs, _ -> runs.add(glyphs) ) { start, count, glyphs, _ -> runs.add(ShapingRun(layout.text.substring(start, start + count), glyphs)) } out.add(runs) lines.add(runs) if (lineNo > 0) { text.append("\n") } if (lineNo > 0) text.append("\n") text.append(layout.text.substring(lineStart, lineEnd)) } shapedText = text.toString() return out return ShapingResult(shapedText, lines, layout.textDirectionHeuristic) } private fun populateGlyphPositions( paint: Paint, textDirectionHeuristic: TextDirectionHeuristic, glyphs: PositionedGlyphs, str: String, outX: FloatArray, outY: FloatArray, ) { val isRtl = textDirectionHeuristic.isRtl(str, 0, str.length) val range = (0 until glyphs.glyphCount()).let { if (isRtl) it.reversed() else it } val sign = if (isRtl) -1 else 1 var xAdjustment = 0f for (i in range) { val xPos = glyphs.getGlyphX(i) outX[i] = xPos + xAdjustment * sign outY[i] = glyphs.getGlyphY(i) // Characters are left-aligned so any modifications to width only effect the positioning // of later characters. As a result, all we need to do is track a cumulative total. The // last character is skipped as the view bounds don't include it's trailing spacing. if (i != range.last()) { val font = glyphs.getFont(i) val nextXPos = when { i + 1 < glyphs.glyphCount() -> glyphs.getGlyphX(i + 1) !isRtl -> glyphs.getAdvance() else -> 0f } xAdjustment += listener?.getCharWidthAdjustment(font, str[i], nextXPos - xPos) ?: 0f } } private fun Layout.getDrawOrigin(lineNo: Int) = val boundsUpdated = listener?.onTotalAdjustmentComputed(paint, glyphs.getAdvance(), xAdjustment) ?: false // RTL glyph positions are relative to zero on the right side, but do not invert the x axis. // and as a result are negative. They are still however drawn relative to the left side of // the view. This means when we shrink the view, they'll end up mispositioned unless we // account for the total adjustment and update each glyph position. For some reason that // isn't clear this misalginment is only present in production and not in robolectric tests. if (isRtl && boundsUpdated) { for (i in range) { outX[i] -= xAdjustment } } } companion object { private fun Layout.getDrawOrigin(lineNo: Int): Float { if (getParagraphDirection(lineNo) == Layout.DIR_LEFT_TO_RIGHT) { getLineLeft(lineNo) return getLineLeft(lineNo) } else { getLineRight(lineNo) return getLineRight(lineNo) } } } }
packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +72 −13 Original line number Diff line number Diff line Loading @@ -23,11 +23,13 @@ import android.graphics.Paint import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.Rect import android.graphics.fonts.Font import android.os.VibrationEffect import android.text.TextPaint import android.util.AttributeSet import android.util.Log import android.util.MathUtils.lerp import android.util.MathUtils.lerpInvSat import android.util.TypedValue import android.view.View import android.view.View.MeasureSpec.EXACTLY Loading Loading @@ -59,6 +61,7 @@ import com.android.systemui.shared.clocks.DimensionParser import com.android.systemui.shared.clocks.FLEX_CLOCK_ID import com.android.systemui.shared.clocks.FontTextStyle import java.lang.Thread import kotlin.math.abs import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt Loading @@ -72,6 +75,10 @@ private fun Paint.getTextBounds(text: CharSequence): VRectF { return VRectF(tempRect) } private fun nearEqual(a: Float, b: Float, tolerance: Float): Boolean { return abs(a - b) < tolerance } enum class VerticalAlignment { TOP, BOTTOM, Loading Loading @@ -112,7 +119,7 @@ enum class XAlignment { @SuppressLint("AppCompatCustomView") open class SimpleDigitalClockTextView( val clockCtx: ClockContext, isLargeClock: Boolean, val isLargeClock: Boolean, attrs: AttributeSet? = null, ) : TextView(clockCtx.context, attrs) { val lockScreenPaint = TextPaint() Loading Loading @@ -198,6 +205,53 @@ open class SimpleDigitalClockTextView( var measuredBaseline = 0 var lockscreenColor = Color.WHITE var aodColor = Color.WHITE var baseWidthAdjustment = 0f var targetWidthAdjustment = 0f private val animatorListener = object : TextAnimatorListener { override fun onInvalidate() = invalidate() override fun onRebased(progress: Float) { baseWidthAdjustment = lerp(baseWidthAdjustment, targetWidthAdjustment, progress) updateAnimationTextBounds() } override fun onPaintModified(paint: Paint) { updateAnimationTextBounds() } override fun getCharWidthAdjustment(font: Font, char: Char, width: Float): Float { if (isLargeClock) return 0f val charMult = SPACING_ADJUSTMENT_GLYPH_MAP.get(char) ?: 1f val wdth = font.axes?.firstOrNull { it.tag == GSFAxes.WIDTH.tag }?.styleValue ?: 0f return width * SPACING_BASE_ADJUSTMENT * charMult * lerpInvSat(30f, 120f, wdth) } override fun onTotalAdjustmentComputed( paint: Paint, lineAdvance: Float, totalAdjustment: Float, ): Boolean { val isBasePaint = paint == textAnimator.textInterpolator.basePaint if (isBasePaint) { if (!nearEqual(baseWidthAdjustment, totalAdjustment, 0.1f)) { baseWidthAdjustment = totalAdjustment updateAnimationTextBounds() } } else { if (!nearEqual(targetWidthAdjustment, totalAdjustment, 0.1f)) { targetWidthAdjustment = totalAdjustment updateAnimationTextBounds() } } // If animation is disabled, then we don't want to adjust the glyph positions with // updated bounds as in the robolectric test environment we don't see the same // misalignment of RTL glyphs from the view bounds as we do in production. return isAnimationEnabled } } fun updateColor(lockscreenColor: Int, aodColor: Int = Color.WHITE) { this.lockscreenColor = lockscreenColor Loading Loading @@ -269,18 +323,7 @@ open class SimpleDigitalClockTextView( val layout = this.layout if (layout != null) { if (!this::textAnimator.isInitialized) { textAnimator = TextAnimator( layout, typefaceCache, object : TextAnimatorListener { override fun onInvalidate() = invalidate() override fun onRebased() = updateAnimationTextBounds() override fun onPaintModified() = updateAnimationTextBounds() }, ) textAnimator = TextAnimator(layout, typefaceCache, animatorListener) setInterpolatorPaint() } else { textAnimator.updateLayout(layout) Loading Loading @@ -649,6 +692,15 @@ open class SimpleDigitalClockTextView( updateAnimationTextBounds() } private fun adjustSpacingBounds(rect: VRectF, adjustment: Float): VRectF { return VRectF( top = rect.top, bottom = rect.bottom, left = rect.left - if (isLayoutRtl()) adjustment else 0f, right = rect.right + if (isLayoutRtl()) 0f else adjustment, ) } /** * Called after textAnimator.setTextStyle textAnimator.setTextStyle will update targetPaint, and * rebase if previous animator is canceled so basePaint will store the state we transition from Loading @@ -663,6 +715,9 @@ open class SimpleDigitalClockTextView( prevTextBounds = textBounds targetTextBounds = textBounds } prevTextBounds = adjustSpacingBounds(prevTextBounds, baseWidthAdjustment) targetTextBounds = adjustSpacingBounds(targetTextBounds, targetWidthAdjustment) } /** Loading Loading @@ -725,6 +780,10 @@ open class SimpleDigitalClockTextView( private val FLEX_AOD_WIDTH_AXIS = GSFAxes.WIDTH to 43f private val FLEX_ROUND_AXIS = GSFAxes.ROUND to 100f // Multipliers for glyphs that need specific spacing adjustment private val SPACING_ADJUSTMENT_GLYPH_MAP = mapOf(':' to 2.5f, '1' to 3.0f) private val SPACING_BASE_ADJUSTMENT = -0.08f private fun fromAxes(vararg axes: Pair<AxisDefinition, Float>): ClockAxisStyle { return ClockAxisStyle(axes.map { (def, value) -> def.tag to value }.toMap()) } Loading
packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +1 −0 Original line number Diff line number Diff line Loading @@ -284,6 +284,7 @@ constructor( // Font axes width max cutoff // A font with a wider font axes than this is at risk of being pushed off screen // Value determined by the very robust and scientific process of eye-balling a few devices private const val FONT_WIDTH_MAX_CUTOFF = 110 } }