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
This commit is contained in:
Seigo Nonaka
2021-08-30 01:27:10 +00:00
parent ac6c0e9c8b
commit 0f761404af
10 changed files with 196 additions and 27 deletions

View File

@@ -137,6 +137,21 @@ public class StaticLayoutPerfTest {
}
}
@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();

View File

@@ -17264,8 +17264,12 @@ package android.graphics.text {
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 @@ package android.text {
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
}

View File

@@ -82,7 +82,9 @@ public abstract class Layout {
/** @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 @@ public abstract class Layout {
* 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);

View File

@@ -377,7 +377,7 @@ public class MeasuredParagraph {
* @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 @@ public class MeasuredParagraph {
@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 @@ public class MeasuredParagraph {
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);

View File

@@ -22,6 +22,7 @@ import android.annotation.IntRange;
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 @@ public class PrecomputedText implements Spannable {
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 @@ public class PrecomputedText implements Spannable {
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 @@ public class PrecomputedText implements Spannable {
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()]);
}

View File

@@ -591,6 +591,20 @@ public class StaticLayout extends Layout {
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 @@ public class StaticLayout extends Layout {
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)

View File

@@ -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. -->

View File

@@ -23,6 +23,7 @@ import static org.junit.Assert.assertNull;
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 @@ public class MeasuredParagraphTest {
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 @@ public class MeasuredParagraphTest {
// 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()));

View File

@@ -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 dalvik.annotation.optimization.CriticalNative;
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 @@ import libcore.util.NativeAllocationRegistry;
* </p>
*/
public class MeasuredText {
private static final String TAG = "MeasuredText";
private long mNativePtr;
private boolean mComputeHyphenation;
private boolean mComputeLayout;
@@ -179,6 +186,7 @@ public class MeasuredText {
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 @@ public class MeasuredText {
* 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 @@ public class MeasuredText {
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 @@ public class MeasuredText {
long hintMtPtr,
@NonNull char[] text,
boolean computeHyphenation,
boolean computeLayout);
boolean computeLayout,
boolean fastHyphenationMode);
private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
}

View File

@@ -80,15 +80,17 @@ static void nAddReplacementRun(JNIEnv* /* unused */, jclass /* unused */, jlong
}
// 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 jint nGetMemoryUsage(CRITICAL_JNI_PARAMS_COMMA jlong ptr) {
}
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[] = {