From 737dfead64dd24a431beff85b685594dc0efb49a Mon Sep 17 00:00:00 2001 From: Roozbeh Pournader Date: Thu, 10 Aug 2017 11:32:24 -0700 Subject: [PATCH] Support extra linespacing based on fallback fonts * Increase the ascent and descent of individual lines in StaticLayout as needed, if any fallback fonts that end up getting used call for it. For backward compatibility, this is hidden behind a builder flag. * Document in Paint.java that the returned parameters are only for the default font, and a layout may need more space based on fallbacks used. Also update for changes in minikin API: * MinikinFont now requires a method for getting vertical extents (ascent, descent, and line gap). * minikin API now allows asking for vertical extents of laid out text. * minikin API's LineBreaker now returns ascents and descents for each line. Finally, added performances test for creating a StaticLayout. Follwing are the numbers on a marlin with a stable clock before and after this CL. For fixed text almost always hitting the cache: Before: mean=260684 median=260188 min=258532 standardDeviation=1897 After: mean=262432 median=261509 min=260429 standardDeviation=2185 For random text almost never hitting the cache: Before: mean=5971827 median=5991126 min=5886871 standardDeviation=83724 After: mean=6337093 median=6317010 min=6311222 standardDeviation=40213 Bug: 28963299 Bug: 29063863 Bug: 32057121 Bug: 37756858 Test: bit FrameworksCoreTests:android.text. Test: bit CtsTextTestCases:* Change-Id: I482a98ff8f472e8bab4f0ba9d1d7b368858038ff --- .../android/text/StaticLayoutPerfTest.java | 91 +++++++++ api/current.txt | 1 + api/system-current.txt | 1 + api/test-current.txt | 1 + core/java/android/text/StaticLayout.java | 51 ++++- core/jni/android_text_StaticLayout.cpp | 26 ++- .../assets/fonts/ascent1em-descent2em.ttf | Bin 0 -> 1776 bytes .../assets/fonts/ascent1em-descent2em.ttx | 181 ++++++++++++++++++ .../assets/fonts/ascent3em-descent4em.ttf | Bin 0 -> 1768 bytes .../assets/fonts/ascent3em-descent4em.ttx | 180 +++++++++++++++++ .../src/android/text/StaticLayoutTest.java | 138 +++++++++++++ graphics/java/android/graphics/Paint.java | 15 ++ libs/hwui/hwui/MinikinSkia.cpp | 11 ++ libs/hwui/hwui/MinikinSkia.h | 3 + libs/hwui/hwui/MinikinUtils.cpp | 2 +- 15 files changed, 684 insertions(+), 17 deletions(-) create mode 100644 apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java create mode 100644 core/tests/coretests/assets/fonts/ascent1em-descent2em.ttf create mode 100644 core/tests/coretests/assets/fonts/ascent1em-descent2em.ttx create mode 100644 core/tests/coretests/assets/fonts/ascent3em-descent4em.ttf create mode 100644 core/tests/coretests/assets/fonts/ascent3em-descent4em.ttx diff --git a/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java b/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java new file mode 100644 index 0000000000000..74d136654c62f --- /dev/null +++ b/apct-tests/perftests/core/src/android/text/StaticLayoutPerfTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2016 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.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; + +import android.support.test.filters.LargeTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.CharBuffer; +import java.util.Random; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class StaticLayoutPerfTest { + + public StaticLayoutPerfTest() { + } + + @Rule + public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + private static final String FIXED_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing " + + "elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad " + + "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " + + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse " + + "cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non " + + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + private static final int FIXED_TEXT_LENGTH = FIXED_TEXT.length(); + + private static TextPaint PAINT = new TextPaint(); + private static final int TEXT_WIDTH = 20 * (int) PAINT.getTextSize(); + + @Test + public void testCreate() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + StaticLayout.Builder.obtain(FIXED_TEXT, 0, FIXED_TEXT_LENGTH, PAINT, TEXT_WIDTH) + .build(); + } + } + + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final int ALPHABET_LENGTH = ALPHABET.length(); + + private static final int PARA_LENGTH = 500; + private final char[] mBuffer = new char[PARA_LENGTH]; + private final Random mRandom = new Random(31415926535L); + + private CharSequence generateRandomParagraph(int wordLen) { + for (int i = 0; i < PARA_LENGTH; i++) { + if (i % (wordLen + 1) == wordLen) { + mBuffer[i] = ' '; + } else { + mBuffer[i] = ALPHABET.charAt(mRandom.nextInt(ALPHABET_LENGTH)); + } + } + return CharBuffer.wrap(mBuffer); + } + + // This tries to simulate the case where the cache hit rate is low, and most of the text is + // new text. + @Test + public void testCreateRandom() { + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + final CharSequence text = generateRandomParagraph(9); + StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH) + .build(); + } + } +} diff --git a/api/current.txt b/api/current.txt index 6bf11bbf69c21..69323c1a126e8 100644 --- a/api/current.txt +++ b/api/current.txt @@ -41761,6 +41761,7 @@ package android.text { method public android.text.StaticLayout.Builder setMaxLines(int); method public android.text.StaticLayout.Builder setText(java.lang.CharSequence); method public android.text.StaticLayout.Builder setTextDirection(android.text.TextDirectionHeuristic); + method public android.text.StaticLayout.Builder setUseLineSpacingFromFallbacks(boolean); } public abstract interface TextDirectionHeuristic { diff --git a/api/system-current.txt b/api/system-current.txt index df2df2b18a90a..973eb9968a8d9 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -45362,6 +45362,7 @@ package android.text { method public android.text.StaticLayout.Builder setMaxLines(int); method public android.text.StaticLayout.Builder setText(java.lang.CharSequence); method public android.text.StaticLayout.Builder setTextDirection(android.text.TextDirectionHeuristic); + method public android.text.StaticLayout.Builder setUseLineSpacingFromFallbacks(boolean); } public abstract interface TextDirectionHeuristic { diff --git a/api/test-current.txt b/api/test-current.txt index aa08dd9679986..fe66ec9741a99 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -42032,6 +42032,7 @@ package android.text { method public android.text.StaticLayout.Builder setMaxLines(int); method public android.text.StaticLayout.Builder setText(java.lang.CharSequence); method public android.text.StaticLayout.Builder setTextDirection(android.text.TextDirectionHeuristic); + method public android.text.StaticLayout.Builder setUseLineSpacingFromFallbacks(boolean); } public abstract interface TextDirectionHeuristic { diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 6a7db4ed4e27f..dd82e1e0b5d2e 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -90,6 +90,7 @@ public class StaticLayout extends Layout { b.mSpacingMult = 1.0f; b.mSpacingAdd = 0.0f; b.mIncludePad = true; + b.mFallbackLineSpacing = false; b.mEllipsizedWidth = width; b.mEllipsize = null; b.mMaxLines = Integer.MAX_VALUE; @@ -227,6 +228,24 @@ public class StaticLayout extends Layout { return this; } + /** + * Set whether to respect the ascent and descent of the fallback fonts that are used in + * displaying the text (which is needed to avoid text from consecutive lines running into + * each other). If set, fallback fonts that end up getting used can increase the ascent + * and descent of the lines that they are used on. + * + *

