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

Commit b507df45 authored by Seigo Nonaka's avatar Seigo Nonaka Committed by Android (Google) Code Review
Browse files

Merge "Introduce PremeasuredText"

parents c824630c fbe63bdd
Loading
Loading
Loading
Loading
+82 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.text;

import static android.text.TextDirectionHeuristics.LTR;

import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;

@@ -182,4 +184,84 @@ public class StaticLayoutPerfTest {
                    .build();
        }
    }

    @Test
    public void testCreate_MeasuredText_NoStyled_Greedy_NoHyphenation() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            state.pauseTiming();
            final PremeasuredText text = PremeasuredText.build(
                    generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
            state.resumeTiming();

            StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
                    .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
                    .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
                    .build();
        }
    }

    @Test
    public void testCreate_MeasuredText_NoStyled_Greedy_Hyphenation() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            state.pauseTiming();
            final PremeasuredText text = PremeasuredText.build(
                    generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
            state.resumeTiming();

            StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
                    .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
                    .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
                    .build();
        }
    }

    @Test
    public void testCreate_MeasuredText_NoStyled_Balanced_NoHyphenation() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            state.pauseTiming();
            final PremeasuredText text = PremeasuredText.build(
                    generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
            state.resumeTiming();

            StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
                    .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
                    .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
                    .build();
        }
    }

    @Test
    public void testCreate_MeasuredText_NoStyled_Balanced_Hyphenation() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            state.pauseTiming();
            final PremeasuredText text = PremeasuredText.build(
                    generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
            state.resumeTiming();

            StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
                    .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
                    .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
                    .build();
        }
    }

    @Test
    public void testCreate_MeasuredText_Styled_Greedy_NoHyphenation() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            state.pauseTiming();
            final PremeasuredText text = PremeasuredText.build(
                    generateRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT, LTR);
            state.resumeTiming();

            StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
                    .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
                    .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
                    .build();
        }
    }
}
+21 −0
Original line number Diff line number Diff line
@@ -42277,6 +42277,27 @@ package android.text {
    method public abstract int getSpanTypeId();
  }
  public class PremeasuredText implements android.text.Spanned {
    method public static android.text.PremeasuredText build(java.lang.CharSequence, android.text.TextPaint, android.text.TextDirectionHeuristic);
    method public static android.text.PremeasuredText build(java.lang.CharSequence, android.text.TextPaint, android.text.TextDirectionHeuristic, int, int);
    method public char charAt(int);
    method public int getEnd();
    method public android.text.TextPaint getPaint();
    method public int getParagraphCount();
    method public int getParagraphEnd(int);
    method public int getParagraphStart(int);
    method public int getSpanEnd(java.lang.Object);
    method public int getSpanFlags(java.lang.Object);
    method public int getSpanStart(java.lang.Object);
    method public <T> T[] getSpans(int, int, java.lang.Class<T>);
    method public int getStart();
    method public java.lang.CharSequence getText();
    method public android.text.TextDirectionHeuristic getTextDir();
    method public int length();
    method public int nextSpanTransition(int, int, java.lang.Class);
    method public java.lang.CharSequence subSequence(int, int);
  }
  public class Selection {
    method public static boolean extendDown(android.text.Spannable, android.text.Layout);
    method public static boolean extendLeft(android.text.Spannable, android.text.Layout);
+272 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.annotation.IntRange;
import android.annotation.NonNull;
import android.util.IntArray;

import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;

import java.util.ArrayList;

/**
 * A text which has already been measured.
 *
 * TODO: Rename to better name? e.g. MeasuredText, FrozenText etc.
 */
public class PremeasuredText implements Spanned {
    private static final char LINE_FEED = '\n';

    // The original text.
    private final @NonNull CharSequence mText;

    // The inclusive start offset of the measuring target.
    private final @IntRange(from = 0) int mStart;

    // The exclusive end offset of the measuring target.
    private final @IntRange(from = 0) int mEnd;

    // The TextPaint used for measurement.
    private final @NonNull TextPaint mPaint;

    // The requested text direction.
    private final @NonNull TextDirectionHeuristic mTextDir;

    // The measured paragraph texts.
    private final @NonNull MeasuredText[] mMeasuredTexts;

    // The sorted paragraph end offsets.
    private final @NonNull int[] mParagraphBreakPoints;

    /**
     * Build PremeasuredText from the text.
     *
     * @param text The text to be measured.
     * @param paint The paint to be used for drawing.
     * @param textDir The text direction.
     * @return The measured text.
     */
    public static @NonNull PremeasuredText build(@NonNull CharSequence text,
                                                 @NonNull TextPaint paint,
                                                 @NonNull TextDirectionHeuristic textDir) {
        return PremeasuredText.build(text, paint, textDir, 0, text.length());
    }

    /**
     * Build PremeasuredText from the specific range of the text..
     *
     * @param text The text to be measured.
     * @param paint The paint to be used for drawing.
     * @param textDir The text direction.
     * @param start The inclusive start offset of the text.
     * @param end The exclusive start offset of the text.
     * @return The measured text.
     */
    public static @NonNull PremeasuredText build(@NonNull CharSequence text,
                                                 @NonNull TextPaint paint,
                                                 @NonNull TextDirectionHeuristic textDir,
                                                 @IntRange(from = 0) int start,
                                                 @IntRange(from = 0) int end) {
        Preconditions.checkNotNull(text);
        Preconditions.checkNotNull(paint);
        Preconditions.checkNotNull(textDir);
        Preconditions.checkArgumentInRange(start, 0, text.length(), "start");
        Preconditions.checkArgumentInRange(end, 0, text.length(), "end");

        final IntArray paragraphEnds = new IntArray();
        final ArrayList<MeasuredText> measuredTexts = new ArrayList<>();

        int paraEnd = 0;
        for (int paraStart = start; paraStart < end; paraStart = paraEnd) {
            paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
            if (paraEnd < 0) {
                // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph end.
                paraEnd = end;
            } else {
                paraEnd++;  // Includes LINE_FEED(U+000A) to the prev paragraph.
            }

            paragraphEnds.add(paraEnd);
            measuredTexts.add(MeasuredText.buildForStaticLayout(
                    paint, text, paraStart, paraEnd, textDir, null /* no recycle */));
        }

        return new PremeasuredText(text, start, end, paint, textDir,
                                   measuredTexts.toArray(new MeasuredText[measuredTexts.size()]),
                                   paragraphEnds.toArray());
    }

    // Use PremeasuredText.build instead.
    private PremeasuredText(@NonNull CharSequence text,
                            @IntRange(from = 0) int start,
                            @IntRange(from = 0) int end,
                            @NonNull TextPaint paint,
                            @NonNull TextDirectionHeuristic textDir,
                            @NonNull MeasuredText[] measuredTexts,
                            @NonNull int[] paragraphBreakPoints) {
        mText = text;
        mStart = start;
        mEnd = end;
        mPaint = paint;
        mMeasuredTexts = measuredTexts;
        mParagraphBreakPoints = paragraphBreakPoints;
        mTextDir = textDir;
    }

    /**
     * Return the underlying text.
     */
    public @NonNull CharSequence getText() {
        return mText;
    }

    /**
     * Returns the inclusive start offset of measured region.
     */
    public @IntRange(from = 0) int getStart() {
        return mStart;
    }

    /**
     * Returns the exclusive end offset of measured region.
     */
    public @IntRange(from = 0) int getEnd() {
        return mEnd;
    }

    /**
     * Returns the text direction associated with char sequence.
     */
    public @NonNull TextDirectionHeuristic getTextDir() {
        return mTextDir;
    }

    /**
     * Returns the paint used to measure this text.
     */
    public @NonNull TextPaint getPaint() {
        return mPaint;
    }

    /**
     * Returns the length of the paragraph of this text.
     */
    public @IntRange(from = 0) int getParagraphCount() {
        return mParagraphBreakPoints.length;
    }

    /**
     * Returns the paragraph start offset of the text.
     */
    public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
        Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
        return paraIndex == 0 ? mStart : mParagraphBreakPoints[paraIndex - 1];
    }

    /**
     * Returns the paragraph end offset of the text.
     */
    public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
        Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
        return mParagraphBreakPoints[paraIndex];
    }

    /** @hide */
    public @NonNull MeasuredText getMeasuredText(@IntRange(from = 0) int paraIndex) {
        return mMeasuredTexts[paraIndex];
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Spanned overrides
    //
    // Just proxy for underlying mText if appropriate.

    @Override
    public <T> T[] getSpans(int start, int end, Class<T> type) {
        if (mText instanceof Spanned) {
            return ((Spanned) mText).getSpans(start, end, type);
        } else {
            return ArrayUtils.emptyArray(type);
        }
    }

    @Override
    public int getSpanStart(Object tag) {
        if (mText instanceof Spanned) {
            return ((Spanned) mText).getSpanStart(tag);
        } else {
            return -1;
        }
    }

    @Override
    public int getSpanEnd(Object tag) {
        if (mText instanceof Spanned) {
            return ((Spanned) mText).getSpanEnd(tag);
        } else {
            return -1;
        }
    }

    @Override
    public int getSpanFlags(Object tag) {
        if (mText instanceof Spanned) {
            return ((Spanned) mText).getSpanFlags(tag);
        } else {
            return 0;
        }
    }

    @Override
    public int nextSpanTransition(int start, int limit, Class type) {
        if (mText instanceof Spanned) {
            return ((Spanned) mText).nextSpanTransition(start, limit, type);
        } else {
            return mText.length();
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // CharSequence overrides.
    //
    // Just proxy for underlying mText.

    @Override
    public int length() {
        return mText.length();
    }

    @Override
    public char charAt(int index) {
        // TODO: Should this be index + mStart ?
        return mText.charAt(index);
    }

    @Override
    public CharSequence subSequence(int start, int end) {
        // TODO: return PremeasuredText.
        // TODO: Should this be index + mStart, end + mStart ?
        return mText.subSequence(start, end);
    }

    @Override
    public String toString() {
        return mText.toString();
    }
}
+32 −21
Original line number Diff line number Diff line
@@ -610,8 +610,8 @@ public class StaticLayout extends Layout {

    /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
        final CharSequence source = b.mText;
        int bufStart = b.mStart;
        int bufEnd = b.mEnd;
        final int bufStart = b.mStart;
        final int bufEnd = b.mEnd;
        TextPaint paint = b.mPaint;
        int outerWidth = b.mWidth;
        TextDirectionHeuristic textDir = b.mTextDir;
@@ -634,10 +634,6 @@ public class StaticLayout extends Layout {
        Paint.FontMetricsInt fm = b.mFontMetricsInt;
        int[] chooseHtv = null;

        Spanned spanned = null;
        if (source instanceof Spanned)
            spanned = (Spanned) source;

        final int[] indents;
        if (mLeftIndents != null || mRightIndents != null) {
            final int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length;
@@ -660,17 +656,35 @@ public class StaticLayout extends Layout {
                b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
                indents, mLeftPaddings, mRightPaddings);

        MeasuredText measured = null;
        try {
            int paraEnd;
            for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
                paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
                if (paraEnd < 0) {
                    paraEnd = bufEnd;
        PremeasuredText premeasured = null;
        final Spanned spanned;
        if (source instanceof PremeasuredText) {
            premeasured = (PremeasuredText) source;

            final CharSequence original = premeasured.getText();
            spanned = (original instanceof Spanned) ? (Spanned) original : null;

            if (bufStart != premeasured.getStart() || bufEnd != premeasured.getEnd()) {
                // The buffer position has changed. Re-measure here.
                premeasured = PremeasuredText.build(original, paint, textDir, bufStart, bufEnd);
            } else {
                // We can use premeasured information.

                // Overwrite with the one when premeasured.
                // TODO: Give an option for developer not to overwrite and measure again here?
                textDir = premeasured.getTextDir();
                paint = premeasured.getPaint();
            }
        } else {
                    paraEnd++;
            premeasured = PremeasuredText.build(source, paint, textDir, bufStart, bufEnd);
            spanned = (source instanceof Spanned) ? (Spanned) source : null;
        }

        try {
            for (int paraIndex = 0; paraIndex < premeasured.getParagraphCount(); paraIndex++) {
                final int paraStart = premeasured.getParagraphStart(paraIndex);
                final int paraEnd = premeasured.getParagraphEnd(paraIndex);

                int firstWidthLineCount = 1;
                int firstWidth = outerWidth;
                int restWidth = outerWidth;
@@ -735,8 +749,7 @@ public class StaticLayout extends Layout {
                    }
                }

                measured = MeasuredText.buildForStaticLayout(
                        paint, source, paraStart, paraEnd, textDir, measured);
                final MeasuredText measured = premeasured.getMeasuredText(paraIndex);
                final char[] chs = measured.getChars();
                final int[] spanEndCache = measured.getSpanEndCache().getRawArray();
                final int[] fmCache = measured.getFontMetrics().getRawArray();
@@ -887,7 +900,8 @@ public class StaticLayout extends Layout {

            if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
                    && mLineCount < mMaximumVisibleLineCount) {
                measured = MeasuredText.buildForBidi(source, bufEnd, bufEnd, textDir, measured);
                final MeasuredText measured =
                        MeasuredText.buildForBidi(source, bufEnd, bufEnd, textDir, null);
                paint.getFontMetricsInt(fm);
                v = out(source,
                        bufEnd, bufEnd, fm.ascent, fm.descent,
@@ -901,9 +915,6 @@ public class StaticLayout extends Layout {
                        ellipsizedWidth, 0, paint, false);
            }
        } finally {
            if (measured != null) {
                measured.recycle();
            }
            nFinish(nativePtr);
        }
    }
+8 −2
Original line number Diff line number Diff line
@@ -77,6 +77,7 @@ import android.text.InputFilter;
import android.text.InputType;
import android.text.Layout;
import android.text.ParcelableSpan;
import android.text.PremeasuredText;
import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
@@ -5326,7 +5327,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            if (imm != null) imm.restartInput(this);
        } else if (type == BufferType.SPANNABLE || mMovement != null) {
            text = mSpannableFactory.newSpannable(text);
        } else if (!(text instanceof CharWrapper)) {
        } else if (!(text instanceof PremeasuredText || text instanceof CharWrapper)) {
            text = TextUtils.stringOrSpannedString(text);
        }

@@ -5610,10 +5611,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                spannable = (Spannable) text;
            } else {
                spannable = mSpannableFactory.newSpannable(text);
                text = spannable;
            }

            SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
            if (spans.length == 0) {
                return text;
            } else {
                text = spannable;
            }

            for (int i = 0; i < spans.length; i++) {
                spannable.removeSpan(spans[i]);
            }