Loading core/java/android/text/StaticLayout.java +21 −9 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.text; import android.annotation.Nullable; import android.graphics.Paint; import android.os.LocaleList; import android.text.style.LeadingMarginSpan; import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; import android.text.style.LineHeightSpan; Loading Loading @@ -333,6 +334,16 @@ public class StaticLayout extends Layout { return this; } private long[] getHyphenators(LocaleList locales) { final int length = locales.size(); final long[] result = new long[length]; for (int i = 0; i < length; i++) { final Locale locale = locales.get(i); result[i] = Hyphenator.get(locale).getNativePtr(); } return result; } /** * Measurement and break iteration is done in native code. The protocol for using * the native code is as follows. Loading @@ -342,7 +353,7 @@ public class StaticLayout extends Layout { * future). * * Then, for each run within the paragraph: * - setLocale (this must be done at least for the first run, optional afterwards) * - setLocales (this must be done at least for the first run, optional afterwards) * - one of the following, depending on the type of run: * + addStyleRun (a text run, to be measured in native code) * + addMeasuredRun (a run already measured in Java, passed into native code) Loading @@ -354,15 +365,15 @@ public class StaticLayout extends Layout { * After all paragraphs, call finish() to release expensive buffers. */ private void setLocale(Locale locale) { if (!locale.equals(mLocale)) { nSetLocale(mNativePtr, locale.toLanguageTag(), Hyphenator.get(locale).getNativePtr()); mLocale = locale; private void setLocales(LocaleList locales) { if (!locales.equals(mLocales)) { nSetLocales(mNativePtr, locales.toLanguageTags(), getHyphenators(locales)); mLocales = locales; } } /* package */ float addStyleRun(TextPaint paint, int start, int end, boolean isRtl) { setLocales(paint.getTextLocales()); return nAddStyleRun(mNativePtr, paint.getNativeInstance(), paint.mNativeTypeface, start, end, isRtl); } Loading Loading @@ -425,7 +436,7 @@ public class StaticLayout extends Layout { // This will go away and be subsumed by native builder code MeasuredText mMeasuredText; Locale mLocale; LocaleList mLocales; private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3); } Loading Loading @@ -594,7 +605,7 @@ public class StaticLayout extends Layout { // store fontMetrics per span range // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range) int[] fmCache = new int[4 * 4]; b.setLocale(paint.getTextLocale()); // TODO: also respect LocaleSpan within the text b.setLocales(paint.getTextLocales()); mLineCount = 0; Loading Loading @@ -1308,7 +1319,8 @@ public class StaticLayout extends Layout { /* package */ static native long nLoadHyphenator(ByteBuffer buf, int offset, int minPrefix, int minSuffix); private static native void nSetLocale(long nativePtr, String locale, long nativeHyphenator); private static native void nSetLocales(long nativePtr, String locales, long[] nativeHyphenators); private static native void nSetIndents(long nativePtr, int[] indents); Loading core/jni/android_text_StaticLayout.cpp +11 −7 Original line number Diff line number Diff line Loading @@ -137,15 +137,19 @@ static jlong nLoadHyphenator(JNIEnv* env, jclass, jobject buffer, jint offset, return reinterpret_cast<jlong>(hyphenator); } static void nSetLocale(JNIEnv* env, jclass, jlong nativePtr, jstring javaLocaleName, jlong nativeHyphenator) { ScopedIcuLocale icuLocale(env, javaLocaleName); static void nSetLocales(JNIEnv* env, jclass, jlong nativePtr, jstring javaLocaleNames, jlongArray nativeHyphenators) { minikin::LineBreaker* b = reinterpret_cast<minikin::LineBreaker*>(nativePtr); minikin::Hyphenator* hyphenator = reinterpret_cast<minikin::Hyphenator*>(nativeHyphenator); if (icuLocale.valid()) { b->setLocale(icuLocale.locale(), hyphenator); ScopedUtfChars localeNames(env, javaLocaleNames); ScopedLongArrayRO hyphArr(env, nativeHyphenators); const size_t numLocales = hyphArr.size(); std::vector<minikin::Hyphenator*> hyphVec; hyphVec.reserve(numLocales); for (size_t i = 0; i < numLocales; i++) { hyphVec.push_back(reinterpret_cast<minikin::Hyphenator*>(hyphArr[i])); } b->setLocales(localeNames.c_str(), hyphVec); } static void nSetIndents(JNIEnv* env, jclass, jlong nativePtr, jintArray indents) { Loading Loading @@ -194,7 +198,7 @@ static const JNINativeMethod gMethods[] = { {"nFreeBuilder", "(J)V", (void*) nFreeBuilder}, {"nFinishBuilder", "(J)V", (void*) nFinishBuilder}, {"nLoadHyphenator", "(Ljava/nio/ByteBuffer;III)J", (void*) nLoadHyphenator}, {"nSetLocale", "(JLjava/lang/String;J)V", (void*) nSetLocale}, {"nSetLocales", "(JLjava/lang/String;[J)V", (void*) nSetLocales}, {"nSetupParagraph", "(J[CIFIF[IIIIZ)V", (void*) nSetupParagraph}, {"nSetIndents", "(J[I)V", (void*) nSetIndents}, {"nAddStyleRun", "(JJJIIZ)F", (void*) nAddStyleRun}, Loading core/tests/coretests/src/android/text/StaticLayoutTest.java +75 −0 Original line number Diff line number Diff line Loading @@ -22,10 +22,12 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.graphics.Paint.FontMetricsInt; import android.os.LocaleList; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.text.Layout.Alignment; import android.text.method.EditorState; import android.text.style.LocaleSpan; import android.util.Log; import org.junit.Before; Loading @@ -35,6 +37,7 @@ import org.junit.runner.RunWith; import java.text.Normalizer; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * Tests StaticLayout vertical metrics behavior. Loading Loading @@ -671,4 +674,76 @@ public class StaticLayoutTest { assertEquals(testLabel, 4, layout.getOffsetToRightOf(5)); } } @Test public void testLocaleSpanAffectsHyphenation() { TextPaint paint = new TextPaint(); paint.setTextLocale(Locale.US); // Private use language, with no hyphenation rules. final Locale privateLocale = Locale.forLanguageTag("qaa"); final String longWord = "philanthropic"; final float wordWidth = paint.measureText(longWord); // Wide enough that words get hyphenated by default. final int paraWidth = Math.round(wordWidth * 1.8f); final String sentence = longWord + " " + longWord + " " + longWord + " " + longWord + " " + longWord + " " + longWord; final int numEnglishLines = StaticLayout.Builder .obtain(sentence, 0, sentence.length(), paint, paraWidth) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) .build() .getLineCount(); { final SpannableString text = new SpannableString(sentence); text.setSpan(new LocaleSpan(privateLocale), 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); final int numPrivateLocaleLines = StaticLayout.Builder .obtain(text, 0, text.length(), paint, paraWidth) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) .build() .getLineCount(); // Since the paragraph set to English gets hyphenated, the number of lines would be // smaller than the number of lines when there is a span setting a language that // doesn't get hyphenated. assertTrue(numEnglishLines < numPrivateLocaleLines); } { // Same as the above test, except that the locale span now uses a locale list starting // with the private non-hyphenating locale. final SpannableString text = new SpannableString(sentence); final LocaleList locales = new LocaleList(privateLocale, Locale.US); text.setSpan(new LocaleSpan(locales), 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); final int numPrivateLocaleLines = StaticLayout.Builder .obtain(text, 0, text.length(), paint, paraWidth) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) .build() .getLineCount(); assertTrue(numEnglishLines < numPrivateLocaleLines); } { final SpannableString text = new SpannableString(sentence); // Apply the private LocaleSpan only to the first word, which is not getting hyphenated // anyway. text.setSpan(new LocaleSpan(privateLocale), 0, longWord.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); final int numPrivateLocaleLines = StaticLayout.Builder .obtain(text, 0, text.length(), paint, paraWidth) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) .build() .getLineCount(); // Since the first word is not hyphenated anyway (there's enough width), the LocaleSpan // should not affect the layout. assertEquals(numEnglishLines, numPrivateLocaleLines); } } } Loading
core/java/android/text/StaticLayout.java +21 −9 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package android.text; import android.annotation.Nullable; import android.graphics.Paint; import android.os.LocaleList; import android.text.style.LeadingMarginSpan; import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; import android.text.style.LineHeightSpan; Loading Loading @@ -333,6 +334,16 @@ public class StaticLayout extends Layout { return this; } private long[] getHyphenators(LocaleList locales) { final int length = locales.size(); final long[] result = new long[length]; for (int i = 0; i < length; i++) { final Locale locale = locales.get(i); result[i] = Hyphenator.get(locale).getNativePtr(); } return result; } /** * Measurement and break iteration is done in native code. The protocol for using * the native code is as follows. Loading @@ -342,7 +353,7 @@ public class StaticLayout extends Layout { * future). * * Then, for each run within the paragraph: * - setLocale (this must be done at least for the first run, optional afterwards) * - setLocales (this must be done at least for the first run, optional afterwards) * - one of the following, depending on the type of run: * + addStyleRun (a text run, to be measured in native code) * + addMeasuredRun (a run already measured in Java, passed into native code) Loading @@ -354,15 +365,15 @@ public class StaticLayout extends Layout { * After all paragraphs, call finish() to release expensive buffers. */ private void setLocale(Locale locale) { if (!locale.equals(mLocale)) { nSetLocale(mNativePtr, locale.toLanguageTag(), Hyphenator.get(locale).getNativePtr()); mLocale = locale; private void setLocales(LocaleList locales) { if (!locales.equals(mLocales)) { nSetLocales(mNativePtr, locales.toLanguageTags(), getHyphenators(locales)); mLocales = locales; } } /* package */ float addStyleRun(TextPaint paint, int start, int end, boolean isRtl) { setLocales(paint.getTextLocales()); return nAddStyleRun(mNativePtr, paint.getNativeInstance(), paint.mNativeTypeface, start, end, isRtl); } Loading Loading @@ -425,7 +436,7 @@ public class StaticLayout extends Layout { // This will go away and be subsumed by native builder code MeasuredText mMeasuredText; Locale mLocale; LocaleList mLocales; private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3); } Loading Loading @@ -594,7 +605,7 @@ public class StaticLayout extends Layout { // store fontMetrics per span range // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range) int[] fmCache = new int[4 * 4]; b.setLocale(paint.getTextLocale()); // TODO: also respect LocaleSpan within the text b.setLocales(paint.getTextLocales()); mLineCount = 0; Loading Loading @@ -1308,7 +1319,8 @@ public class StaticLayout extends Layout { /* package */ static native long nLoadHyphenator(ByteBuffer buf, int offset, int minPrefix, int minSuffix); private static native void nSetLocale(long nativePtr, String locale, long nativeHyphenator); private static native void nSetLocales(long nativePtr, String locales, long[] nativeHyphenators); private static native void nSetIndents(long nativePtr, int[] indents); Loading
core/jni/android_text_StaticLayout.cpp +11 −7 Original line number Diff line number Diff line Loading @@ -137,15 +137,19 @@ static jlong nLoadHyphenator(JNIEnv* env, jclass, jobject buffer, jint offset, return reinterpret_cast<jlong>(hyphenator); } static void nSetLocale(JNIEnv* env, jclass, jlong nativePtr, jstring javaLocaleName, jlong nativeHyphenator) { ScopedIcuLocale icuLocale(env, javaLocaleName); static void nSetLocales(JNIEnv* env, jclass, jlong nativePtr, jstring javaLocaleNames, jlongArray nativeHyphenators) { minikin::LineBreaker* b = reinterpret_cast<minikin::LineBreaker*>(nativePtr); minikin::Hyphenator* hyphenator = reinterpret_cast<minikin::Hyphenator*>(nativeHyphenator); if (icuLocale.valid()) { b->setLocale(icuLocale.locale(), hyphenator); ScopedUtfChars localeNames(env, javaLocaleNames); ScopedLongArrayRO hyphArr(env, nativeHyphenators); const size_t numLocales = hyphArr.size(); std::vector<minikin::Hyphenator*> hyphVec; hyphVec.reserve(numLocales); for (size_t i = 0; i < numLocales; i++) { hyphVec.push_back(reinterpret_cast<minikin::Hyphenator*>(hyphArr[i])); } b->setLocales(localeNames.c_str(), hyphVec); } static void nSetIndents(JNIEnv* env, jclass, jlong nativePtr, jintArray indents) { Loading Loading @@ -194,7 +198,7 @@ static const JNINativeMethod gMethods[] = { {"nFreeBuilder", "(J)V", (void*) nFreeBuilder}, {"nFinishBuilder", "(J)V", (void*) nFinishBuilder}, {"nLoadHyphenator", "(Ljava/nio/ByteBuffer;III)J", (void*) nLoadHyphenator}, {"nSetLocale", "(JLjava/lang/String;J)V", (void*) nSetLocale}, {"nSetLocales", "(JLjava/lang/String;[J)V", (void*) nSetLocales}, {"nSetupParagraph", "(J[CIFIF[IIIIZ)V", (void*) nSetupParagraph}, {"nSetIndents", "(J[I)V", (void*) nSetIndents}, {"nAddStyleRun", "(JJJIIZ)F", (void*) nAddStyleRun}, Loading
core/tests/coretests/src/android/text/StaticLayoutTest.java +75 −0 Original line number Diff line number Diff line Loading @@ -22,10 +22,12 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.graphics.Paint.FontMetricsInt; import android.os.LocaleList; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.text.Layout.Alignment; import android.text.method.EditorState; import android.text.style.LocaleSpan; import android.util.Log; import org.junit.Before; Loading @@ -35,6 +37,7 @@ import org.junit.runner.RunWith; import java.text.Normalizer; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * Tests StaticLayout vertical metrics behavior. Loading Loading @@ -671,4 +674,76 @@ public class StaticLayoutTest { assertEquals(testLabel, 4, layout.getOffsetToRightOf(5)); } } @Test public void testLocaleSpanAffectsHyphenation() { TextPaint paint = new TextPaint(); paint.setTextLocale(Locale.US); // Private use language, with no hyphenation rules. final Locale privateLocale = Locale.forLanguageTag("qaa"); final String longWord = "philanthropic"; final float wordWidth = paint.measureText(longWord); // Wide enough that words get hyphenated by default. final int paraWidth = Math.round(wordWidth * 1.8f); final String sentence = longWord + " " + longWord + " " + longWord + " " + longWord + " " + longWord + " " + longWord; final int numEnglishLines = StaticLayout.Builder .obtain(sentence, 0, sentence.length(), paint, paraWidth) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) .build() .getLineCount(); { final SpannableString text = new SpannableString(sentence); text.setSpan(new LocaleSpan(privateLocale), 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); final int numPrivateLocaleLines = StaticLayout.Builder .obtain(text, 0, text.length(), paint, paraWidth) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) .build() .getLineCount(); // Since the paragraph set to English gets hyphenated, the number of lines would be // smaller than the number of lines when there is a span setting a language that // doesn't get hyphenated. assertTrue(numEnglishLines < numPrivateLocaleLines); } { // Same as the above test, except that the locale span now uses a locale list starting // with the private non-hyphenating locale. final SpannableString text = new SpannableString(sentence); final LocaleList locales = new LocaleList(privateLocale, Locale.US); text.setSpan(new LocaleSpan(locales), 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); final int numPrivateLocaleLines = StaticLayout.Builder .obtain(text, 0, text.length(), paint, paraWidth) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) .build() .getLineCount(); assertTrue(numEnglishLines < numPrivateLocaleLines); } { final SpannableString text = new SpannableString(sentence); // Apply the private LocaleSpan only to the first word, which is not getting hyphenated // anyway. text.setSpan(new LocaleSpan(privateLocale), 0, longWord.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); final int numPrivateLocaleLines = StaticLayout.Builder .obtain(text, 0, text.length(), paint, paraWidth) .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY) .build() .getLineCount(); // Since the first word is not hyphenated anyway (there's enough width), the LocaleSpan // should not affect the layout. assertEquals(numEnglishLines, numPrivateLocaleLines); } } }