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

Commit 16ad1f7b authored by Seigo Nonaka's avatar Seigo Nonaka
Browse files

Add highlight APIs into TextView and Layout

Bug: 244502332
Test: atest LayoutDrawOrderTest
Change-Id: If625a1f2866c524ab709e69cd9f19212c60a8e34
parent fe4041ee
Loading
Loading
Loading
Loading
+18 −0
Original line number Diff line number Diff line
@@ -45669,6 +45669,19 @@ package android.text {
    method public int previousStartBoundary(@IntRange(from=0) int);
  }
  public class Highlights {
    method @NonNull public android.graphics.Paint getPaint(int);
    method @NonNull public int[] getRanges(int);
    method public int getSize();
  }
  public static final class Highlights.Builder {
    ctor public Highlights.Builder();
    method @NonNull public android.text.Highlights.Builder addRange(@NonNull android.graphics.Paint, int, int);
    method @NonNull public android.text.Highlights.Builder addRanges(@NonNull android.graphics.Paint, @NonNull int...);
    method @NonNull public android.text.Highlights build();
  }
  public class Html {
    method public static String escapeHtml(CharSequence);
    method @Deprecated public static android.text.Spanned fromHtml(String);
@@ -45760,6 +45773,9 @@ package android.text {
    ctor protected Layout(CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float);
    method public void draw(android.graphics.Canvas);
    method public void draw(android.graphics.Canvas, android.graphics.Path, android.graphics.Paint, int);
    method public void draw(@NonNull android.graphics.Canvas, @Nullable java.util.List<android.graphics.Path>, @Nullable java.util.List<android.graphics.Paint>, @Nullable android.graphics.Path, @Nullable android.graphics.Paint, int);
    method public void drawBackground(@NonNull android.graphics.Canvas);
    method public void drawText(@NonNull android.graphics.Canvas);
    method public void fillCharacterBounds(@IntRange(from=0) int, @IntRange(from=0) int, @NonNull float[], @IntRange(from=0) int);
    method public final android.text.Layout.Alignment getAlignment();
    method public abstract int getBottomPadding();
@@ -58573,6 +58589,7 @@ package android.widget {
    method public boolean getFreezesText();
    method public int getGravity();
    method @ColorInt public int getHighlightColor();
    method @Nullable public android.text.Highlights getHighlights();
    method public CharSequence getHint();
    method public final android.content.res.ColorStateList getHintTextColors();
    method public int getHyphenationFrequency();
@@ -58702,6 +58719,7 @@ package android.widget {
    method public void setGravity(int);
    method public void setHeight(int);
    method public void setHighlightColor(@ColorInt int);
    method public void setHighlights(@Nullable android.text.Highlights);
    method public final void setHint(CharSequence);
    method public final void setHint(@StringRes int);
    method public final void setHintTextColor(@ColorInt int);
+148 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text;

import android.graphics.Paint;
import android.util.Pair;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * A class that represents of the highlight of the text.
 */
public class Highlights {
    private final List<Pair<Paint, int[]>> mHighlights;

    private Highlights(List<Pair<Paint, int[]>> highlights) {
        mHighlights = highlights;
    }

    /**
     * Returns a number of highlight.
     *
     * @return a number of highlight.
     *
     * @see Builder#addRange(Paint, int, int)
     * @see Builder#addRanges(Paint, int...)
     */
    public @IntRange(from = 0) int getSize() {
        return mHighlights.size();
    }

    /**
     * Returns a paint used for the i-th highlight.
     *
     * @param index an index of the highlight. Must be between 0 and {@link #getSize()}
     * @return a paint object
     *
     * @see Builder#addRange(Paint, int, int)
     * @see Builder#addRanges(Paint, int...)
     */
    public @NonNull Paint getPaint(@IntRange(from = 0) int index) {
        return mHighlights.get(index).first;
    }

    /**
     * Returns ranges of the i-th highlight.
     *
     * Ranges are represented of flattened inclusive start and exclusive end integers array. The
     * inclusive start offset of the {@code i}-th range is stored in {@code 2 * i}-th of the array.
     * The exclusive end offset of the {@code i}-th range is stored in {@code 2* i + 1}-th of the
     * array. For example, the two ranges: (1, 2) and (3, 4) are flattened into single int array
     * [1, 2, 3, 4].
     *
     * @param index an index of the highlight. Must be between 0 and {@link #getSize()}
     * @return a paint object
     *
     * @see Builder#addRange(Paint, int, int)
     * @see Builder#addRanges(Paint, int...)
     */
    public @NonNull int[] getRanges(int index) {
        return mHighlights.get(index).second;
    }

    /**
     * A builder for the Highlights.
     */
    public static final class Builder {
        private final List<Pair<Paint, int[]>> mHighlights = new ArrayList<>();

        /**
         * Add single range highlight.
         *
         * @param paint a paint object used for drawing highlight path.
         * @param start an inclusive offset of the text.
         * @param end an exclusive offset of the text.
         * @return this builder instance.
         */
        public @NonNull Builder addRange(@NonNull Paint paint, @IntRange(from = 0) int start,
                @IntRange(from = 0) int end) {
            if (start > end) {
                throw new IllegalArgumentException("start must not be larger than end: "
                        + start + ", " + end);
            }
            Objects.requireNonNull(paint);

            int[] range = new int[] {start, end};
            mHighlights.add(new Pair<>(paint, range));
            return this;
        }

        /**
         * Add multiple ranges highlight.
         *
         * @param paint a paint object used for drawing highlight path.
         * @param ranges a flatten ranges. The {@code 2 * i}-th element is an inclusive start offset
         *              of the {@code i}-th character. The {@code 2 * i + 1}-th element is an
         *              exclusive end offset of the {@code i}-th character.
         * @return this builder instance.
         */
        public @NonNull Builder addRanges(@NonNull Paint paint, @NonNull int... ranges) {
            if (ranges.length % 2 == 1) {
                throw new IllegalArgumentException(
                        "Flatten ranges must have even numbered elements");
            }
            for (int j = 0; j < ranges.length / 2; ++j) {
                int start = ranges[j * 2];
                int end = ranges[j * 2 + 1];
                if (start > end) {
                    throw new IllegalArgumentException(
                            "Reverse range found in the flatten range: " + Arrays.toString(
                                    ranges));
                }
            }
            Objects.requireNonNull(paint);
            mHighlights.add(new Pair<>(paint, ranges));
            return this;
        }

        /**
         * Build a new Highlights instance.
         *
         * @return a new Highlights instance.
         */
        public @NonNull Highlights build() {
            return new Highlights(mHighlights);
        }
    }
}
+133 −16
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import com.android.internal.util.GrowingArrayUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;

/**
 * A base class that manages text layout in visual elements on
@@ -347,9 +348,13 @@ public abstract class Layout {

    /**
     * Draw this Layout on the specified Canvas.
     *
     * This API draws background first, then draws text on top of it.
     *
     * @see #draw(Canvas, List, List, Path, Paint, int)
     */
    public void draw(Canvas c) {
        draw(c, null, null, 0);
        draw(c, (Path) null, (Paint) null, 0);
    }

    /**
@@ -357,23 +362,142 @@ public abstract class Layout {
     * between the background and the text.
     *
     * @param canvas the canvas
     * @param highlight the path of the highlight or cursor; can be null
     * @param highlightPaint the paint for the highlight
     * @param selectionHighlight the path of the selection highlight or cursor; can be null
     * @param selectionHighlightPaint the paint for the selection highlight
     * @param cursorOffsetVertical the amount to temporarily translate the
     *        canvas while rendering the highlight
     *
     * @see #draw(Canvas, List, List, Path, Paint, int)
     */
    public void draw(Canvas canvas, Path highlight, Paint highlightPaint,
    public void draw(
            Canvas canvas, Path selectionHighlight,
            Paint selectionHighlightPaint, int cursorOffsetVertical) {
        draw(canvas, null, null, selectionHighlight, selectionHighlightPaint, cursorOffsetVertical);
    }

    /**
     * Draw this layout on the specified canvas.
     *
     * This API draws background first, then draws highlight paths on top of it, then draws
     * selection or cursor, then finally draws text on top of it.
     *
     * @see #drawBackground(Canvas)
     * @see #drawText(Canvas)
     *
     * @param canvas the canvas
     * @param highlightPaths the path of the highlights. The highlightPaths and highlightPaints must
     *                      have the same length and aligned in the same order. For example, the
     *                      paint of the n-th of the highlightPaths should be stored at the n-th of
     *                      highlightPaints.
     * @param highlightPaints the paints for the highlights. The highlightPaths and highlightPaints
     *                        must have the same length and aligned in the same order. For example,
     *                        the paint of the n-th of the highlightPaths should be stored at the
     *                        n-th of highlightPaints.
     * @param selectionPath the selection or cursor path
     * @param selectionPaint the paint for the selection or cursor.
     * @param cursorOffsetVertical the amount to temporarily translate the canvas while rendering
     *                            the highlight
     */
    public void draw(@NonNull Canvas canvas,
            @Nullable List<Path> highlightPaths,
            @Nullable List<Paint> highlightPaints,
            @Nullable Path selectionPath,
            @Nullable Paint selectionPaint,
            int cursorOffsetVertical) {
        final long lineRange = getLineRangeForDraw(canvas);
        int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
        int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
        if (lastLine < 0) return;

        drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
                firstLine, lastLine);
        drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
                cursorOffsetVertical, firstLine, lastLine);
        drawText(canvas, firstLine, lastLine);
    }

    /**
     * Draw text part of this layout.
     *
     * Different from {@link #draw(Canvas, List, List, Path, Paint, int)} API, this API only draws
     * text part, not drawing highlights, selections, or backgrounds.
     *
     * @see #draw(Canvas, List, List, Path, Paint, int)
     * @see #drawBackground(Canvas)
     *
     * @param canvas the canvas
     */
    public void drawText(@NonNull Canvas canvas) {
        final long lineRange = getLineRangeForDraw(canvas);
        int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
        int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
        if (lastLine < 0) return;
        drawText(canvas, firstLine, lastLine);
    }

    /**
     * Draw background of this layout.
     *
     * Different from {@link #draw(Canvas, List, List, Path, Paint, int)} API, this API only draws
     * background, not drawing text, highlights or selections. The background here is drawn by
     * {@link LineBackgroundSpan} attached to the text.
     *
     * @see #draw(Canvas, List, List, Path, Paint, int)
     * @see #drawText(Canvas)
     *
     * @param canvas the canvas
     */
    public void drawBackground(@NonNull Canvas canvas) {
        final long lineRange = getLineRangeForDraw(canvas);
        int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
        int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
        if (lastLine < 0) return;
        drawBackground(canvas, firstLine, lastLine);
    }

    /**
     * @hide public for Editor.java
     */
    public void drawWithoutText(
            @NonNull Canvas canvas,
            @Nullable List<Path> highlightPaths,
            @Nullable List<Paint> highlightPaints,
            @Nullable Path selectionPath,
            @Nullable Paint selectionPaint,
            int cursorOffsetVertical,
            int firstLine,
            int lastLine) {
        drawBackground(canvas, firstLine, lastLine);
        if (highlightPaths == null && highlightPaints == null) {
            return;
        }
        if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
        try {
            if (highlightPaths != null) {
                if (highlightPaints == null) {
                    throw new IllegalArgumentException(
                            "if highlight is specified, highlightPaint must be specified.");
                }
                if (highlightPaints.size() != highlightPaths.size()) {
                    throw new IllegalArgumentException(
                            "The highlight path size is different from the size of highlight"
                                    + " paints");
                }
                for (int i = 0; i < highlightPaths.size(); ++i) {
                    final Path highlight = highlightPaths.get(i);
                    final Paint highlightPaint = highlightPaints.get(i);
                    if (highlight != null) {
                        canvas.drawPath(highlight, highlightPaint);
                    }
                }
            }

            if (selectionPath != null) {
                canvas.drawPath(selectionPath, selectionPaint);
            }
        } finally {
            if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
        }
    }

    private boolean isJustificationRequired(int lineNum) {
        if (mJustificationMode == JUSTIFICATION_MODE_NONE) return false;
        final int lineEnd = getLineEnd(lineNum);
@@ -635,8 +759,9 @@ public abstract class Layout {
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
            int cursorOffsetVertical, int firstLine, int lastLine) {
    public void drawBackground(
            @NonNull Canvas canvas,
            int firstLine, int lastLine) {
        // First, draw LineBackgroundSpans.
        // LineBackgroundSpans know nothing about the alignment, margins, or
        // direction of the layout or line.  XXX: Should they?
@@ -700,14 +825,6 @@ public abstract class Layout {
            }
            mLineBackgroundSpans.recycle();
        }

        // There can be a highlight even without spans if we are drawing
        // a non-spanned transformation of a spanned editing buffer.
        if (highlight != null) {
            if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
            canvas.drawPath(highlight, highlightPaint);
            if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
        }
    }

    /**
+23 −16
Original line number Diff line number Diff line
@@ -2054,7 +2054,10 @@ public class Editor {
        }
    }

    void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
    void onDraw(Canvas canvas, Layout layout,
            List<Path> highlightPaths,
            List<Paint> highlightPaints,
            Path selectionHighlight, Paint selectionHighlightPaint,
            int cursorOffsetVertical) {
        final int selectionStart = mTextView.getSelectionStart();
        final int selectionEnd = mTextView.getSelectionEnd();
@@ -2078,37 +2081,40 @@ public class Editor {
            mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
        }

        if (highlight != null && selectionStart == selectionEnd && mDrawableForCursor != null) {
        if (selectionHighlight != null && selectionStart == selectionEnd
                && mDrawableForCursor != null) {
            drawCursor(canvas, cursorOffsetVertical);
            // Rely on the drawable entirely, do not draw the cursor line.
            // Has to be done after the IMM related code above which relies on the highlight.
            highlight = null;
            selectionHighlight = null;
        }

        if (mSelectionActionModeHelper != null) {
            mSelectionActionModeHelper.onDraw(canvas);
            if (mSelectionActionModeHelper.isDrawingHighlight()) {
                highlight = null;
                selectionHighlight = null;
            }
        }

        if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
            drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
                    cursorOffsetVertical);
            drawHardwareAccelerated(canvas, layout, highlightPaths, highlightPaints,
                    selectionHighlight, selectionHighlightPaint, cursorOffsetVertical);
        } else {
            layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
            layout.draw(canvas, highlightPaths, highlightPaints, selectionHighlight,
                    selectionHighlightPaint, cursorOffsetVertical);
        }
    }

    private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
            Paint highlightPaint, int cursorOffsetVertical) {
    private void drawHardwareAccelerated(Canvas canvas, Layout layout,
            List<Path> highlightPaths, List<Paint> highlightPaints,
            Path selectionHighlight, Paint selectionHighlightPaint, int cursorOffsetVertical) {
        final long lineRange = layout.getLineRangeForDraw(canvas);
        int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
        int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
        if (lastLine < 0) return;

        layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
                firstLine, lastLine);
        layout.drawWithoutText(canvas, highlightPaths, highlightPaints, selectionHighlight,
                selectionHighlightPaint, cursorOffsetVertical, firstLine, lastLine);

        if (layout instanceof DynamicLayout) {
            if (mTextRenderNodes == null) {
@@ -2154,8 +2160,9 @@ public class Editor {
                    continue;
                }
                startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
                        highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
                        blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
                        selectionHighlight, selectionHighlightPaint, cursorOffsetVertical,
                        blockEndLines, blockIndices, i, numberOfBlocks,
                        startIndexToFindAvailableRenderNode);
                if (blockEndLines[i] >= lastLine) {
                    lastIndex = Math.max(indexFirstChangedBlock, i + 1);
                    break;
@@ -2169,9 +2176,9 @@ public class Editor {
                            || mTextRenderNodes[blockIndex] == null
                            || mTextRenderNodes[blockIndex].needsToBeShifted) {
                        startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
                                layout, highlight, highlightPaint, cursorOffsetVertical,
                                blockEndLines, blockIndices, block, numberOfBlocks,
                                startIndexToFindAvailableRenderNode);
                                layout, selectionHighlight, selectionHighlightPaint,
                                cursorOffsetVertical, blockEndLines, blockIndices, block,
                                numberOfBlocks, startIndexToFindAvailableRenderNode);
                    }
                }
            }
+92 −3

File changed.

Preview size limit exceeded, changes collapsed.