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

Commit bbff6dae authored by chenjean's avatar chenjean
Browse files

fix(HCT): White Outline with white text when text following an emoji

Goal: Ensure the accurate rendering of the background rectangle color(e.g. white text with black background) for text following an emoji, irrespective of any intervening whitespace.

Root Cause:
1. The text color is updated during the emoji character processing in determineContrastingBackgroundColor, but the bgPaint update is skipped due to the 'skip draw background on emoji' condition in onCharacterBounds. Consequently, subsequent text color change checks return "no text color change", resulting in the return of an incorrect bgPaint color.
2. The first text character immediately following an emoji being affected by the emoji's transparency, producing an incorrect background color. (This issue does not occur with 'Emoji + space + text' patterns.)
3. Emojis composed of two characters cause the second character to be incorrectly identified as non-emoji, triggering premature text color updates.

Solution:
1. Implement synchronized updates for both the text color and bgPaint.
2. Update SpanColors.getColorAt() to match Spanned.SPAN_INCLUSIVE_EXCLUSIVE.
3. In emoji processing, consider only the first character for emoji identification.

Bug: 401070918
Flag: com.android.graphics.hwui.flags.high_contrast_text_small_text_rect
Test: manually test on emoji case (e.g. "emoji + space + text", "emoji + text")
Test: atest core/tests/coretests/src/android/text/LayoutTest.java
Test: atest core/tests/coretests/src/android/text/SpanColorsTest.java
Test: atest cts/tests/tests/uirendering/src/android/uirendering/cts/testclasses/TextViewHighContrastTextTests.kt
Change-Id: I096b2e026d6963e8e574e2ae26d9ac5abfc926e7
parent 82a0eeda
Loading
Loading
Loading
Loading
+8 −5
Original line number Diff line number Diff line
@@ -1066,6 +1066,7 @@ public abstract class Layout {
                lastLine,
                new CharacterBoundsListener() {
                    int mLastLineNum = -1;
                    int mNumCharactersToSkip = 0;
                    final RectF mLineBackground = new RectF();

                    @ColorInt int mLastColor = originalTextColor;
@@ -1073,16 +1074,14 @@ public abstract class Layout {
                    @Override
                    public void onCharacterBounds(int index, int lineNum, float left, float top,
                            float right, float bottom) {

                        // Skip processing if the character is a space or a tap to avoid
                        // rendering an abrupt, empty rectangle.
                        if (TextLine.isLineEndSpace(mText.charAt(index))) {
                        if (TextLine.isLineEndSpace(mText.charAt(index))
                                || mNumCharactersToSkip > 0) {
                            mNumCharactersToSkip--;
                            return;
                        }

                        var newBackground = determineContrastingBackgroundColor(index);
                        var hasBgColorChanged = newBackground != bgPaint.getColor();

                        // To avoid highlighting emoji sequences, we use Extended_Pictgraphs as a
                        // heuristic. Highlighting is skipped based on code points, not glyph type
                        // (text vs. color), so emojis with default text presentation are
@@ -1094,9 +1093,13 @@ public abstract class Layout {
                        var isEmoji = Character.isEmojiComponent(codePoint)
                                || Character.isExtendedPictographic(codePoint);
                        if (isEmoji && !isStandardNumber(index)) {
                            mNumCharactersToSkip = Character.charCount(codePoint) - 1;
                            return;
                        }

                        var newBackground = determineContrastingBackgroundColor(index);
                        var hasBgColorChanged = newBackground != bgPaint.getColor();

                        if (lineNum != mLastLineNum || hasBgColorChanged) {
                            // Draw what we have so far, then reset the rect and update its color
                            drawRect();
+1 −1
Original line number Diff line number Diff line
@@ -72,7 +72,7 @@ public class SpanColors {
        mWorkPaint.setColor(finalColor);
        for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
            if ((index >= mCharacterStyleSpanSet.spanStarts[k])
                    && (index <= mCharacterStyleSpanSet.spanEnds[k])) {
                    && (index < mCharacterStyleSpanSet.spanEnds[k])) {
                final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
                span.updateDrawState(mWorkPaint);

+70 −4
Original line number Diff line number Diff line
@@ -948,10 +948,11 @@ public class LayoutTest {

       Text  |   Background
     ========================
        al   |    BW
        a    |    BW
        l    |    WW
        w    |    WW
        ei   |    WW
        \t;  |    WW
        ei\t |    WW
        ;    |    BB
        s    |    BB
        df   |    BB
        s    |    BB
@@ -1012,7 +1013,7 @@ public class LayoutTest {
        expect.that(removeAlpha(backgroundCommands.get(3).paint.getColor()))
                .isEqualTo(Color.WHITE);
        expect.that(removeAlpha(backgroundCommands.get(4).paint.getColor()))
                .isEqualTo(Color.WHITE);
                .isEqualTo(Color.BLACK);
        expect.that(removeAlpha(backgroundCommands.get(5).paint.getColor()))
                .isEqualTo(Color.BLACK);
        expect.that(removeAlpha(backgroundCommands.get(6).paint.getColor()))
@@ -1041,6 +1042,71 @@ public class LayoutTest {
        testSpannableStringAppliesAllColorsCorrectly(spannedText);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testSingleEmoji_drawsSameBackgrounds() {
        SpannableString spannedText = new SpannableString(" 😀 ");
        testSpannableStringAppliesAllColorsCorrectly(spannedText);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testTwoEmojis_drawsSameBackgrounds() {
        SpannableString spannedText = new SpannableString(" 😀😀 ");
        testSpannableStringAppliesAllColorsCorrectly(spannedText);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testSingleTextBetweenEmoji_drawsCorrectBackgroundsOnText() {
        // TODO(b/405847642): Find a way to verify emojis at the beginning and end of a string are
        // rendered without rectangles.
        SpannableString spannedText = new SpannableString("😀!😀");
        testSpannableStringAppliesAllColorsCorrectly(spannedText);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testEmojiWithinText_drawsSameBackgroundswithText() {
        SpannableString spannedText = new SpannableString("Hello😀World");
        testSpannableStringAppliesAllColorsCorrectly(spannedText);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testEmojiWithinTextWithSpace_drawsSameBackgrounds() {
        SpannableString spannedText = new SpannableString("Hello 😀 World");
        testSpannableStringAppliesAllColorsCorrectly(spannedText);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testEmojiAtStart_drawsCorrectBackgroundsOnText() {
        SpannableString spannedText = new SpannableString("😀HelloWorld");
        testSpannableStringAppliesAllColorsCorrectly(spannedText);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testLeadingEmojiWithSpace_drawsCorrectBackgroundsOnText() {
        SpannableString spannedText = new SpannableString("😀 HelloWorld");
        testSpannableStringAppliesAllColorsCorrectly(spannedText);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testEmojiAtEnd_drawsCorrectBackgroundsOnText() {
        SpannableString spannedText = new SpannableString("HelloWorld😀");
        testSpannableStringAppliesAllColorsCorrectly(spannedText);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testEmojiAtEndWithSpace_drawsCorrectBackgroundsOnText() {
        SpannableString spannedText = new SpannableString("HelloWorld 😀");
        testSpannableStringAppliesAllColorsCorrectly(spannedText);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testRoundedRectSize_belowMinimum_usesMinimumValue() {
+99 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.graphics.Color;
import android.graphics.drawable.ShapeDrawable;
import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.annotations.Presubmit;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.text.style.UnderlineSpan;
@@ -41,6 +42,7 @@ public class SpanColorsTest {
    private final TextPaint mWorkPaint = new TextPaint();
    private SpanColors mSpanColors;
    private SpannableString mSpannedText;
    private SpannableString mSpannedTextWithEmoji;

    @Before
    public void setup() {
@@ -55,6 +57,13 @@ public class SpanColorsTest {
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        mSpannedText.setSpan(new ForegroundColorSpan(Color.BLUE), 12, 16,
                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        mSpannedTextWithEmoji = new SpannableString("Hello 🫱🏻‍🫲🏾world!");
        mSpannedTextWithEmoji.setSpan(new ForegroundColorSpan(Color.RED), 0, 5,
                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        mSpannedTextWithEmoji.setSpan(new ForegroundColorSpan(Color.GREEN),
                mSpannedTextWithEmoji.length() - 2, mSpannedTextWithEmoji.length(),
                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    }

    @Test
@@ -72,9 +81,99 @@ public class SpanColorsTest {
    @Test
    public void testMultipleColorSpans() {
        mSpanColors.init(mWorkPaint, mSpannedText, 0, mSpannedText.length());
        assertThat(mSpanColors.getColorAt(0)).isEqualTo(Color.RED);
        assertThat(mSpanColors.getColorAt(1)).isEqualTo(Color.RED);
        assertThat(mSpanColors.getColorAt(2)).isEqualTo(Color.RED);
        assertThat(mSpanColors.getColorAt(3)).isEqualTo(Color.RED);
        assertThat(mSpanColors.getColorAt(4)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(5)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(6)).isEqualTo(Color.GREEN);
        assertThat(mSpanColors.getColorAt(7)).isEqualTo(Color.GREEN);
        assertThat(mSpanColors.getColorAt(8)).isEqualTo(Color.GREEN);
        assertThat(mSpanColors.getColorAt(9)).isEqualTo(Color.GREEN);
        assertThat(mSpanColors.getColorAt(10)).isEqualTo(Color.GREEN);
        assertThat(mSpanColors.getColorAt(11)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(12)).isEqualTo(Color.BLUE);
        assertThat(mSpanColors.getColorAt(13)).isEqualTo(Color.BLUE);
        assertThat(mSpanColors.getColorAt(14)).isEqualTo(Color.BLUE);
        assertThat(mSpanColors.getColorAt(15)).isEqualTo(Color.BLUE);
        assertThat(mSpanColors.getColorAt(16)).isEqualTo(SpanColors.NO_COLOR_FOUND);
    }

    @Test
    public void testSingleColorSpanWithEmoji() {
        mSpanColors.init(mWorkPaint, mSpannedTextWithEmoji, 1, 4);
        assertThat(mSpanColors.getColorAt(3)).isEqualTo(Color.RED);
    }

    @Test
    public void testMultipleColorSpansWithEmoji() {
        mSpanColors.init(mWorkPaint, mSpannedTextWithEmoji, 0, mSpannedText.length());

        assertThat(mSpanColors.getColorAt(0)).isEqualTo(Color.RED);
        assertThat(mSpanColors.getColorAt(1)).isEqualTo(Color.RED);
        assertThat(mSpanColors.getColorAt(2)).isEqualTo(Color.RED);
        assertThat(mSpanColors.getColorAt(3)).isEqualTo(Color.RED);
        assertThat(mSpanColors.getColorAt(4)).isEqualTo(Color.RED);
        assertThat(mSpanColors.getColorAt(5)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(6)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(7)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(8)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(9)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(10)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(11)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(12)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(13)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(14)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(15)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(16)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(17)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(18)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(19)).isEqualTo(Color.GREEN);
        assertThat(mSpanColors.getColorAt(20)).isEqualTo(Color.GREEN);
    }

    @Test
    public void testSingleColorSpanWithEmojiAndCharacterStyle() {
        mSpannedTextWithEmoji.setSpan(new CharacterStyle() {
            @Override
            public void updateDrawState(TextPaint ds) {
                ds.setColor(Color.BLACK);
            }
        }, 1, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        mSpanColors.init(mWorkPaint, mSpannedTextWithEmoji, 1, 11);
        assertThat(mSpanColors.getColorAt(6)).isEqualTo(Color.BLACK);
    }

    @Test
    public void testMultipleColorSpansWithEmojiAndCharacterStyle() {
        mSpannedTextWithEmoji.setSpan(new CharacterStyle() {
            @Override
            public void updateDrawState(TextPaint ds) {
                ds.setColor(Color.BLACK);
            }
        }, 1, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        mSpanColors.init(mWorkPaint, mSpannedTextWithEmoji, 0, mSpannedTextWithEmoji.length());
        assertThat(mSpanColors.getColorAt(0)).isEqualTo(Color.RED);
        assertThat(mSpanColors.getColorAt(1)).isEqualTo(Color.BLACK);
        assertThat(mSpanColors.getColorAt(2)).isEqualTo(Color.BLACK);
        assertThat(mSpanColors.getColorAt(3)).isEqualTo(Color.BLACK);
        assertThat(mSpanColors.getColorAt(4)).isEqualTo(Color.BLACK);
        assertThat(mSpanColors.getColorAt(5)).isEqualTo(Color.BLACK);
        assertThat(mSpanColors.getColorAt(6)).isEqualTo(Color.BLACK);
        assertThat(mSpanColors.getColorAt(7)).isEqualTo(Color.BLACK);
        assertThat(mSpanColors.getColorAt(8)).isEqualTo(Color.BLACK);
        assertThat(mSpanColors.getColorAt(9)).isEqualTo(Color.BLACK);
        assertThat(mSpanColors.getColorAt(10)).isEqualTo(Color.BLACK);
        assertThat(mSpanColors.getColorAt(11)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(12)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(13)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(14)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(15)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(16)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(17)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(18)).isEqualTo(SpanColors.NO_COLOR_FOUND);
        assertThat(mSpanColors.getColorAt(19)).isEqualTo(Color.GREEN);
        assertThat(mSpanColors.getColorAt(20)).isEqualTo(Color.GREEN);
    }
}