Recompute PcT with existing PcT for different direction
The text direction can not be fully determined in detached state.
To improve even in that case, compute PrecomputedText from existing
PrecomputedText with new direction.
Here is the performance difference. According to the perf test result,
up to 80% of computation can be recycled from existing PrecomputedText.
android.text.StaticLayoutPerfTest (u sec):
PrecomputedText Greedy NoHyphenation : 371 -> 371: ( +0, +0.0%)
PrecomputedText Greedy NoHyphenation DirDifferent: 6,923 -> 1,437: (-5486, -79.2%)
RandomText Greedy NoHyphenation : 6,633 -> 6,627: ( -6, -0.1%)
On the other hand, this CL increase the memory usage of the
PrecomputedText up to 10%. Here is an reference memory usage.
android.text.PrecomputedTextMemoryUsageTest (bytes):
MemoryUsage
Arabic Hyphenation : 17,135 -> 18,116: ( +981, +5.7%)
Arabic NoHyphenation : 17,135 -> 18,116: ( +981, +5.7%)
CJK Hyphenation : 29,000 -> 31,584: (+2584, +8.9%)
CJK NoHyphenation : 29,000 -> 31,584: (+2584, +8.9%)
Latin Hyphenation : 16,526 -> 17,185: ( +659, +4.0%)
Latin NoHyphenation : 14,200 -> 14,784: ( +584, +4.1%)
Bug: 119312268
Test: atest CtsWidgetTestCases
Test: atest CtsTextTestCases
Test: atest CtsGraphicsTestCases
Test: minikin_tests
Change-Id: Ia02c201afac5d7d1c086a45f15696f39a6b2a76c
diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java
index f9370a8..7e41878 100644
--- a/core/java/android/text/MeasuredParagraph.java
+++ b/core/java/android/text/MeasuredParagraph.java
@@ -377,6 +377,9 @@
* @param start the inclusive start offset of the target region in the text
* @param end the exclusive end offset of the target region in the text
* @param textDir the text direction
+ * @param computeHyphenation true if need to compute hyphenation, otherwise false
+ * @param computeLayout true if need to compute full layout, otherwise false.
+ * @param hint pass if you already have measured paragraph.
* @param recycle pass existing MeasuredParagraph if you want to recycle it.
*
* @return measured text
@@ -389,12 +392,18 @@
@NonNull TextDirectionHeuristic textDir,
boolean computeHyphenation,
boolean computeLayout,
+ @Nullable MeasuredParagraph hint,
@Nullable MeasuredParagraph recycle) {
final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
mt.resetAndAnalyzeBidi(text, start, end, textDir);
- final MeasuredText.Builder builder = new MeasuredText.Builder(mt.mCopiedBuffer);
- builder.setComputeHyphenation(computeHyphenation);
- builder.setComputeLayout(computeLayout);
+ final MeasuredText.Builder builder;
+ if (hint == null) {
+ builder = new MeasuredText.Builder(mt.mCopiedBuffer)
+ .setComputeHyphenation(computeHyphenation)
+ .setComputeLayout(computeLayout);
+ } else {
+ builder = new MeasuredText.Builder(hint.mMeasuredText);
+ }
if (mt.mTextLength == 0) {
// Need to build empty native measured text for StaticLayout.
// TODO: Stop creating empty measured text for empty lines.
diff --git a/core/java/android/text/PrecomputedText.java b/core/java/android/text/PrecomputedText.java
index b7ea012..08741d6 100644
--- a/core/java/android/text/PrecomputedText.java
+++ b/core/java/android/text/PrecomputedText.java
@@ -17,6 +17,7 @@
package android.text;
import android.annotation.FloatRange;
+import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -25,6 +26,8 @@
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Objects;
@@ -119,6 +122,16 @@
}
/**
+ * Builder constructor from existing params.
+ */
+ public Builder(@NonNull Params params) {
+ mPaint = params.mPaint;
+ mTextDir = params.mTextDir;
+ mBreakStrategy = params.mBreakStrategy;
+ mHyphenationFrequency = params.mHyphenationFrequency;
+ }
+
+ /**
* Set the line break strategy.
*
* The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}.
@@ -220,13 +233,41 @@
}
/** @hide */
- public boolean isSameTextMetricsInternal(@NonNull TextPaint paint,
+ @IntDef(value = { UNUSABLE, NEED_RECOMPUTE, USABLE })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CheckResultUsableResult {}
+
+ /**
+ * Constant for returning value of checkResultUsable indicating that given parameter is not
+ * compatible.
+ * @hide
+ */
+ public static final int UNUSABLE = 0;
+
+ /**
+ * Constant for returning value of checkResultUsable indicating that given parameter is not
+ * compatible but partially usable for creating new PrecomputedText.
+ * @hide
+ */
+ public static final int NEED_RECOMPUTE = 1;
+
+ /**
+ * Constant for returning value of checkResultUsable indicating that given parameter is
+ * compatible.
+ * @hide
+ */
+ public static final int USABLE = 2;
+
+ /** @hide */
+ public @CheckResultUsableResult int checkResultUsable(@NonNull TextPaint paint,
@NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy,
@Layout.HyphenationFrequency int frequency) {
- return mTextDir == textDir
- && mBreakStrategy == strategy
- && mHyphenationFrequency == frequency
- && mPaint.equalsForTextMeasurement(paint);
+ if (mBreakStrategy == strategy && mHyphenationFrequency == frequency
+ && mPaint.equalsForTextMeasurement(paint)) {
+ return mTextDir == textDir ? USABLE : NEED_RECOMPUTE;
+ } else {
+ return UNUSABLE;
+ }
}
/**
@@ -243,8 +284,8 @@
return false;
}
Params param = (Params) o;
- return isSameTextMetricsInternal(param.mPaint, param.mTextDir, param.mBreakStrategy,
- param.mHyphenationFrequency);
+ return checkResultUsable(param.mPaint, param.mTextDir, param.mBreakStrategy,
+ param.mHyphenationFrequency) == Params.USABLE;
}
@Override
@@ -321,11 +362,55 @@
* @return A {@link PrecomputedText}
*/
public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params params) {
- ParagraphInfo[] paraInfo = createMeasuredParagraphs(
- text, params, 0, text.length(), true /* computeLayout */);
+ ParagraphInfo[] paraInfo = null;
+ if (text instanceof PrecomputedText) {
+ final PrecomputedText hintPct = (PrecomputedText) text;
+ final PrecomputedText.Params hintParams = hintPct.getParams();
+ final @Params.CheckResultUsableResult int checkResult =
+ hintParams.checkResultUsable(params.mPaint, params.mTextDir,
+ params.mBreakStrategy, params.mHyphenationFrequency);
+ switch (checkResult) {
+ case Params.USABLE:
+ return hintPct;
+ case Params.NEED_RECOMPUTE:
+ // To be able to use PrecomputedText for new params, at least break strategy and
+ // hyphenation frequency must be the same.
+ if (params.getBreakStrategy() == hintParams.getBreakStrategy()
+ && params.getHyphenationFrequency()
+ == hintParams.getHyphenationFrequency()) {
+ paraInfo = createMeasuredParagraphsFromPrecomputedText(
+ hintPct, params, true /* compute layout */);
+ }
+ break;
+ case Params.UNUSABLE:
+ // Unable to use anything in PrecomputedText. Create PrecomputedText as the
+ // normal text input.
+ }
+
+ }
+ if (paraInfo == null) {
+ paraInfo = createMeasuredParagraphs(
+ text, params, 0, text.length(), true /* computeLayout */);
+ }
return new PrecomputedText(text, 0, text.length(), params, paraInfo);
}
+ private static ParagraphInfo[] createMeasuredParagraphsFromPrecomputedText(
+ @NonNull PrecomputedText pct, @NonNull Params params, boolean computeLayout) {
+ final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE
+ && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE;
+ ArrayList<ParagraphInfo> result = new ArrayList<>();
+ for (int i = 0; i < pct.getParagraphCount(); ++i) {
+ final int paraStart = pct.getParagraphStart(i);
+ final int paraEnd = pct.getParagraphEnd(i);
+ result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
+ params.getTextPaint(), pct, paraStart, paraEnd, params.getTextDirection(),
+ needHyphenation, computeLayout, pct.getMeasuredParagraph(i),
+ null /* no recycle */)));
+ }
+ return result.toArray(new ParagraphInfo[result.size()]);
+ }
+
/** @hide */
public static ParagraphInfo[] createMeasuredParagraphs(
@NonNull CharSequence text, @NonNull Params params,
@@ -350,7 +435,8 @@
result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
params.getTextPaint(), text, paraStart, paraEnd, params.getTextDirection(),
- needHyphenation, computeLayout, null /* no recycle */)));
+ needHyphenation, computeLayout, null /* no hint */,
+ null /* no recycle */)));
}
return result.toArray(new ParagraphInfo[result.size()]);
}
@@ -434,12 +520,15 @@
* Returns true if the given TextPaint gives the same result of text layout for this text.
* @hide
*/
- public boolean canUseMeasuredResult(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
- @NonNull TextDirectionHeuristic textDir, @NonNull TextPaint paint,
- @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) {
- return mStart == start
- && mEnd == end
- && mParams.isSameTextMetricsInternal(paint, textDir, strategy, frequency);
+ public @Params.CheckResultUsableResult int checkResultUsable(@IntRange(from = 0) int start,
+ @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir,
+ @NonNull TextPaint paint, @Layout.BreakStrategy int strategy,
+ @Layout.HyphenationFrequency int frequency) {
+ if (mStart != start || mEnd != end) {
+ return Params.UNUSABLE;
+ } else {
+ return mParams.checkResultUsable(paint, textDir, strategy, frequency);
+ }
}
/** @hide */
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 8cb18b2..3d0c662 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -650,10 +650,26 @@
final Spanned spanned = (source instanceof Spanned) ? (Spanned) source : null;
if (source instanceof PrecomputedText) {
PrecomputedText precomputed = (PrecomputedText) source;
- if (precomputed.canUseMeasuredResult(bufStart, bufEnd, textDir, paint,
- b.mBreakStrategy, b.mHyphenationFrequency)) {
- // Some parameters are different from the ones when measured text is created.
- paragraphInfo = precomputed.getParagraphInfo();
+ final @PrecomputedText.Params.CheckResultUsableResult int checkResult =
+ precomputed.checkResultUsable(bufStart, bufEnd, textDir, paint,
+ b.mBreakStrategy, b.mHyphenationFrequency);
+ switch (checkResult) {
+ case PrecomputedText.Params.UNUSABLE:
+ break;
+ case PrecomputedText.Params.NEED_RECOMPUTE:
+ final PrecomputedText.Params newParams =
+ new PrecomputedText.Params.Builder(paint)
+ .setBreakStrategy(b.mBreakStrategy)
+ .setHyphenationFrequency(b.mHyphenationFrequency)
+ .setTextDirection(textDir)
+ .build();
+ precomputed = PrecomputedText.create(precomputed, newParams);
+ paragraphInfo = precomputed.getParagraphInfo();
+ break;
+ case PrecomputedText.Params.USABLE:
+ // Some parameters are different from the ones when measured text is created.
+ paragraphInfo = precomputed.getParagraphInfo();
+ break;
}
}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 2a42232..a973595 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -6028,14 +6028,22 @@
if (mTextDir == null) {
mTextDir = getTextDirectionHeuristic();
}
- if (!precomputed.getParams().isSameTextMetricsInternal(
- getPaint(), mTextDir, mBreakStrategy, mHyphenationFrequency)) {
- throw new IllegalArgumentException(
+ final @PrecomputedText.Params.CheckResultUsableResult int checkResult =
+ precomputed.getParams().checkResultUsable(getPaint(), mTextDir, mBreakStrategy,
+ mHyphenationFrequency);
+ switch (checkResult) {
+ case PrecomputedText.Params.UNUSABLE:
+ throw new IllegalArgumentException(
"PrecomputedText's Parameters don't match the parameters of this TextView."
+ "Consider using setTextMetricsParams(precomputedText.getParams()) "
+ "to override the settings of this TextView: "
+ "PrecomputedText: " + precomputed.getParams()
+ "TextView: " + getTextMetricsParams());
+ case PrecomputedText.Params.NEED_RECOMPUTE:
+ precomputed = PrecomputedText.create(precomputed, getTextMetricsParams());
+ break;
+ case PrecomputedText.Params.USABLE:
+ // pass through
}
} else if (type == BufferType.SPANNABLE || mMovement != null) {
text = mSpannableFactory.newSpannable(text);
diff --git a/core/jni/android/graphics/text/MeasuredText.cpp b/core/jni/android/graphics/text/MeasuredText.cpp
index 0bfadb4..d7d96fb 100644
--- a/core/jni/android/graphics/text/MeasuredText.cpp
+++ b/core/jni/android/graphics/text/MeasuredText.cpp
@@ -84,14 +84,14 @@
// Regular JNI
static jlong nBuildMeasuredText(JNIEnv* env, jclass /* unused */, jlong builderPtr,
- jcharArray javaText, jboolean computeHyphenation,
- jboolean computeLayout) {
+ jlong hintPtr, jcharArray javaText, jboolean computeHyphenation,
+ jboolean computeLayout) {
ScopedCharArrayRO text(env, javaText);
const minikin::U16StringPiece textBuffer(text.get(), text.size());
// Pass the ownership to Java.
- return toJLong(toBuilder(builderPtr)->build(textBuffer, computeHyphenation,
- computeLayout).release());
+ return toJLong(toBuilder(builderPtr)->build(textBuffer, computeHyphenation, computeLayout,
+ toMeasuredParagraph(hintPtr)).release());
}
// Regular JNI
@@ -147,7 +147,7 @@
{"nInitBuilder", "()J", (void*) nInitBuilder},
{"nAddStyleRun", "(JJIIZ)V", (void*) nAddStyleRun},
{"nAddReplacementRun", "(JJIIF)V", (void*) nAddReplacementRun},
- {"nBuildMeasuredText", "(J[CZZ)J", (void*) nBuildMeasuredText},
+ {"nBuildMeasuredText", "(JJ[CZZ)J", (void*) nBuildMeasuredText},
{"nFreeBuilder", "(J)V", (void*) nFreeBuilder},
};
diff --git a/core/tests/coretests/src/android/text/MeasuredParagraphTest.java b/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
index 3d15eb9..a0dca2c 100644
--- a/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
+++ b/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
@@ -133,7 +133,8 @@
public void buildForStaticLayout() {
MeasuredParagraph mt = null;
- mt = MeasuredParagraph.buildForStaticLayout(PAINT, "XXX", 0, 3, LTR, false, false, null);
+ mt = MeasuredParagraph.buildForStaticLayout(
+ PAINT, "XXX", 0, 3, LTR, false, false, null /* no hint */, null);
assertNotNull(mt);
assertNotNull(mt.getChars());
assertEquals("XXX", charsToString(mt.getChars()));
@@ -147,8 +148,8 @@
assertNotNull(mt.getMeasuredText());
// Recycle it
- MeasuredParagraph mt2 =
- MeasuredParagraph.buildForStaticLayout(PAINT, "_VVV_", 1, 4, RTL, false, false, mt);
+ MeasuredParagraph mt2 = MeasuredParagraph.buildForStaticLayout(
+ PAINT, "_VVV_", 1, 4, RTL, false, false, null /* no hint */, mt);
assertEquals(mt2, mt);
assertNotNull(mt2.getChars());
assertEquals("VVV", charsToString(mt.getChars()));