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

Commit 2ab87690 authored by Tyler Freeman's avatar Tyler Freeman Committed by Android (Google) Code Review
Browse files

Merge "fix(high contrast text): fix highlight/selection is obscured by high...

Merge "fix(high contrast text): fix highlight/selection is obscured by high contrast text background rectangle" into main
parents eb8decbc eca25fbc
Loading
Loading
Loading
Loading
+88 −4
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.text;

import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance;
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
@@ -28,7 +29,9 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.BlendMode;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
@@ -46,7 +49,9 @@ import android.text.style.ReplacementSpan;
import android.text.style.TabStopSpan;
import android.widget.TextView;

import com.android.graphics.hwui.flags.Flags;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.GrowingArrayUtils;

@@ -480,9 +485,23 @@ public abstract class Layout {
        int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
        if (lastLine < 0) return;

        if (shouldDrawHighlightsOnTop(canvas)) {
            drawBackground(canvas, firstLine, lastLine);
        } else {
            drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
                    cursorOffsetVertical, firstLine, lastLine);
        }

        drawText(canvas, firstLine, lastLine);

        // Since high contrast text draws a solid rectangle background behind the text, it covers up
        // the highlights and selections. In this case we draw over the top of the text with a
        // blend mode that ensures the text stays high-contrast.
        if (shouldDrawHighlightsOnTop(canvas)) {
            drawHighlights(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
                    cursorOffsetVertical, firstLine, lastLine);
        }

        if (leftShift != 0) {
            // Manually translate back to the original position because of b/324498002, using
            // save/restore disappears the toggle switch drawables.
@@ -490,6 +509,19 @@ public abstract class Layout {
        }
    }

    private static boolean shouldDrawHighlightsOnTop(Canvas canvas) {
        return Flags.highContrastTextSmallTextRect() && canvas.isHighContrastTextEnabled();
    }

    private static Paint setToHighlightPaint(Paint p, BlendMode blendMode, Paint outPaint) {
        if (p == null) return null;
        outPaint.set(p);
        outPaint.setBlendMode(blendMode);
        // Yellow for maximum contrast
        outPaint.setColor(Color.YELLOW);
        return outPaint;
    }

    /**
     * Draw text part of this layout.
     *
@@ -542,11 +574,28 @@ public abstract class Layout {
            int firstLine,
            int lastLine) {
        drawBackground(canvas, firstLine, lastLine);
        drawHighlights(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
                cursorOffsetVertical, firstLine, lastLine);
    }

    /**
     * @hide public for Editor.java
     */
    public void drawHighlights(
            @NonNull Canvas canvas,
            @Nullable List<Path> highlightPaths,
            @Nullable List<Paint> highlightPaints,
            @Nullable Path selectionPath,
            @Nullable Paint selectionPaint,
            int cursorOffsetVertical,
            int firstLine,
            int lastLine) {
        if (highlightPaths == null && highlightPaints == null) {
            return;
        }
        if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
        try {
            BlendMode blendMode = determineHighContrastHighlightBlendMode(canvas);
            if (highlightPaths != null) {
                if (highlightPaints == null) {
                    throw new IllegalArgumentException(
@@ -559,7 +608,12 @@ public abstract class Layout {
                }
                for (int i = 0; i < highlightPaths.size(); ++i) {
                    final Path highlight = highlightPaths.get(i);
                    final Paint highlightPaint = highlightPaints.get(i);
                    Paint highlightPaint = highlightPaints.get(i);
                    if (shouldDrawHighlightsOnTop(canvas)) {
                        highlightPaint = setToHighlightPaint(highlightPaint, blendMode,
                                mWorkPlainPaint);
                    }

                    if (highlight != null) {
                        canvas.drawPath(highlight, highlightPaint);
                    }
@@ -567,6 +621,10 @@ public abstract class Layout {
            }

            if (selectionPath != null) {
                if (shouldDrawHighlightsOnTop(canvas)) {
                    selectionPaint = setToHighlightPaint(selectionPaint, blendMode,
                            mWorkPlainPaint);
                }
                canvas.drawPath(selectionPath, selectionPaint);
            }
        } finally {
@@ -574,6 +632,31 @@ public abstract class Layout {
        }
    }

    @Nullable
    private BlendMode determineHighContrastHighlightBlendMode(Canvas canvas) {
        if (!shouldDrawHighlightsOnTop(canvas)) {
            return null;
        }

        return isHighContrastTextDark() ? BlendMode.MULTIPLY : BlendMode.DIFFERENCE;
    }

    private boolean isHighContrastTextDark() {
        // High-contrast text mode
        // Determine if the text is black-on-white or white-on-black, so we know what blendmode will
        // give the highest contrast and most realistic text color.
        // This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h
        if (highContrastTextLuminance()) {
            var lab = new double[3];
            ColorUtils.colorToLAB(mPaint.getColor(), lab);
            return lab[0] < 0.5;
        } else {
            var color = mPaint.getColor();
            int channelSum = Color.red(color) + Color.green(color) + Color.blue(color);
            return channelSum < (128 * 3);
        }
    }

    private boolean isJustificationRequired(int lineNum) {
        if (mJustificationMode == JUSTIFICATION_MODE_NONE) return false;
        final int lineEnd = getLineEnd(lineNum);
@@ -3396,7 +3479,8 @@ public abstract class Layout {
    private CharSequence mText;
    @UnsupportedAppUsage
    private TextPaint mPaint;
    private TextPaint mWorkPaint = new TextPaint();
    private final TextPaint mWorkPaint = new TextPaint();
    private final Paint mWorkPlainPaint = new Paint();
    private int mWidth;
    private Alignment mAlignment = Alignment.ALIGN_NORMAL;
    private float mSpacingMult;
+16 −2
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ package android.widget;
import static android.view.ContentInfo.SOURCE_DRAG_AND_DROP;
import static android.widget.TextView.ACCESSIBILITY_ACTION_SMART_START_ID;

import static com.android.graphics.hwui.flags.Flags.highContrastTextSmallTextRect;

import android.R;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
@@ -2151,8 +2153,15 @@ public class Editor {
        int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
        if (lastLine < 0) return;

        boolean shouldDrawHighlightsOnTop = highContrastTextSmallTextRect()
                && canvas.isHighContrastTextEnabled();

        if (!shouldDrawHighlightsOnTop) {
            layout.drawWithoutText(canvas, highlightPaths, highlightPaints, selectionHighlight,
                    selectionHighlightPaint, cursorOffsetVertical, firstLine, lastLine);
        } else {
            layout.drawBackground(canvas, firstLine, lastLine);
        }

        if (layout instanceof DynamicLayout) {
            if (mTextRenderNodes == null) {
@@ -2226,6 +2235,11 @@ public class Editor {
            // Boring layout is used for empty and hint text
            layout.drawText(canvas, firstLine, lastLine);
        }

        if (shouldDrawHighlightsOnTop) {
            layout.drawHighlights(canvas, highlightPaths, highlightPaints, selectionHighlight,
                    selectionHighlightPaint, cursorOffsetVertical, firstLine, lastLine);
        }
    }

    private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
+270 −5
Original line number Diff line number Diff line
@@ -16,6 +16,11 @@

package android.text;

import static com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -26,11 +31,16 @@ import static org.junit.Assert.fail;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.text.Layout.Alignment;
import android.text.style.StrikethroughSpan;

@@ -38,6 +48,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

@@ -49,6 +60,9 @@ import java.util.Locale;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class LayoutTest {
    @Rule
    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();

    private static final int LINE_COUNT = 5;
    private static final int LINE_HEIGHT = 12;
    private static final int LINE_DESCENT = 4;
@@ -638,22 +652,268 @@ public class LayoutTest {
        }
    }

    private final class MockCanvas extends Canvas {
    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testDrawSelectionAndHighlight_drawsHighContrastSelectionAndHighlight() {
        Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
                mAlign, mSpacingMult, mSpacingAdd);

        List<Path> highlightPaths = new ArrayList<>();
        List<Paint> highlightPaints = new ArrayList<>();

        Path selectionPath = new Path();
        RectF selectionRect = new RectF(0f, 0f, mWidth / 2f, LINE_HEIGHT);
        selectionPath.addRect(selectionRect, Path.Direction.CW);
        highlightPaths.add(selectionPath);

        Paint selectionPaint = new Paint();
        selectionPaint.setColor(Color.CYAN);
        highlightPaints.add(selectionPaint);

        final int width = 256;
        final int height = 256;
        MockCanvas c = new MockCanvas(width, height);
        c.setHighContrastTextEnabled(true);
        layout.draw(c, highlightPaths, highlightPaints, selectionPath, selectionPaint,
                /* cursorOffsetVertical= */ 0);
        List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
        var textsDrawn = LINE_COUNT;
        var highlightsDrawn = 2;
        assertThat(drawCommands.size()).isEqualTo(textsDrawn + highlightsDrawn);

        var highlightsFound = 0;
        var curLineIndex = 0;
        for (int i = 0; i < drawCommands.size(); i++) {
            MockCanvas.DrawCommand drawCommand = drawCommands.get(i);

            if (drawCommand.path != null) {
                assertThat(drawCommand.path).isEqualTo(selectionPath);
                assertThat(drawCommand.paint.getColor()).isEqualTo(Color.YELLOW);
                assertThat(drawCommand.paint.getBlendMode()).isNotNull();
                highlightsFound++;
            } else if (drawCommand.text != null) {
                int start = layout.getLineStart(curLineIndex);
                int end = layout.getLineEnd(curLineIndex);
                assertEquals(LAYOUT_TEXT.toString().substring(start, end), drawCommand.text);
                curLineIndex++;

                assertWithMessage("highlight is drawn on top of text")
                        .that(highlightsFound).isEqualTo(0);
            }
        }

        assertThat(highlightsFound).isEqualTo(2);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabled_testDrawHighlight_drawsHighContrastHighlight() {
        Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
                mAlign, mSpacingMult, mSpacingAdd);

        List<Path> highlightPaths = new ArrayList<>();
        List<Paint> highlightPaints = new ArrayList<>();

        Path selectionPath = new Path();
        RectF selectionRect = new RectF(0f, 0f, mWidth / 2f, LINE_HEIGHT);
        selectionPath.addRect(selectionRect, Path.Direction.CW);
        highlightPaths.add(selectionPath);

        Paint selectionPaint = new Paint();
        selectionPaint.setColor(Color.CYAN);
        highlightPaints.add(selectionPaint);

        final int width = 256;
        final int height = 256;
        MockCanvas c = new MockCanvas(width, height);
        c.setHighContrastTextEnabled(true);
        layout.draw(c, highlightPaths, highlightPaints, /* selectionPath= */ null,
                /* selectionPaint= */ null, /* cursorOffsetVertical= */ 0);
        List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
        var textsDrawn = LINE_COUNT;
        var highlightsDrawn = 1;
        assertThat(drawCommands.size()).isEqualTo(textsDrawn + highlightsDrawn);

        var highlightsFound = 0;
        var curLineIndex = 0;
        for (int i = 0; i < drawCommands.size(); i++) {
            MockCanvas.DrawCommand drawCommand = drawCommands.get(i);

            if (drawCommand.path != null) {
                assertThat(drawCommand.path).isEqualTo(selectionPath);
                assertThat(drawCommand.paint.getColor()).isEqualTo(Color.YELLOW);
                assertThat(drawCommand.paint.getBlendMode()).isNotNull();
                highlightsFound++;
            } else if (drawCommand.text != null) {
                int start = layout.getLineStart(curLineIndex);
                int end = layout.getLineEnd(curLineIndex);
                assertEquals(LAYOUT_TEXT.toString().substring(start, end), drawCommand.text);
                curLineIndex++;

                assertWithMessage("highlight is drawn on top of text")
                        .that(highlightsFound).isEqualTo(0);
            }
        }

        assertThat(highlightsFound).isEqualTo(1);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextDisabledByDefault_testDrawHighlight_drawsNormalHighlightBehind() {
        Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
                mAlign, mSpacingMult, mSpacingAdd);

        List<Path> highlightPaths = new ArrayList<>();
        List<Paint> highlightPaints = new ArrayList<>();

        Path selectionPath = new Path();
        RectF selectionRect = new RectF(0f, 0f, mWidth / 2f, LINE_HEIGHT);
        selectionPath.addRect(selectionRect, Path.Direction.CW);
        highlightPaths.add(selectionPath);

        Paint selectionPaint = new Paint();
        selectionPaint.setColor(Color.CYAN);
        highlightPaints.add(selectionPaint);

        final int width = 256;
        final int height = 256;
        MockCanvas c = new MockCanvas(width, height);
        layout.draw(c, highlightPaths, highlightPaints, /* selectionPath= */ null,
                /* selectionPaint= */ null, /* cursorOffsetVertical= */ 0);
        List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
        var textsDrawn = LINE_COUNT;
        var highlightsDrawn = 1;
        assertThat(drawCommands.size()).isEqualTo(textsDrawn + highlightsDrawn);

        var highlightsFound = 0;
        var curLineIndex = 0;
        for (int i = 0; i < drawCommands.size(); i++) {
            MockCanvas.DrawCommand drawCommand = drawCommands.get(i);

            if (drawCommand.path != null) {
                assertThat(drawCommand.path).isEqualTo(selectionPath);
                assertThat(drawCommand.paint.getColor()).isEqualTo(Color.CYAN);
                assertThat(drawCommand.paint.getBlendMode()).isNull();
                highlightsFound++;
            } else if (drawCommand.text != null) {
                int start = layout.getLineStart(curLineIndex);
                int end = layout.getLineEnd(curLineIndex);
                assertEquals(LAYOUT_TEXT.toString().substring(start, end), drawCommand.text);
                curLineIndex++;

                assertWithMessage("highlight is drawn behind text")
                        .that(highlightsFound).isGreaterThan(0);
            }
        }

        assertThat(highlightsFound).isEqualTo(1);
    }

    @Test
    @RequiresFlagsDisabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
    public void highContrastTextEnabledButFlagOff_testDrawHighlight_drawsNormalHighlightBehind() {
        Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
                mAlign, mSpacingMult, mSpacingAdd);

        List<Path> highlightPaths = new ArrayList<>();
        List<Paint> highlightPaints = new ArrayList<>();

        Path selectionPath = new Path();
        RectF selectionRect = new RectF(0f, 0f, mWidth / 2f, LINE_HEIGHT);
        selectionPath.addRect(selectionRect, Path.Direction.CW);
        highlightPaths.add(selectionPath);

        Paint selectionPaint = new Paint();
        selectionPaint.setColor(Color.CYAN);
        highlightPaints.add(selectionPaint);

        final int width = 256;
        final int height = 256;
        MockCanvas c = new MockCanvas(width, height);
        c.setHighContrastTextEnabled(true);
        layout.draw(c, highlightPaths, highlightPaints, /* selectionPath= */ null,
                /* selectionPaint= */ null, /* cursorOffsetVertical= */ 0);
        List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
        var textsDrawn = LINE_COUNT;
        var highlightsDrawn = 1;
        assertThat(drawCommands.size()).isEqualTo(textsDrawn + highlightsDrawn);

        class DrawCommand {
        var highlightsFound = 0;
        var curLineIndex = 0;
        for (int i = 0; i < drawCommands.size(); i++) {
            MockCanvas.DrawCommand drawCommand = drawCommands.get(i);

            if (drawCommand.path != null) {
                assertThat(drawCommand.path).isEqualTo(selectionPath);
                assertThat(drawCommand.paint.getColor()).isEqualTo(Color.CYAN);
                assertThat(drawCommand.paint.getBlendMode()).isNull();
                highlightsFound++;
            } else if (drawCommand.text != null) {
                int start = layout.getLineStart(curLineIndex);
                int end = layout.getLineEnd(curLineIndex);
                assertEquals(LAYOUT_TEXT.toString().substring(start, end), drawCommand.text);
                curLineIndex++;

                assertWithMessage("highlight is drawn behind text")
                        .that(highlightsFound).isGreaterThan(0);
            }
        }

        assertThat(highlightsFound).isEqualTo(1);
    }

    @Test
    public void mockCanvasHighContrastOverridesCorrectly() {
        var canvas = new MockCanvas(100, 100);

        assertThat(canvas.isHighContrastTextEnabled()).isFalse();
        canvas.setHighContrastTextEnabled(true);
        assertThat(canvas.isHighContrastTextEnabled()).isTrue();
        canvas.setHighContrastTextEnabled(false);
        assertThat(canvas.isHighContrastTextEnabled()).isFalse();
    }

    private static final class MockCanvas extends Canvas {

        static class DrawCommand {
            public final String text;
            public final float x;
            public final float y;
            public final Path path;
            public final Paint paint;

            DrawCommand(String text, float x, float y) {
            DrawCommand(String text, float x, float y, Paint paint) {
                this.text = text;
                this.x = x;
                this.y = y;
                this.paint = paint;
                path = null;
            }

            DrawCommand(Path path, Paint paint) {
                this.path = path;
                this.paint = paint;
                y = 0;
                x = 0;
                text = null;
            }
        }

        List<DrawCommand> mDrawCommands;

        private Boolean mIsHighContrastTextOverride = null;

        public void setHighContrastTextEnabled(boolean enabled) {
            mIsHighContrastTextOverride = enabled;
        }

        @Override
        public boolean isHighContrastTextEnabled() {
            return mIsHighContrastTextOverride == null ? super.isHighContrastTextEnabled()
                    : mIsHighContrastTextOverride;
        }

        MockCanvas(int width, int height) {
            super();
            mDrawCommands = new ArrayList<>();
@@ -666,7 +926,7 @@ public class LayoutTest {

        @Override
        public void drawText(String text, int start, int end, float x, float y, Paint p) {
            mDrawCommands.add(new DrawCommand(text.substring(start, end), x, y));
            mDrawCommands.add(new DrawCommand(text.substring(start, end), x, y, p));
        }

        @Override
@@ -676,7 +936,7 @@ public class LayoutTest {

        @Override
        public void drawText(char[] text, int index, int count, float x, float y, Paint p) {
            mDrawCommands.add(new DrawCommand(new String(text, index, count), x, y));
            mDrawCommands.add(new DrawCommand(new String(text, index, count), x, y, p));
        }

        @Override
@@ -691,6 +951,11 @@ public class LayoutTest {
            drawText(text, index, count, x, y, paint);
        }

        @Override
        public void drawPath(Path path, Paint p) {
            mDrawCommands.add(new DrawCommand(path, p));
        }

        List<DrawCommand> getDrawCommands() {
            return mDrawCommands;
        }
+14 −0
Original line number Diff line number Diff line
@@ -152,6 +152,18 @@ public class Canvas extends BaseCanvas {
        return false;
    }

    /**
     * Indicates whether this Canvas is drawing high contrast text.
     *
     * @see android.view.accessibility.AccessibilityManager#isHighTextContrastEnabled()
     * @return True if high contrast text is enabled, false otherwise.
     *
     * @hide
     */
    public boolean isHighContrastTextEnabled() {
        return nIsHighContrastText(mNativeCanvasWrapper);
    }

    /**
     * Specify a bitmap for the canvas to draw into. All canvas state such as
     * layers, filters, and the save/restore stack are reset. Additionally,
@@ -1452,6 +1464,8 @@ public class Canvas extends BaseCanvas {
    @CriticalNative
    private static native boolean nIsOpaque(long canvasHandle);
    @CriticalNative
    private static native boolean nIsHighContrastText(long canvasHandle);
    @CriticalNative
    private static native int nGetWidth(long canvasHandle);
    @CriticalNative
    private static native int nGetHeight(long canvasHandle);
+1 −0
Original line number Diff line number Diff line
@@ -92,6 +92,7 @@ public:
            // high contrast draw path
            int color = paint.getColor();
            bool darken;
            // This equation should match the one in core/java/android/text/Layout.java
            if (flags::high_contrast_text_luminance()) {
                uirenderer::Lab lab = uirenderer::sRGBToLab(color);
                darken = lab.L <= 50;
Loading