For backward compatibility reasons, the default is {@code false}, but setting this to + * true is strongly recommended. It is required to be true if text could be in languages + * like Burmese or Tibetan where text is typically much taller or deeper than Latin text. + * + * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts + * @return this builder, useful for chaining + */ + public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) { + mFallbackLineSpacing = useLineSpacingFromFallbacks; + return this; + } + /** * Set the width as used for ellipsizing purposes, if it differs from the * normal layout width. The default is the {@code width} @@ -432,6 +451,7 @@ public class StaticLayout extends Layout { float mSpacingMult; float mSpacingAdd; boolean mIncludePad; + boolean mFallbackLineSpacing; int mEllipsizedWidth; TextUtils.TruncateAt mEllipsize; int mMaxLines; @@ -606,6 +626,7 @@ public class StaticLayout extends Layout { TextPaint paint = b.mPaint; int outerWidth = b.mWidth; TextDirectionHeuristic textDir = b.mTextDir; + final boolean fallbackLineSpacing = b.mFallbackLineSpacing; float spacingmult = b.mSpacingMult; float spacingadd = b.mSpacingAdd; float ellipsizedWidth = b.mEllipsizedWidth; @@ -784,11 +805,14 @@ public class StaticLayout extends Layout { nGetWidths(b.mNativePtr, widths); int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks, - lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length); + lineBreaks.widths, lineBreaks.ascents, lineBreaks.descents, lineBreaks.flags, + lineBreaks.breaks.length); - int[] breaks = lineBreaks.breaks; - float[] lineWidths = lineBreaks.widths; - int[] flags = lineBreaks.flags; + final int[] breaks = lineBreaks.breaks; + final float[] lineWidths = lineBreaks.widths; + final float[] ascents = lineBreaks.ascents; + final float[] descents = lineBreaks.descents; + final int[] flags = lineBreaks.flags; final int remainingLineCount = mMaximumVisibleLineCount - mLineCount; final boolean ellipsisMayBeApplied = ellipsize != null @@ -799,7 +823,7 @@ public class StaticLayout extends Layout { && ellipsisMayBeApplied) { // Calculate width and flag. float width = 0; - int flag = 0; + int flag = 0; // XXX May need to also have starting hyphen edit for (int i = remainingLineCount - 1; i < breakCount; i++) { if (i == breakCount - 1) { width += lineWidths[i]; @@ -808,7 +832,7 @@ public class StaticLayout extends Layout { width += widths[j]; } } - flag |= flags[i] & TAB_MASK; // XXX May need to also have starting hyphen edit + flag |= flags[i] & TAB_MASK; } // Treat the last line and overflowed lines as a single line. breaks[remainingLineCount - 1] = breaks[breakCount - 1]; @@ -859,8 +883,14 @@ public class StaticLayout extends Layout { boolean moreChars = (endPos < bufEnd); + final int ascent = fallbackLineSpacing + ? Math.min(fmAscent, (int) Math.round(ascents[breakIndex])) + : fmAscent; + final int descent = fallbackLineSpacing + ? Math.max(fmDescent, (int) Math.round(descents[breakIndex])) + : fmDescent; v = out(source, here, endPos, - fmAscent, fmDescent, fmTop, fmBottom, + ascent, descent, fmTop, fmBottom, v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, flags[breakIndex], needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad, addLastLineSpacing, chs, widths, paraStart, ellipsize, @@ -891,8 +921,6 @@ public class StaticLayout extends Layout { if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) && mLineCount < mMaximumVisibleLineCount) { - // Log.e("text", "output last " + bufEnd); - measured.setPara(source, bufEnd, bufEnd, textDir, b); paint.getFontMetricsInt(fm); @@ -1470,7 +1498,8 @@ public class StaticLayout extends Layout { // to reduce the number of JNI calls in the common case where the // arrays do not have to be resized private static native int nComputeLineBreaks(long nativePtr, LineBreaks recycle, - int[] recycleBreaks, float[] recycleWidths, int[] recycleFlags, int recycleLength); + int[] recycleBreaks, float[] recycleWidths, float[] recycleAscents, + float[] recycleDescents, int[] recycleFlags, int recycleLength); private int mLineCount; private int mTopPadding, mBottomPadding; @@ -1529,6 +1558,8 @@ public class StaticLayout extends Layout { private static final int INITIAL_SIZE = 16; public int[] breaks = new int[INITIAL_SIZE]; public float[] widths = new float[INITIAL_SIZE]; + public float[] ascents = new float[INITIAL_SIZE]; + public float[] descents = new float[INITIAL_SIZE]; public int[] flags = new int[INITIAL_SIZE]; // hasTab // breaks, widths, and flags should all have the same length } diff --git a/core/jni/android_text_StaticLayout.cpp b/core/jni/android_text_StaticLayout.cpp index 82a6411826c90..ed6942eb54232 100644 --- a/core/jni/android_text_StaticLayout.cpp +++ b/core/jni/android_text_StaticLayout.cpp @@ -44,6 +44,8 @@ namespace android { struct JLineBreaksID { jfieldID breaks; jfieldID widths; + jfieldID ascents; + jfieldID descents; jfieldID flags; }; @@ -73,35 +75,45 @@ static void nSetupParagraph(JNIEnv* env, jclass, jlong nativePtr, jcharArray tex } static void recycleCopy(JNIEnv* env, jobject recycle, jintArray recycleBreaks, - jfloatArray recycleWidths, jintArray recycleFlags, + jfloatArray recycleWidths, jfloatArray recycleAscents, + jfloatArray recycleDescents, jintArray recycleFlags, jint recycleLength, size_t nBreaks, const jint* breaks, - const jfloat* widths, const jint* flags) { + const jfloat* widths, const jfloat* ascents, const jfloat* descents, + const jint* flags) { if ((size_t)recycleLength < nBreaks) { // have to reallocate buffers recycleBreaks = env->NewIntArray(nBreaks); recycleWidths = env->NewFloatArray(nBreaks); + recycleAscents = env->NewFloatArray(nBreaks); + recycleDescents = env->NewFloatArray(nBreaks); recycleFlags = env->NewIntArray(nBreaks); env->SetObjectField(recycle, gLineBreaks_fieldID.breaks, recycleBreaks); env->SetObjectField(recycle, gLineBreaks_fieldID.widths, recycleWidths); + env->SetObjectField(recycle, gLineBreaks_fieldID.ascents, recycleAscents); + env->SetObjectField(recycle, gLineBreaks_fieldID.descents, recycleDescents); env->SetObjectField(recycle, gLineBreaks_fieldID.flags, recycleFlags); } // copy data env->SetIntArrayRegion(recycleBreaks, 0, nBreaks, breaks); env->SetFloatArrayRegion(recycleWidths, 0, nBreaks, widths); + env->SetFloatArrayRegion(recycleAscents, 0, nBreaks, ascents); + env->SetFloatArrayRegion(recycleDescents, 0, nBreaks, descents); env->SetIntArrayRegion(recycleFlags, 0, nBreaks, flags); } static jint nComputeLineBreaks(JNIEnv* env, jclass, jlong nativePtr, jobject recycle, jintArray recycleBreaks, - jfloatArray recycleWidths, jintArray recycleFlags, + jfloatArray recycleWidths, jfloatArray recycleAscents, + jfloatArray recycleDescents, jintArray recycleFlags, jint recycleLength) { minikin::LineBreaker* b = reinterpret_cast(nativePtr); size_t nBreaks = b->computeBreaks(); - recycleCopy(env, recycle, recycleBreaks, recycleWidths, recycleFlags, recycleLength, - nBreaks, b->getBreaks(), b->getWidths(), b->getFlags()); + recycleCopy(env, recycle, recycleBreaks, recycleWidths, recycleAscents, recycleDescents, + recycleFlags, recycleLength, nBreaks, b->getBreaks(), b->getWidths(), b->getAscents(), + b->getDescents(), b->getFlags()); b->finish(); @@ -205,7 +217,7 @@ static const JNINativeMethod gMethods[] = { {"nAddMeasuredRun", "(JII[F)V", (void*) nAddMeasuredRun}, {"nAddReplacementRun", "(JIIF)V", (void*) nAddReplacementRun}, {"nGetWidths", "(J[F)V", (void*) nGetWidths}, - {"nComputeLineBreaks", "(JLandroid/text/StaticLayout$LineBreaks;[I[F[II)I", + {"nComputeLineBreaks", "(JLandroid/text/StaticLayout$LineBreaks;[I[F[F[F[II)I", (void*) nComputeLineBreaks} }; @@ -216,6 +228,8 @@ int register_android_text_StaticLayout(JNIEnv* env) gLineBreaks_fieldID.breaks = GetFieldIDOrDie(env, gLineBreaks_class, "breaks", "[I"); gLineBreaks_fieldID.widths = GetFieldIDOrDie(env, gLineBreaks_class, "widths", "[F"); + gLineBreaks_fieldID.ascents = GetFieldIDOrDie(env, gLineBreaks_class, "ascents", "[F"); + gLineBreaks_fieldID.descents = GetFieldIDOrDie(env, gLineBreaks_class, "descents", "[F"); gLineBreaks_fieldID.flags = GetFieldIDOrDie(env, gLineBreaks_class, "flags", "[I"); return RegisterMethodsOrDie(env, "android/text/StaticLayout", gMethods, NELEM(gMethods)); diff --git a/core/tests/coretests/assets/fonts/ascent1em-descent2em.ttf b/core/tests/coretests/assets/fonts/ascent1em-descent2em.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f34698f3ebce5b82abb5598be835b082db6c071b GIT binary patch literal 1776 zcmai#yKfX%6vn^VS(}Hz=~6^Oy0R#Ygcq=)BuXfF1cWj2BCm}D1u$N(cL(i@-8E~c zN<@RG_y>^E22m!G3kr%96e&^!ktRi?6r{8SQ3|Bxckaw!?~*7p+IwE#IrqEg+|eqK zv|rySlq;5w&D0K6A4^80YD@=NqUA7UkX zP80e%)$_ajOUAhVxkFtLW}t;I;)o2+Ehoes;a36n<>0UkTW{26Z#Zwkw^u3 zT21!rkP{=FisV-CIhF9O6DfP#WskXqUZ!G6ks2WDK6V~t6Ym06m7F@!&JZR;OpjnL zv8&Z@s>^EouX{cf@*Nd3j%_NIQ)@WqD7Y)+cBl}09(KwC=d@F26(%*DHAy$VU~+EP zyXnvCI9EjX96Va&8zbYEu!+%~*4QaIc|{wZf7>HtavXLm?M+OjrjI-I$uTxNry6Ed z7-6$SrtQ)7J9f}+!zAXMNuO3c$A}r&6s(JpR#H~h|2{jld>12TVp&OE53d7nO#Aqa zm049;C%Z(~@bsx*Il~=V1=%HPSu3#Vr8WO;X2Ykm9j$Z!Rl)8L-;7@;xsxTVS=LG9 z8E>04%w`$HqAqY_myk}&;1+mKYaV-6=X{kS9>?u3;{DQR%zE8+DC#_`;jbd+h#JQF zP=Rl9ckCY8l-s@^$uyF_#_y(VogJNDiLC6_m>liLS~{KVhIZM(zOC5bt}+=mMVrey zvg&J9$+?5p!Jo{``K%61tiRb8V4b$y)sc?bJuzgDs7>9vZrJ~yUxAz3<8AA69pml$ tw?LhpFSFoJR3)0+$Ly=_;<-d`&oYsDW^zu`=@;t%Po%+*_P_D|^A?pm7100y literal 0 HcmV?d00001 diff --git a/core/tests/coretests/assets/fonts/ascent1em-descent2em.ttx b/core/tests/coretests/assets/fonts/ascent1em-descent2em.ttx new file mode 100644 index 0000000000000..68d43239575f1 --- /dev/null +++ b/core/tests/coretests/assets/fonts/ascent1em-descent2em.ttx @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (C) 2017 The Android Open Source Project + + + Sample Font + + + Regular + + + Sample Font + + + SampleFont-Regular + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + 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. + + + http://www.apache.org/licenses/LICENSE-2.0 + + + + + + + + + + + + + + + + diff --git a/core/tests/coretests/assets/fonts/ascent3em-descent4em.ttf b/core/tests/coretests/assets/fonts/ascent3em-descent4em.ttf new file mode 100644 index 0000000000000000000000000000000000000000..085d1337723160ffca135c2c63e392bca7db032d GIT binary patch literal 1768 zcmai#O>Yxd6o%iio$ys)$NDN_0a7p}i?#w5q!dI5imNywZ5M^HgA{ zvkQ$|^~X|l3hV7^EAHwunf?fUzTVv3V&{qeu(eT(HxKK9r+c% zla1D3C;tXX(F2y*W~UmPhW;DNY%AXBa<1tISzJ4A)tWEwgSY%Mg>I)m80~3+(0%jE zxboY7@oMqb@r!>eAF@CH>yqL1V(*`SZBL&A%B2#RBYS_|mO?yM%QXH(-jc%Gkb2_HGsazPd&+4Qer1qov zr*tyJAJbalrY@_au6ET^qPiLyz~~g#)9efST<7#9(i)x``=Z*M*8`KVJC(rJusM~n zZR4r>xNm*TPsmj=mKCc-WYhbu64}POidF~H#M_&Q$s*27#oXXj%imU?)z07cd}`nw z88ePuGFDS<5;ITSO|U&OB%X)evdBH{)>(!*O`_(g#sxa&#@=0jU&p;-s^{R*0dI_q zTVRtQJ8f}NxOq)Go`2UzCSV+TC#_9FrndJx4Z#?j_qUp4RGeb74${u(`xAReKf@&9 zo}o?^&oQP4)&;9#%)ACxR{tR?lzEi6Ma7-q6U zn?&~U8ma=DL7MYpJ)69pooJQ&zYI1%&R5H6j+rc@EwRqAf9-YCCfRHdv8HQG>^i&C z263yri&{Zn(iNYjgvBxaC9L0g#FCfogp#hZTKxCy%Xlru>QIAkm>ru#>vGrEBXlFw zHTkd0R@v$Pl|f}wWBYVHw$koo6WZql`?eB4T~!d)MeEBpd)?=%gSms$!ybC(JgNs1 zt8exNSfvfSIaM*66N?<+wXR#$E%wv>D>AtQ-mcE;Ebq|2Me@A=GK>C3b-ZC7v#+|3 c + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (C) 2017 The Android Open Source Project + + + Sample Font + + + Regular + + + Sample Font + + + SampleFont-Regular + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + 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. + + + http://www.apache.org/licenses/LICENSE-2.0 + + + + + + + + + + + + + + + + diff --git a/core/tests/coretests/src/android/text/StaticLayoutTest.java b/core/tests/coretests/src/android/text/StaticLayoutTest.java index 2dfab87906c19..1bf32ca31c704 100644 --- a/core/tests/coretests/src/android/text/StaticLayoutTest.java +++ b/core/tests/coretests/src/android/text/StaticLayoutTest.java @@ -21,20 +21,33 @@ import static android.text.Layout.Alignment.ALIGN_NORMAL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import android.content.Context; +import android.content.res.AssetManager; import android.graphics.Canvas; +import android.graphics.FontFamily; import android.graphics.Paint.FontMetricsInt; +import android.graphics.Typeface; import android.os.LocaleList; +import android.support.test.InstrumentationRegistry; 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.ArrayMap; import android.util.Log; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.text.Normalizer; import java.util.ArrayList; import java.util.List; @@ -799,4 +812,129 @@ public class StaticLayoutTest { layout.drawText(canvas, 0, 0); assertEquals(31, paint.getHyphenEdit()); } + + private String getTestFontsDir() { + final Context targetCtx = InstrumentationRegistry.getInstrumentation().getTargetContext(); + final File cacheDir = new File(targetCtx.getCacheDir(), "StaticLayoutTest"); + if (!cacheDir.isDirectory()) { + final boolean dirsCreated = cacheDir.mkdirs(); + if (!dirsCreated) { + throw new RuntimeException("Creating test directories for fonts failed."); + } + } + return cacheDir.getAbsolutePath() + "/"; + } + + private TextPaint setupPaintForFallbackFonts(String[] fontFiles, String xml) { + final String testFontsDir = getTestFontsDir(); + final String testFontsXml = new File(testFontsDir, "fonts.xml").getAbsolutePath(); + final AssetManager am = + InstrumentationRegistry.getInstrumentation().getContext().getAssets(); + for (String fontFile : fontFiles) { + final String sourceInAsset = "fonts/" + fontFile; + final File outInCache = new File(testFontsDir, fontFile); + try (InputStream is = am.open(sourceInAsset)) { + Files.copy(is, outInCache.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + try (FileOutputStream fos = new FileOutputStream(testFontsXml)) { + fos.write(xml.getBytes(Charset.forName("UTF-8"))); + } catch (IOException e) { + throw new RuntimeException(e); + } + + final ArrayMap fontMap = new ArrayMap<>(); + final ArrayMap fallbackMap = new ArrayMap<>(); + Typeface.buildSystemFallback(testFontsXml, testFontsDir, fontMap, fallbackMap); + + final TextPaint paint = new TextPaint(); + final Typeface testTypeface = fontMap.get("sans-serif"); + paint.setTypeface(testTypeface); + return paint; + } + + void destroyFallbackFonts(String[] fontFiles) { + final String testFontsDir = getTestFontsDir(); + for (String fontFile : fontFiles) { + final File outInCache = new File(testFontsDir, fontFile); + outInCache.delete(); + } + } + + @Test + public void testFallbackLineSpacing() { + // All glyphs in the fonts are 1em wide. + final String[] testFontFiles = { + // ascent == 1em, descent == 2em, only supports 'a' and space + "ascent1em-descent2em.ttf", + // ascent == 3em, descent == 4em, only supports 'b' + "ascent3em-descent4em.ttf" + }; + final String xml = "" + + "" + + " " + + " ascent1em-descent2em.ttf" + + " " + + " " + + " ascent3em-descent4em.ttf" + + " " + + ""; + + try { + final TextPaint paint = setupPaintForFallbackFonts(testFontFiles, xml); + final int textSize = 100; + paint.setTextSize(textSize); + assertEquals(-textSize, paint.ascent(), 0.0f); + assertEquals(2 * textSize, paint.descent(), 0.0f); + + final int paraWidth = 5 * textSize; + final String text = "aaaaa aabaa aaaaa"; // This should result in three lines. + + // Old line spacing. All lines should get their ascent and descents from the first font. + StaticLayout layout = StaticLayout.Builder + .obtain(text, 0, text.length(), paint, paraWidth) + .setIncludePad(false) + .setUseLineSpacingFromFallbacks(false) + .build(); + assertEquals(3, layout.getLineCount()); + assertEquals(-textSize, layout.getLineAscent(0)); + assertEquals(2 * textSize, layout.getLineDescent(0)); + assertEquals(-textSize, layout.getLineAscent(1)); + assertEquals(2 * textSize, layout.getLineDescent(1)); + assertEquals(-textSize, layout.getLineAscent(2)); + assertEquals(2 * textSize, layout.getLineDescent(2)); + + // New line spacing. The second line has a 'b', so it needs more ascent and descent. + layout = StaticLayout.Builder + .obtain(text, 0, text.length(), paint, paraWidth) + .setIncludePad(false) + .setUseLineSpacingFromFallbacks(true) + .build(); + assertEquals(3, layout.getLineCount()); + assertEquals(-textSize, layout.getLineAscent(0)); + assertEquals(2 * textSize, layout.getLineDescent(0)); + assertEquals(-3 * textSize, layout.getLineAscent(1)); + assertEquals(4 * textSize, layout.getLineDescent(1)); + assertEquals(-textSize, layout.getLineAscent(2)); + assertEquals(2 * textSize, layout.getLineDescent(2)); + + // The default is the old line spacing, for backward compatibility. + layout = StaticLayout.Builder + .obtain(text, 0, text.length(), paint, paraWidth) + .setIncludePad(false) + .build(); + assertEquals(3, layout.getLineCount()); + assertEquals(-textSize, layout.getLineAscent(0)); + assertEquals(2 * textSize, layout.getLineDescent(0)); + assertEquals(-textSize, layout.getLineAscent(1)); + assertEquals(2 * textSize, layout.getLineDescent(1)); + assertEquals(-textSize, layout.getLineAscent(2)); + assertEquals(2 * textSize, layout.getLineDescent(2)); + } finally { + destroyFallbackFonts(testFontFiles); + } + } } diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 46ada23c632f5..1a06a5683cd31 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -1729,6 +1729,9 @@ public class Paint { * Return the distance above (negative) the baseline (ascent) based on the * current typeface and text size. * + *

Note that this is the ascent of the main typeface, and actual text rendered may need a + * larger ascent because fallback fonts may get used in rendering the text. + * * @return the distance above (negative) the baseline (ascent) based on the * current typeface and text size. */ @@ -1740,6 +1743,9 @@ public class Paint { * Return the distance below (positive) the baseline (descent) based on the * current typeface and text size. * + *

Note that this is the descent of the main typeface, and actual text rendered may need a + * larger descent because fallback fonts may get used in rendering the text. + * * @return the distance below (positive) the baseline (descent) based on * the current typeface and text size. */ @@ -1783,6 +1789,9 @@ public class Paint { * settings for typeface, textSize, etc. If metrics is not null, return the * fontmetric values in it. * + *

Note that these are the values for the main typeface, and actual text rendered may need a + * larger set of values because fallback fonts may get used in rendering the text. + * * @param metrics If this object is not null, its fields are filled with * the appropriate values given the paint's text attributes. * @return the font's recommended interline spacing. @@ -1844,6 +1853,9 @@ public class Paint { * and clipping. If you want more control over the rounding, call * getFontMetrics(). * + *

Note that these are the values for the main typeface, and actual text rendered may need a + * larger set of values because fallback fonts may get used in rendering the text. + * * @return the font's interline spacing. */ public int getFontMetricsInt(FontMetricsInt fmi) { @@ -1860,6 +1872,9 @@ public class Paint { * Return the recommend line spacing based on the current typeface and * text size. * + *

Note that this is the value for the main typeface, and actual text rendered may need a + * larger value because fallback fonts may get used in rendering the text. + * * @return recommend line spacing based on the current typeface and * text size. */ diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp index ba4e3a4df578a..2b29542fb6238 100644 --- a/libs/hwui/hwui/MinikinSkia.cpp +++ b/libs/hwui/hwui/MinikinSkia.cpp @@ -67,6 +67,17 @@ void MinikinFontSkia::GetBounds(minikin::MinikinRect* bounds, uint32_t glyph_id, bounds->mBottom = skBounds.fBottom; } +void MinikinFontSkia::GetFontExtent(minikin::MinikinExtent* extent, + const minikin::MinikinPaint& paint) const { + SkPaint skPaint; + MinikinFontSkia_SetSkiaPaint(this, &skPaint, paint); + SkPaint::FontMetrics metrics; + skPaint.getFontMetrics(&metrics); + extent->ascent = metrics.fAscent; + extent->descent = metrics.fDescent; + extent->line_gap = metrics.fLeading; +} + SkTypeface *MinikinFontSkia::GetSkTypeface() const { return mTypeface.get(); } diff --git a/libs/hwui/hwui/MinikinSkia.h b/libs/hwui/hwui/MinikinSkia.h index 6c12485845fd0..a19f4a7694443 100644 --- a/libs/hwui/hwui/MinikinSkia.h +++ b/libs/hwui/hwui/MinikinSkia.h @@ -37,6 +37,9 @@ public: void GetBounds(minikin::MinikinRect* bounds, uint32_t glyph_id, const minikin::MinikinPaint &paint) const; + void GetFontExtent(minikin::MinikinExtent* extent, + const minikin::MinikinPaint &paint) const; + SkTypeface* GetSkTypeface() const; sk_sp RefSkTypeface() const; diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp index 5e7f1cf2da1cb..5577bbf2bfc9b 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -68,7 +68,7 @@ float MinikinUtils::measureText(const Paint* paint, int bidiFlags, const Typefac minikin::FontStyle minikinStyle = prepareMinikinPaint(&minikinPaint, paint, typeface); const Typeface* resolvedTypeface = Typeface::resolveDefault(typeface); return minikin::Layout::measureText(buf, start, count, bufSize, bidiFlags, minikinStyle, - minikinPaint, resolvedTypeface->fFontCollection, advances); + minikinPaint, resolvedTypeface->fFontCollection, advances, nullptr /* extent */); } bool MinikinUtils::hasVariationSelector(const Typeface* typeface, uint32_t codepoint, uint32_t vs) {