Implement fast hyphenation algorithm
RandomText Balanced Hyphenation ON
min: 2,312,291
mean: 2,320,574
median: 2,320,112
RandomText Balanced Hyphenation ON(Fast)
min: 985,850
mean: 990,922
median: 986,586
RandomText Balanced Hyphenation OFF
min: 909,060
median: 923,585
mean: 930,052
Bug: 201096525
Test: TreeHugger and minikin_tests
Change-Id: Ide2d73acb3de85e91018e524c89df2c949235a4e
diff --git a/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java b/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java
index 8a6c60f..1cd5d96 100644
--- a/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java
+++ b/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java
@@ -138,6 +138,21 @@
}
@Test
+ public void testCreate_RandomText_NoStyled_Balanced_Hyphenation_Fast() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = mTextUtil.nextRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ state.resumeTiming();
+
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL_FAST)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
+ .build();
+ }
+ }
+
+ @Test
public void testCreate_RandomText_Styled_Greedy_NoHyphenation() {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
diff --git a/core/api/current.txt b/core/api/current.txt
index 9514447..7198285 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -17264,8 +17264,12 @@
method @NonNull public android.graphics.text.MeasuredText.Builder appendReplacementRun(@NonNull android.graphics.Paint, @IntRange(from=0) int, @FloatRange(from=0) @Px float);
method @NonNull public android.graphics.text.MeasuredText.Builder appendStyleRun(@NonNull android.graphics.Paint, @IntRange(from=0) int, boolean);
method @NonNull public android.graphics.text.MeasuredText build();
- method @NonNull public android.graphics.text.MeasuredText.Builder setComputeHyphenation(boolean);
+ method @Deprecated @NonNull public android.graphics.text.MeasuredText.Builder setComputeHyphenation(boolean);
+ method @NonNull public android.graphics.text.MeasuredText.Builder setComputeHyphenation(int);
method @NonNull public android.graphics.text.MeasuredText.Builder setComputeLayout(boolean);
+ field public static final int HYPHENATION_MODE_FAST = 2; // 0x2
+ field public static final int HYPHENATION_MODE_NONE = 0; // 0x0
+ field public static final int HYPHENATION_MODE_NORMAL = 1; // 0x1
}
public final class PositionedGlyphs {
@@ -44451,8 +44455,10 @@
field public static final int DIR_LEFT_TO_RIGHT = 1; // 0x1
field public static final int DIR_RIGHT_TO_LEFT = -1; // 0xffffffff
field public static final int HYPHENATION_FREQUENCY_FULL = 2; // 0x2
+ field public static final int HYPHENATION_FREQUENCY_FULL_FAST = 4; // 0x4
field public static final int HYPHENATION_FREQUENCY_NONE = 0; // 0x0
field public static final int HYPHENATION_FREQUENCY_NORMAL = 1; // 0x1
+ field public static final int HYPHENATION_FREQUENCY_NORMAL_FAST = 3; // 0x3
field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1
field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0
}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 505f400..da3e9b6 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -82,7 +82,9 @@
/** @hide */
@IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = {
HYPHENATION_FREQUENCY_NORMAL,
+ HYPHENATION_FREQUENCY_NORMAL_FAST,
HYPHENATION_FREQUENCY_FULL,
+ HYPHENATION_FREQUENCY_FULL_FAST,
HYPHENATION_FREQUENCY_NONE
})
@Retention(RetentionPolicy.SOURCE)
@@ -95,21 +97,40 @@
* layout and there is otherwise no valid break. Soft hyphens are ignored and will not be used
* as suggestions for potential line breaks.
*/
- public static final int HYPHENATION_FREQUENCY_NONE = LineBreaker.HYPHENATION_FREQUENCY_NONE;
+ public static final int HYPHENATION_FREQUENCY_NONE = 0;
/**
* Value for hyphenation frequency indicating a light amount of automatic hyphenation, which
* is a conservative default. Useful for informal cases, such as short sentences or chat
* messages.
*/
- public static final int HYPHENATION_FREQUENCY_NORMAL = LineBreaker.HYPHENATION_FREQUENCY_NORMAL;
+ public static final int HYPHENATION_FREQUENCY_NORMAL = 1;
/**
* Value for hyphenation frequency indicating the full amount of automatic hyphenation, typical
* in typography. Useful for running text and where it's important to put the maximum amount of
* text in a screen with limited space.
*/
- public static final int HYPHENATION_FREQUENCY_FULL = LineBreaker.HYPHENATION_FREQUENCY_FULL;
+ public static final int HYPHENATION_FREQUENCY_FULL = 2;
+
+ /**
+ * Value for hyphenation frequency indicating a light amount of automatic hyphenation with
+ * using faster algorithm.
+ *
+ * This option is useful for informal cases, such as short sentences or chat messages. To make
+ * text rendering faster with hyphenation, this algorithm ignores some hyphen character related
+ * typographic features, e.g. kerning.
+ */
+ public static final int HYPHENATION_FREQUENCY_NORMAL_FAST = 3;
+ /**
+ * Value for hyphenation frequency indicating the full amount of automatic hyphenation with
+ * using faster algorithm.
+ *
+ * This option is useful for running text and where it's important to put the maximum amount of
+ * text in a screen with limited space. To make text rendering faster with hyphenation, this
+ * algorithm ignores some hyphen character related typographic features, e.g. kerning.
+ */
+ public static final int HYPHENATION_FREQUENCY_FULL_FAST = 4;
private static final ParagraphStyle[] NO_PARA_SPANS =
ArrayUtils.emptyArray(ParagraphStyle.class);
diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java
index 7e41878..6a3c618 100644
--- a/core/java/android/text/MeasuredParagraph.java
+++ b/core/java/android/text/MeasuredParagraph.java
@@ -377,7 +377,7 @@
* @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 hyphenationMode a hyphenation mode
* @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.
@@ -390,7 +390,7 @@
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@NonNull TextDirectionHeuristic textDir,
- boolean computeHyphenation,
+ int hyphenationMode,
boolean computeLayout,
@Nullable MeasuredParagraph hint,
@Nullable MeasuredParagraph recycle) {
@@ -399,7 +399,7 @@
final MeasuredText.Builder builder;
if (hint == null) {
builder = new MeasuredText.Builder(mt.mCopiedBuffer)
- .setComputeHyphenation(computeHyphenation)
+ .setComputeHyphenation(hyphenationMode)
.setComputeLayout(computeLayout);
} else {
builder = new MeasuredText.Builder(hint.mMeasuredText);
diff --git a/core/java/android/text/PrecomputedText.java b/core/java/android/text/PrecomputedText.java
index 08741d6..152570f 100644
--- a/core/java/android/text/PrecomputedText.java
+++ b/core/java/android/text/PrecomputedText.java
@@ -22,6 +22,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
+import android.graphics.text.MeasuredText;
import android.text.style.MetricAffectingSpan;
import com.android.internal.util.Preconditions;
@@ -395,17 +396,30 @@
return new PrecomputedText(text, 0, text.length(), params, paraInfo);
}
+ private static boolean isFastHyphenation(int frequency) {
+ return frequency == Layout.HYPHENATION_FREQUENCY_FULL_FAST
+ || frequency == Layout.HYPHENATION_FREQUENCY_NORMAL_FAST;
+ }
+
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;
+ final int hyphenationMode;
+ if (needHyphenation) {
+ hyphenationMode = isFastHyphenation(params.getHyphenationFrequency())
+ ? MeasuredText.Builder.HYPHENATION_MODE_FAST :
+ MeasuredText.Builder.HYPHENATION_MODE_NORMAL;
+ } else {
+ hyphenationMode = MeasuredText.Builder.HYPHENATION_MODE_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),
+ hyphenationMode, computeLayout, pct.getMeasuredParagraph(i),
null /* no recycle */)));
}
return result.toArray(new ParagraphInfo[result.size()]);
@@ -421,6 +435,14 @@
Preconditions.checkNotNull(params);
final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE
&& params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE;
+ final int hyphenationMode;
+ if (needHyphenation) {
+ hyphenationMode = isFastHyphenation(params.getHyphenationFrequency())
+ ? MeasuredText.Builder.HYPHENATION_MODE_FAST :
+ MeasuredText.Builder.HYPHENATION_MODE_NORMAL;
+ } else {
+ hyphenationMode = MeasuredText.Builder.HYPHENATION_MODE_NONE;
+ }
int paraEnd = 0;
for (int paraStart = start; paraStart < end; paraStart = paraEnd) {
@@ -435,8 +457,7 @@
result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
params.getTextPaint(), text, paraStart, paraEnd, params.getTextDirection(),
- needHyphenation, computeLayout, null /* no hint */,
- null /* no recycle */)));
+ hyphenationMode, computeLayout, null /* no hint */, null /* no recycle */)));
}
return result.toArray(new ParagraphInfo[result.size()]);
}
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index f99d430..6984e4d 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -591,6 +591,20 @@
generate(b, b.mIncludePad, b.mIncludePad);
}
+ private static int getBaseHyphenationFrequency(int frequency) {
+ switch (frequency) {
+ case Layout.HYPHENATION_FREQUENCY_FULL:
+ case Layout.HYPHENATION_FREQUENCY_FULL_FAST:
+ return LineBreaker.HYPHENATION_FREQUENCY_FULL;
+ case Layout.HYPHENATION_FREQUENCY_NORMAL:
+ case Layout.HYPHENATION_FREQUENCY_NORMAL_FAST:
+ return LineBreaker.HYPHENATION_FREQUENCY_NORMAL;
+ case Layout.HYPHENATION_FREQUENCY_NONE:
+ default:
+ return LineBreaker.HYPHENATION_FREQUENCY_NONE;
+ }
+ }
+
/* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
final CharSequence source = b.mText;
final int bufStart = b.mStart;
@@ -641,7 +655,7 @@
final LineBreaker lineBreaker = new LineBreaker.Builder()
.setBreakStrategy(b.mBreakStrategy)
- .setHyphenationFrequency(b.mHyphenationFrequency)
+ .setHyphenationFrequency(getBaseHyphenationFrequency(b.mHyphenationFrequency))
// TODO: Support more justification mode, e.g. letter spacing, stretching.
.setJustificationMode(b.mJustificationMode)
.setIndents(indents)
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 819857f..dba7b51 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5383,6 +5383,16 @@
<!-- Standard amount of hyphenation, useful for running text and for
screens with limited space for text. -->
<enum name="full" value="2" />
+
+ <!-- Same to hyphenationFrequency="normal" but using faster algorithm for measuring
+ hyphenation break points. To make text rendering faster with hyphenation, this algorithm
+ ignores some hyphen character related typographic features, e.g. kerning. -->
+ <enum name="normalFast" value="3" />
+
+ <!-- Same to hyphenationFrequency="full" but using faster algorithm for measuring
+ hyphenation break points. To make text rendering faster with hyphenation, this algorithm
+ ignores some hyphen character related typographic features, e.g. kerning. -->
+ <enum name="fullFast" value="4" />
</attr>
<!-- Specify the type of auto-size. Note that this feature is not supported by EditText,
works only for TextView. -->
diff --git a/core/tests/coretests/src/android/text/MeasuredParagraphTest.java b/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
index 57bb434..d6a7682 100644
--- a/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
+++ b/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.graphics.Typeface;
+import android.graphics.text.MeasuredText;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -135,7 +136,8 @@
MeasuredParagraph mt = null;
mt = MeasuredParagraph.buildForStaticLayout(
- PAINT, "XXX", 0, 3, LTR, false, false, null /* no hint */, null);
+ PAINT, "XXX", 0, 3, LTR, MeasuredText.Builder.HYPHENATION_MODE_NONE, false,
+ null /* no hint */, null);
assertNotNull(mt);
assertNotNull(mt.getChars());
assertEquals("XXX", charsToString(mt.getChars()));
@@ -150,7 +152,8 @@
// Recycle it
MeasuredParagraph mt2 = MeasuredParagraph.buildForStaticLayout(
- PAINT, "_VVV_", 1, 4, RTL, false, false, null /* no hint */, mt);
+ PAINT, "_VVV_", 1, 4, RTL, MeasuredText.Builder.HYPHENATION_MODE_NONE, false,
+ null /* no hint */, mt);
assertEquals(mt2, mt);
assertNotNull(mt2.getChars());
assertEquals("VVV", charsToString(mt.getChars()));
diff --git a/graphics/java/android/graphics/text/MeasuredText.java b/graphics/java/android/graphics/text/MeasuredText.java
index 31c3d09..df5b3f5 100644
--- a/graphics/java/android/graphics/text/MeasuredText.java
+++ b/graphics/java/android/graphics/text/MeasuredText.java
@@ -17,12 +17,14 @@
package android.graphics.text;
import android.annotation.FloatRange;
+import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Px;
import android.graphics.Paint;
import android.graphics.Rect;
+import android.util.Log;
import com.android.internal.util.Preconditions;
@@ -30,6 +32,9 @@
import libcore.util.NativeAllocationRegistry;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Result of text shaping of the single paragraph string.
*
@@ -49,6 +54,8 @@
* </p>
*/
public class MeasuredText {
+ private static final String TAG = "MeasuredText";
+
private long mNativePtr;
private boolean mComputeHyphenation;
private boolean mComputeLayout;
@@ -179,6 +186,7 @@
private final @NonNull char[] mText;
private boolean mComputeHyphenation = false;
private boolean mComputeLayout = true;
+ private boolean mFastHyphenation = false;
private int mCurrentOffset = 0;
private @Nullable MeasuredText mHintMt = null;
@@ -275,10 +283,78 @@
* Even if you pass false to this method, you can still enable automatic hyphenation of
* LineBreaker but line break computation becomes slower.
*
+ * @deprecated use setComputeHyphenation(int) instead.
+ *
* @param computeHyphenation true if you want to use automatic hyphenations.
*/
public @NonNull Builder setComputeHyphenation(boolean computeHyphenation) {
- mComputeHyphenation = computeHyphenation;
+ setComputeHyphenation(
+ computeHyphenation ? HYPHENATION_MODE_NORMAL : HYPHENATION_MODE_NONE);
+ return this;
+ }
+
+ /** @hide */
+ @IntDef(prefix = { "HYPHENATION_MODE_" }, value = {
+ HYPHENATION_MODE_NONE,
+ HYPHENATION_MODE_NORMAL,
+ HYPHENATION_MODE_FAST
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface HyphenationMode {}
+
+ /**
+ * A value for hyphenation calculation mode.
+ *
+ * This value indicates that no hyphenation points are calculated.
+ */
+ public static final int HYPHENATION_MODE_NONE = 0;
+
+ /**
+ * A value for hyphenation calculation mode.
+ *
+ * This value indicates that hyphenation points are calculated.
+ */
+ public static final int HYPHENATION_MODE_NORMAL = 1;
+
+ /**
+ * A value for hyphenation calculation mode.
+ *
+ * This value indicates that hyphenation points are calculated with faster algorithm. This
+ * algorithm measures text width with ignoring the context of hyphen character shaping, e.g.
+ * kerning.
+ */
+ public static final int HYPHENATION_MODE_FAST = 2;
+
+ /**
+ * By passing true to this method, the build method will calculate hyphenation break
+ * points faster with ignoring some typographic features, e.g. kerning.
+ *
+ * {@link #HYPHENATION_MODE_NONE} is by default.
+ *
+ * @see #setComputeHyphenation(boolean)
+ *
+ * @param mode a hyphenation mode.
+ */
+ public @NonNull Builder setComputeHyphenation(@HyphenationMode int mode) {
+ switch (mode) {
+ case HYPHENATION_MODE_NONE:
+ mComputeHyphenation = false;
+ mFastHyphenation = false;
+ break;
+ case HYPHENATION_MODE_NORMAL:
+ mComputeHyphenation = true;
+ mFastHyphenation = false;
+ break;
+ case HYPHENATION_MODE_FAST:
+ mComputeHyphenation = true;
+ mFastHyphenation = true;
+ break;
+ default:
+ Log.e(TAG, "Unknown hyphenation mode: " + mode);
+ mComputeHyphenation = false;
+ mFastHyphenation = false;
+ break;
+ }
return this;
}
@@ -319,7 +395,7 @@
try {
long hintPtr = (mHintMt == null) ? 0 : mHintMt.getNativePtr();
long ptr = nBuildMeasuredText(mNativePtr, hintPtr, mText, mComputeHyphenation,
- mComputeLayout);
+ mComputeLayout, mFastHyphenation);
final MeasuredText res = new MeasuredText(ptr, mText, mComputeHyphenation,
mComputeLayout);
sRegistry.registerNativeAllocation(res, ptr);
@@ -378,7 +454,8 @@
long hintMtPtr,
@NonNull char[] text,
boolean computeHyphenation,
- boolean computeLayout);
+ boolean computeLayout,
+ boolean fastHyphenationMode);
private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
}
diff --git a/libs/hwui/jni/text/MeasuredText.cpp b/libs/hwui/jni/text/MeasuredText.cpp
index 7793746..bd9bd71 100644
--- a/libs/hwui/jni/text/MeasuredText.cpp
+++ b/libs/hwui/jni/text/MeasuredText.cpp
@@ -80,15 +80,17 @@
}
// Regular JNI
-static jlong nBuildMeasuredText(JNIEnv* env, jclass /* unused */, jlong builderPtr,
- jlong hintPtr, jcharArray javaText, jboolean computeHyphenation,
- jboolean computeLayout) {
+static jlong nBuildMeasuredText(JNIEnv* env, jclass /* unused */, jlong builderPtr, jlong hintPtr,
+ jcharArray javaText, jboolean computeHyphenation,
+ jboolean computeLayout, jboolean fastHyphenationMode) {
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,
- toMeasuredParagraph(hintPtr)).release());
+ return toJLong(toBuilder(builderPtr)
+ ->build(textBuffer, computeHyphenation, computeLayout,
+ fastHyphenationMode, toMeasuredParagraph(hintPtr))
+ .release());
}
// Regular JNI
@@ -140,12 +142,12 @@
}
static const JNINativeMethod gMTBuilderMethods[] = {
- // MeasuredParagraphBuilder native functions.
- {"nInitBuilder", "()J", (void*) nInitBuilder},
- {"nAddStyleRun", "(JJIIZ)V", (void*) nAddStyleRun},
- {"nAddReplacementRun", "(JJIIF)V", (void*) nAddReplacementRun},
- {"nBuildMeasuredText", "(JJ[CZZ)J", (void*) nBuildMeasuredText},
- {"nFreeBuilder", "(J)V", (void*) nFreeBuilder},
+ // MeasuredParagraphBuilder native functions.
+ {"nInitBuilder", "()J", (void*)nInitBuilder},
+ {"nAddStyleRun", "(JJIIZ)V", (void*)nAddStyleRun},
+ {"nAddReplacementRun", "(JJIIF)V", (void*)nAddReplacementRun},
+ {"nBuildMeasuredText", "(JJ[CZZZ)J", (void*)nBuildMeasuredText},
+ {"nFreeBuilder", "(J)V", (void*)nFreeBuilder},
};
static const JNINativeMethod gMTMethods[] = {