Support fallback linespacing in DynamicLayout
Bug: 28963299
Test: bit FrameworksCoreTests:android.text.
Change-Id: I132499d5927b26fb45522ffee99bac12aca3721f
diff --git a/api/current.txt b/api/current.txt
index 74adf5c..b95dc19 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -41417,6 +41417,7 @@
method public android.text.DynamicLayout.Builder setJustificationMode(int);
method public android.text.DynamicLayout.Builder setLineSpacing(float, float);
method public android.text.DynamicLayout.Builder setTextDirection(android.text.TextDirectionHeuristic);
+ method public android.text.DynamicLayout.Builder setUseLineSpacingFromFallbacks(boolean);
}
public abstract interface Editable implements java.lang.Appendable java.lang.CharSequence android.text.GetChars android.text.Spannable {
diff --git a/api/system-current.txt b/api/system-current.txt
index 1841ce0..7036c90 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -45019,6 +45019,7 @@
method public android.text.DynamicLayout.Builder setJustificationMode(int);
method public android.text.DynamicLayout.Builder setLineSpacing(float, float);
method public android.text.DynamicLayout.Builder setTextDirection(android.text.TextDirectionHeuristic);
+ method public android.text.DynamicLayout.Builder setUseLineSpacingFromFallbacks(boolean);
}
public abstract interface Editable implements java.lang.Appendable java.lang.CharSequence android.text.GetChars android.text.Spannable {
diff --git a/api/test-current.txt b/api/test-current.txt
index 2bb96fe..170261a 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -41681,6 +41681,7 @@
method public android.text.DynamicLayout.Builder setJustificationMode(int);
method public android.text.DynamicLayout.Builder setLineSpacing(float, float);
method public android.text.DynamicLayout.Builder setTextDirection(android.text.TextDirectionHeuristic);
+ method public android.text.DynamicLayout.Builder setUseLineSpacingFromFallbacks(boolean);
}
public abstract interface Editable implements java.lang.Appendable java.lang.CharSequence android.text.GetChars android.text.Spannable {
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index 661b608..5e40935 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -80,6 +80,7 @@
b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
b.mIncludePad = true;
+ b.mFallbackLineSpacing = false;
b.mEllipsizedWidth = width;
b.mEllipsize = null;
b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
@@ -172,6 +173,25 @@
}
/**
+ * 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
+ */
+ @NonNull
+ 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}.
*
@@ -270,6 +290,7 @@
private float mSpacingMult;
private float mSpacingAdd;
private boolean mIncludePad;
+ private boolean mFallbackLineSpacing;
private int mBreakStrategy;
private int mHyphenationFrequency;
private int mJustificationMode;
@@ -320,7 +341,7 @@
@IntRange(from = 0) int ellipsizedWidth) {
this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
spacingmult, spacingadd, includepad,
- StaticLayout.BREAK_STRATEGY_SIMPLE, StaticLayout.HYPHENATION_FREQUENCY_NONE,
+ Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE,
Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth);
}
@@ -388,6 +409,7 @@
private void generate(@NonNull Builder b) {
mBase = b.mBase;
+ mFallbackLineSpacing = b.mFallbackLineSpacing;
if (b.mEllipsize != null) {
mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
mEllipsizedWidth = b.mEllipsizedWidth;
@@ -573,6 +595,7 @@
.setWidth(getWidth())
.setTextDirection(getTextDirectionHeuristic())
.setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
+ .setUseLineSpacingFromFallbacks(mFallbackLineSpacing)
.setEllipsizedWidth(mEllipsizedWidth)
.setEllipsize(mEllipsizeAt)
.setBreakStrategy(mBreakStrategy)
@@ -1033,10 +1056,11 @@
private void reflow(CharSequence s, int where, int before, int after) {
DynamicLayout ml = mLayout.get();
- if (ml != null)
+ if (ml != null) {
ml.reflow(s, where, before, after);
- else if (s instanceof Spannable)
+ } else if (s instanceof Spannable) {
((Spannable) s).removeSpan(this);
+ }
}
public void beforeTextChanged(CharSequence s, int where, int before, int after) {
@@ -1093,6 +1117,7 @@
private CharSequence mDisplay;
private ChangeWatcher mWatcher;
private boolean mIncludePad;
+ private boolean mFallbackLineSpacing;
private boolean mEllipsize;
private int mEllipsizedWidth;
private TextUtils.TruncateAt mEllipsizeAt;
diff --git a/core/tests/coretests/src/android/text/DynamicLayoutTest.java b/core/tests/coretests/src/android/text/DynamicLayoutTest.java
index 5ef08e0..ed6bfbf 100644
--- a/core/tests/coretests/src/android/text/DynamicLayoutTest.java
+++ b/core/tests/coretests/src/android/text/DynamicLayoutTest.java
@@ -186,4 +186,77 @@
ALIGN_NORMAL, 1.0f /*spacingMultiplier*/, 0f /*spacingAdd*/, false /*includepad*/);
layout.getLineExtra(100);
}
+
+ @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 (FontFallbackSetup setup =
+ new FontFallbackSetup("DynamicLayout", testFontFiles, xml)) {
+ final TextPaint paint = setup.getPaintFor("sans-serif");
+ 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.
+ DynamicLayout layout = DynamicLayout.Builder
+ .obtain(text, 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 = DynamicLayout.Builder
+ .obtain(text, 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 = DynamicLayout.Builder
+ .obtain(text, 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));
+ }
+ }
}
diff --git a/core/tests/coretests/src/android/text/FontFallbackSetup.java b/core/tests/coretests/src/android/text/FontFallbackSetup.java
new file mode 100644
index 0000000..bcf2514
--- /dev/null
+++ b/core/tests/coretests/src/android/text/FontFallbackSetup.java
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+
+package android.text;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.FontFamily;
+import android.graphics.Typeface;
+import android.support.test.InstrumentationRegistry;
+import android.util.ArrayMap;
+
+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;
+
+public class FontFallbackSetup implements AutoCloseable {
+ private final String[] mTestFontFiles;
+ private final String mXml;
+ private final String mTestFontsDir;
+ final ArrayMap<String, Typeface> mFontMap = new ArrayMap<>();
+
+ public FontFallbackSetup(@NonNull String testSubDir, @NonNull String[] testFontFiles,
+ @NonNull String xml) {
+ mTestFontFiles = testFontFiles;
+ mXml = xml;
+
+ final Context targetCtx = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ final File cacheDir = new File(targetCtx.getCacheDir(), testSubDir);
+ if (!cacheDir.isDirectory()) {
+ final boolean dirsCreated = cacheDir.mkdirs();
+ if (!dirsCreated) {
+ throw new RuntimeException("Creating test directories for fonts failed.");
+ }
+ }
+ mTestFontsDir = cacheDir.getAbsolutePath() + "/";
+
+ final String testFontsXml = new File(mTestFontsDir, "fonts.xml").getAbsolutePath();
+ final AssetManager am =
+ InstrumentationRegistry.getInstrumentation().getContext().getAssets();
+ for (String fontFile : mTestFontFiles) {
+ final String sourceInAsset = "fonts/" + fontFile;
+ final File outInCache = new File(mTestFontsDir, 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(mXml.getBytes(Charset.forName("UTF-8")));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();
+ Typeface.buildSystemFallback(testFontsXml, mTestFontsDir, mFontMap, fallbackMap);
+ }
+
+ @NonNull
+ public TextPaint getPaintFor(@NonNull String fontName) {
+ final Typeface testTypeface = mFontMap.get(fontName);
+ final TextPaint paint = new TextPaint();
+ paint.setTypeface(testTypeface);
+ return paint;
+ }
+
+ @Override
+ public void close() {
+ for (String fontFile : mTestFontFiles) {
+ final File outInCache = new File(mTestFontsDir, fontFile);
+ outInCache.delete();
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/text/StaticLayoutTest.java b/core/tests/coretests/src/android/text/StaticLayoutTest.java
index 1bf32ca..ab4b119 100644
--- a/core/tests/coretests/src/android/text/StaticLayoutTest.java
+++ b/core/tests/coretests/src/android/text/StaticLayoutTest.java
@@ -21,33 +21,20 @@
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;
@@ -813,57 +800,6 @@
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.
@@ -883,8 +819,9 @@
+ " </family>"
+ "</familyset>";
- try {
- final TextPaint paint = setupPaintForFallbackFonts(testFontFiles, xml);
+ try (FontFallbackSetup setup =
+ new FontFallbackSetup("StaticLayout", testFontFiles, xml)) {
+ final TextPaint paint = setup.getPaintFor("sans-serif");
final int textSize = 100;
paint.setTextSize(textSize);
assertEquals(-textSize, paint.ascent(), 0.0f);
@@ -933,8 +870,6 @@
assertEquals(2 * textSize, layout.getLineDescent(1));
assertEquals(-textSize, layout.getLineAscent(2));
assertEquals(2 * textSize, layout.getLineDescent(2));
- } finally {
- destroyFallbackFonts(testFontFiles);
}
}
}