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
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 0000000..74d1366
--- /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 6bf11bb..69323c1 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -41761,6 +41761,7 @@
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 df2df2b..973eb99 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -45362,6 +45362,7 @@
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 aa08dd9..fe66ec9 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -42032,6 +42032,7 @@
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 6a7db4e..dd82e1e 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -90,6 +90,7 @@
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;
@@ -228,6 +229,24 @@
}
/**
+ * 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.
+ *
+ * <p>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}
* passed to {@link #obtain}.
@@ -432,6 +451,7 @@
float mSpacingMult;
float mSpacingAdd;
boolean mIncludePad;
+ boolean mFallbackLineSpacing;
int mEllipsizedWidth;
TextUtils.TruncateAt mEllipsize;
int mMaxLines;
@@ -606,6 +626,7 @@
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 @@
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 @@
&& 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 @@
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 @@
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 @@
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 @@
// 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 @@
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 82a6411..ed6942e 100644
--- a/core/jni/android_text_StaticLayout.cpp
+++ b/core/jni/android_text_StaticLayout.cpp
@@ -44,6 +44,8 @@
struct JLineBreaksID {
jfieldID breaks;
jfieldID widths;
+ jfieldID ascents;
+ jfieldID descents;
jfieldID flags;
};
@@ -73,35 +75,45 @@
}
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<minikin::LineBreaker*>(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 @@
{"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 @@
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 0000000..f34698f
--- /dev/null
+++ b/core/tests/coretests/assets/fonts/ascent1em-descent2em.ttf
Binary files differ
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 0000000..68d4323
--- /dev/null
+++ b/core/tests/coretests/assets/fonts/ascent1em-descent2em.ttx
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.0">
+
+ <GlyphOrder>
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="1em"/>
+ </GlyphOrder>
+
+ <head>
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x640cdb2f"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1000"/>
+ <created value="Fri Mar 17 07:26:00 2017"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="7"/>
+ <fontDirectionHint value="2"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x10000"/>
+ <ascent value="1000"/>
+ <descent value="-2000"/>
+ <lineGap value="0"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ </hhea>
+
+ <maxp>
+ <tableVersion value="0x10000"/>
+ <maxZones value="0"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="3"/>
+ <xAvgCharWidth value="594"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00001000"/>
+ <ySubscriptXSize value="650"/>
+ <ySubscriptYSize value="600"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="75"/>
+ <ySuperscriptXSize value="650"/>
+ <ySuperscriptYSize value="600"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="350"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="300"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="5"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="UKWN"/>
+ <fsSelection value="00000000 01000000"/>
+ <usFirstCharIndex value="32"/>
+ <usLastCharIndex value="122"/>
+ <sTypoAscender value="800"/>
+ <sTypoDescender value="-200"/>
+ <sTypoLineGap value="200"/>
+ <usWinAscent value="1000"/>
+ <usWinDescent value="200"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="500"/>
+ <sCapHeight value="700"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="0"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="1000" lsb="0"/>
+ <mtx name="1em" width="1000" lsb="0"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="3" platEncID="10" language="0">
+ <map code="0x0020" name="1em" /> <!-- SPACE -->
+ <map code="0x0061" name="1em" /> <!-- LATIN SMALL LETTER A -->
+ </cmap_format_4>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+ <TTGlyph name=".notdef" xMin="0" yMin="0" xMax="0" yMax="0" />
+ <TTGlyph name="1em" xMin="0" yMin="0" xMax="0" yMax="0" />
+ </glyf>
+
+ <name>
+ <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
+ Copyright (C) 2017 The Android Open Source Project
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Sample Font
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Sample Font
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ SampleFont-Regular
+ </namerecord>
+ <namerecord nameID="13" platformID="3" platEncID="1" langID="0x409">
+ 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.
+ </namerecord>
+ <namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
+ http://www.apache.org/licenses/LICENSE-2.0
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="3.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-75"/>
+ <underlineThickness value="50"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ </post>
+
+</ttFont>
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 0000000..085d133
--- /dev/null
+++ b/core/tests/coretests/assets/fonts/ascent3em-descent4em.ttf
Binary files differ
diff --git a/core/tests/coretests/assets/fonts/ascent3em-descent4em.ttx b/core/tests/coretests/assets/fonts/ascent3em-descent4em.ttx
new file mode 100644
index 0000000..432331b
--- /dev/null
+++ b/core/tests/coretests/assets/fonts/ascent3em-descent4em.ttx
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.0">
+
+ <GlyphOrder>
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="1em"/>
+ </GlyphOrder>
+
+ <head>
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x640cdb2f"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1000"/>
+ <created value="Fri Mar 17 07:26:00 2017"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="7"/>
+ <fontDirectionHint value="2"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x10000"/>
+ <ascent value="3000"/>
+ <descent value="-4000"/>
+ <lineGap value="0"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ </hhea>
+
+ <maxp>
+ <tableVersion value="0x10000"/>
+ <maxZones value="0"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="3"/>
+ <xAvgCharWidth value="594"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00001000"/>
+ <ySubscriptXSize value="650"/>
+ <ySubscriptYSize value="600"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="75"/>
+ <ySuperscriptXSize value="650"/>
+ <ySuperscriptYSize value="600"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="350"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="300"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="5"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="UKWN"/>
+ <fsSelection value="00000000 01000000"/>
+ <usFirstCharIndex value="32"/>
+ <usLastCharIndex value="122"/>
+ <sTypoAscender value="800"/>
+ <sTypoDescender value="-200"/>
+ <sTypoLineGap value="200"/>
+ <usWinAscent value="1000"/>
+ <usWinDescent value="200"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="500"/>
+ <sCapHeight value="700"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="0"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="1000" lsb="0"/>
+ <mtx name="1em" width="1000" lsb="0"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="3" platEncID="10" language="0">
+ <map code="0x0062" name="1em" /> <!-- LATIN SMALL LETTER B -->
+ </cmap_format_4>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+ <TTGlyph name=".notdef" xMin="0" yMin="0" xMax="0" yMax="0" />
+ <TTGlyph name="1em" xMin="0" yMin="0" xMax="0" yMax="0" />
+ </glyf>
+
+ <name>
+ <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
+ Copyright (C) 2017 The Android Open Source Project
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Sample Font
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Sample Font
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ SampleFont-Regular
+ </namerecord>
+ <namerecord nameID="13" platformID="3" platEncID="1" langID="0x409">
+ 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.
+ </namerecord>
+ <namerecord nameID="14" platformID="3" platEncID="1" langID="0x409">
+ http://www.apache.org/licenses/LICENSE-2.0
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="3.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-75"/>
+ <underlineThickness value="50"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ </post>
+
+</ttFont>
diff --git a/core/tests/coretests/src/android/text/StaticLayoutTest.java b/core/tests/coretests/src/android/text/StaticLayoutTest.java
index 2dfab87..1bf32ca 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 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 @@
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<String, Typeface> fontMap = new ArrayMap<>();
+ final ArrayMap<String, FontFamily[]> 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 = "<?xml version='1.0' encoding='UTF-8'?>"
+ + "<familyset>"
+ + " <family name='sans-serif'>"
+ + " <font weight='400' style='normal'>ascent1em-descent2em.ttf</font>"
+ + " </family>"
+ + " <family>"
+ + " <font weight='400' style='normal'>ascent3em-descent4em.ttf</font>"
+ + " </family>"
+ + "</familyset>";
+
+ 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 46ada23..1a06a56 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -1729,6 +1729,9 @@
* Return the distance above (negative) the baseline (ascent) based on the
* current typeface and text size.
*
+ * <p>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 @@
* Return the distance below (positive) the baseline (descent) based on the
* current typeface and text size.
*
+ * <p>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 @@
* settings for typeface, textSize, etc. If metrics is not null, return the
* fontmetric values in it.
*
+ * <p>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 @@
* and clipping. If you want more control over the rounding, call
* getFontMetrics().
*
+ * <p>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 @@
* Return the recommend line spacing based on the current typeface and
* text size.
*
+ * <p>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 ba4e3a4..2b29542 100644
--- a/libs/hwui/hwui/MinikinSkia.cpp
+++ b/libs/hwui/hwui/MinikinSkia.cpp
@@ -67,6 +67,17 @@
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 6c12485..a19f4a7 100644
--- a/libs/hwui/hwui/MinikinSkia.h
+++ b/libs/hwui/hwui/MinikinSkia.h
@@ -37,6 +37,9 @@
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<SkTypeface> RefSkTypeface() const;
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index 5e7f1cf..5577bbf 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -68,7 +68,7 @@
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) {