Add breakStrategy attribute to TextView
This patch adds plumbing to TextView to control the strategy used
for breaking paragraphs into lines.
The default for TextView is "quality", while the default for EditText
is "simple", largely to avoid too much re-layout when editing.
StaticLayout now has a builder which provides access to more
functionality and is also cleaner than the old mechanism of having
lots of constructors with varying numbers of arguments. This patch
changes TextView to use that builder, and also contains cleanups
of the Builder within StaticLayout.
Change-Id: Iee3cf3a05a3e51ba0834554e4a3ec606e9cabca5
diff --git a/api/current.txt b/api/current.txt
index b3d5fd0..202139c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -354,6 +354,7 @@
field public static final int bottomRightRadius = 16843180; // 0x10101ac
field public static final int breadCrumbShortTitle = 16843524; // 0x1010304
field public static final int breadCrumbTitle = 16843523; // 0x1010303
+ field public static final int breakStrategy = 16844011; // 0x10104eb
field public static final int bufferType = 16843086; // 0x101014e
field public static final int button = 16843015; // 0x1010107
field public static final int buttonBarButtonStyle = 16843567; // 0x101032f
@@ -30920,6 +30921,9 @@
method public final void increaseWidthTo(int);
method public boolean isRtlCharAt(int);
method protected final boolean isSpanned();
+ field public static final int BREAK_STRATEGY_BALANCED = 2; // 0x2
+ field public static final int BREAK_STRATEGY_HIGH_QUALITY = 1; // 0x1
+ field public static final int BREAK_STRATEGY_SIMPLE = 0; // 0x0
field public static final int DIR_LEFT_TO_RIGHT = 1; // 0x1
field public static final int DIR_RIGHT_TO_LEFT = -1; // 0xffffffff
}
@@ -40082,6 +40086,7 @@
method public void endBatchEdit();
method public boolean extractText(android.view.inputmethod.ExtractedTextRequest, android.view.inputmethod.ExtractedText);
method public final int getAutoLinkMask();
+ method public int getBreakStrategy();
method public int getCompoundDrawablePadding();
method public android.content.res.ColorStateList getCompoundDrawableTintList();
method public android.graphics.PorterDuff.Mode getCompoundDrawableTintMode();
@@ -40183,6 +40188,7 @@
method public void removeTextChangedListener(android.text.TextWatcher);
method public void setAllCaps(boolean);
method public final void setAutoLinkMask(int);
+ method public void setBreakStrategy(int);
method public void setCompoundDrawablePadding(int);
method public void setCompoundDrawableTintList(android.content.res.ColorStateList);
method public void setCompoundDrawableTintMode(android.graphics.PorterDuff.Mode);
diff --git a/api/system-current.txt b/api/system-current.txt
index 9462054..c6c37d5 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -426,6 +426,7 @@
field public static final int bottomRightRadius = 16843180; // 0x10101ac
field public static final int breadCrumbShortTitle = 16843524; // 0x1010304
field public static final int breadCrumbTitle = 16843523; // 0x1010303
+ field public static final int breakStrategy = 16844011; // 0x10104eb
field public static final int bufferType = 16843086; // 0x101014e
field public static final int button = 16843015; // 0x1010107
field public static final int buttonBarButtonStyle = 16843567; // 0x101032f
@@ -33462,6 +33463,9 @@
method public final void increaseWidthTo(int);
method public boolean isRtlCharAt(int);
method protected final boolean isSpanned();
+ field public static final int BREAK_STRATEGY_BALANCED = 2; // 0x2
+ field public static final int BREAK_STRATEGY_HIGH_QUALITY = 1; // 0x1
+ field public static final int BREAK_STRATEGY_SIMPLE = 0; // 0x0
field public static final int DIR_LEFT_TO_RIGHT = 1; // 0x1
field public static final int DIR_RIGHT_TO_LEFT = -1; // 0xffffffff
}
@@ -42925,6 +42929,7 @@
method public void endBatchEdit();
method public boolean extractText(android.view.inputmethod.ExtractedTextRequest, android.view.inputmethod.ExtractedText);
method public final int getAutoLinkMask();
+ method public int getBreakStrategy();
method public int getCompoundDrawablePadding();
method public android.content.res.ColorStateList getCompoundDrawableTintList();
method public android.graphics.PorterDuff.Mode getCompoundDrawableTintMode();
@@ -43026,6 +43031,7 @@
method public void removeTextChangedListener(android.text.TextWatcher);
method public void setAllCaps(boolean);
method public final void setAutoLinkMask(int);
+ method public void setBreakStrategy(int);
method public void setCompoundDrawablePadding(int);
method public void setCompoundDrawableTintList(android.content.res.ColorStateList);
method public void setCompoundDrawableTintMode(android.graphics.PorterDuff.Mode);
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index 7d2e1ef..239b386 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -79,7 +79,8 @@
boolean includepad,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
- spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth);
+ spacingmult, spacingadd, includepad, StaticLayout.BREAK_STRATEGY_SIMPLE,
+ ellipsize, ellipsizedWidth);
}
/**
@@ -95,7 +96,7 @@
TextPaint paint,
int width, Alignment align, TextDirectionHeuristic textDir,
float spacingmult, float spacingadd,
- boolean includepad,
+ boolean includepad, int breakStrategy,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
super((ellipsize == null)
? display
@@ -120,6 +121,7 @@
mObjects = new PackedObjectVector<Directions>(1);
mIncludePad = includepad;
+ mBreakStrategy = breakStrategy;
/*
* This is annoying, but we can't refer to the layout until
@@ -279,10 +281,9 @@
sBuilder = null;
}
- // TODO: make sure reflowed is properly initialized
if (reflowed == null) {
reflowed = new StaticLayout(null);
- b = StaticLayout.Builder.obtain();
+ b = StaticLayout.Builder.obtain(text, where, where + after, getWidth());
}
b.setText(text, where, where + after)
@@ -292,7 +293,8 @@
.setSpacingMult(getSpacingMultiplier())
.setSpacingAdd(getSpacingAdd())
.setEllipsizedWidth(mEllipsizedWidth)
- .setEllipsize(mEllipsizeAt);
+ .setEllipsize(mEllipsizeAt)
+ .setBreakStrategy(mBreakStrategy);
reflowed.generate(b, false, true);
int n = reflowed.getLineCount();
@@ -717,6 +719,7 @@
private boolean mEllipsize;
private int mEllipsizedWidth;
private TextUtils.TruncateAt mEllipsizeAt;
+ private int mBreakStrategy;
private PackedIntVector mInts;
private PackedObjectVector<Directions> mObjects;
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 22abb18..16ae5e2 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -16,6 +16,7 @@
package android.text;
+import android.annotation.IntDef;
import android.emoji.EmojiFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
@@ -33,6 +34,8 @@
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.GrowingArrayUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
/**
@@ -43,6 +46,31 @@
* For text that will not change, use a {@link StaticLayout}.
*/
public abstract class Layout {
+ /** @hide */
+ @IntDef({BREAK_STRATEGY_SIMPLE, BREAK_STRATEGY_HIGH_QUALITY, BREAK_STRATEGY_BALANCED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BreakStrategy {}
+
+ /**
+ * Value for break strategy indicating simple line breaking. Automatic hyphens are not added
+ * (though soft hyphens are respected), and modifying text generally doesn't affect the layout
+ * before it (which yields a more consistent user experience when editing), but layout may not
+ * be the highest quality.
+ */
+ public static final int BREAK_STRATEGY_SIMPLE = 0;
+
+ /**
+ * Value for break strategy indicating high quality line breaking, including automatic
+ * hyphenation and doing whole-paragraph optimization of line breaks.
+ */
+ public static final int BREAK_STRATEGY_HIGH_QUALITY = 1;
+
+ /**
+ * Value for break strategy indicating balanced line breaking. The breaks are chosen to
+ * make all lines as close to the same length as possible, including automatic hyphenation.
+ */
+ public static final int BREAK_STRATEGY_BALANCED = 2;
+
private static final ParagraphStyle[] NO_PARA_SPANS =
ArrayUtils.emptyArray(ParagraphStyle.class);
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 4174df0..2bcb352 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -23,6 +23,7 @@
import android.text.style.MetricAffectingSpan;
import android.text.style.TabStopSpan;
import android.util.Log;
+import android.util.Pools.SynchronizedPool;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.GrowingArrayUtils;
@@ -56,28 +57,23 @@
mNativePtr = nNewBuilder();
}
- static Builder obtain() {
- Builder b = null;
- synchronized (sLock) {
- for (int i = 0; i < sCached.length; i++) {
- if (sCached[i] != null) {
- b = sCached[i];
- sCached[i] = null;
- break;
- }
- }
- }
+ public static Builder obtain(CharSequence source, int start, int end, int width) {
+ Builder b = sPool.acquire();
if (b == null) {
b = new Builder();
}
// set default initial values
- b.mWidth = 0;
+ b.mText = source;
+ b.mStart = start;
+ b.mEnd = end;
+ b.mWidth = width;
+ b.mAlignment = Alignment.ALIGN_NORMAL;
b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
b.mSpacingMult = 1.0f;
b.mSpacingAdd = 0.0f;
b.mIncludePad = true;
- b.mEllipsizedWidth = 0;
+ b.mEllipsizedWidth = width;
b.mEllipsize = null;
b.mMaxLines = Integer.MAX_VALUE;
@@ -85,18 +81,11 @@
return b;
}
- static void recycle(Builder b) {
+ private static void recycle(Builder b) {
b.mPaint = null;
b.mText = null;
MeasuredText.recycle(b.mMeasuredText);
- synchronized (sLock) {
- for (int i = 0; i < sCached.length; i++) {
- if (sCached[i] == null) {
- sCached[i] = b;
- break;
- }
- }
- }
+ sPool.release(b);
}
// release any expensive state
@@ -129,6 +118,11 @@
return this;
}
+ public Builder setAlignment(Alignment alignment) {
+ mAlignment = alignment;
+ return this;
+ }
+
public Builder setTextDir(TextDirectionHeuristic textDir) {
mTextDir = textDir;
return this;
@@ -166,6 +160,11 @@
return this;
}
+ public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
+ mBreakStrategy = breakStrategy;
+ return this;
+ }
+
/**
* Measurement and break iteration is done in native code. The protocol for using
* the native code is as follows.
@@ -207,10 +206,8 @@
}
public StaticLayout build() {
- // TODO: can optimize based on whether ellipsis is needed
- StaticLayout result = new StaticLayout(mText);
- result.generate(this, this.mIncludePad, this.mIncludePad);
- recycle(this);
+ StaticLayout result = new StaticLayout(this);
+ Builder.recycle(this);
return result;
}
@@ -230,6 +227,7 @@
int mEnd;
TextPaint mPaint;
int mWidth;
+ Alignment mAlignment;
TextDirectionHeuristic mTextDir;
float mSpacingMult;
float mSpacingAdd;
@@ -237,6 +235,7 @@
int mEllipsizedWidth;
TextUtils.TruncateAt mEllipsize;
int mMaxLines;
+ int mBreakStrategy;
Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
@@ -245,8 +244,7 @@
Locale mLocale;
- private static final Object sLock = new Object();
- private static final Builder[] sCached = new Builder[3];
+ private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3);
}
public StaticLayout(CharSequence source, TextPaint paint,
@@ -316,10 +314,9 @@
: new Ellipsizer(source),
paint, outerwidth, align, textDir, spacingmult, spacingadd);
- Builder b = Builder.obtain();
- b.setText(source, bufstart, bufend)
+ Builder b = Builder.obtain(source, bufstart, bufend, outerwidth)
.setPaint(paint)
- .setWidth(outerwidth)
+ .setAlignment(align)
.setTextDir(textDir)
.setSpacingMult(spacingmult)
.setSpacingAdd(spacingadd)
@@ -366,6 +363,35 @@
mLines = new int[mLineDirections.length];
}
+ private StaticLayout(Builder b) {
+ super((b.mEllipsize == null)
+ ? b.mText
+ : (b.mText instanceof Spanned)
+ ? new SpannedEllipsizer(b.mText)
+ : new Ellipsizer(b.mText),
+ b.mPaint, b.mWidth, b.mAlignment, b.mSpacingMult, b.mSpacingAdd);
+
+ if (b.mEllipsize != null) {
+ Ellipsizer e = (Ellipsizer) getText();
+
+ e.mLayout = this;
+ e.mWidth = b.mEllipsizedWidth;
+ e.mMethod = b.mEllipsize;
+ mEllipsizedWidth = b.mEllipsizedWidth;
+
+ mColumns = COLUMNS_ELLIPSIZE;
+ } else {
+ mColumns = COLUMNS_NORMAL;
+ mEllipsizedWidth = b.mWidth;
+ }
+
+ mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns);
+ mLines = new int[mLineDirections.length];
+ mMaximumVisibleLineCount = b.mMaxLines;
+
+ generate(b, b.mIncludePad, b.mIncludePad);
+ }
+
/* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
CharSequence source = b.mText;
int bufStart = b.mStart;
@@ -477,10 +503,9 @@
}
}
- int breakStrategy = 0; // 0 = kBreakStrategy_Greedy
nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart,
firstWidth, firstWidthLineCount, restWidth,
- variableTabStops, TAB_INCREMENT, breakStrategy);
+ variableTabStops, TAB_INCREMENT, b.mBreakStrategy);
// measurement has to be done before performing line breaking
// but we don't want to recompute fontmetrics or span ranges the
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 718ef93..2723080 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -543,6 +543,8 @@
private float mSpacingMult = 1.0f;
private float mSpacingAdd = 0.0f;
+ private int mBreakStrategy;
+
private int mMaximum = Integer.MAX_VALUE;
private int mMaxMode = LINES;
private int mMinimum = 0;
@@ -680,6 +682,7 @@
boolean elegant = false;
float letterSpacing = 0;
String fontFeatureSettings = null;
+ mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
final Resources.Theme theme = context.getTheme();
@@ -1133,6 +1136,9 @@
case com.android.internal.R.styleable.TextView_fontFeatureSettings:
fontFeatureSettings = a.getString(attr);
break;
+
+ case com.android.internal.R.styleable.TextView_breakStrategy:
+ mBreakStrategy = a.getInt(attr, Layout.BREAK_STRATEGY_SIMPLE);
}
}
a.recycle();
@@ -2960,6 +2966,35 @@
}
/**
+ * Sets the break strategy for breaking paragraphs into lines. The default value for
+ * TextView is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}, and the default value for
+ * EditText is {@link Layout#BREAK_STRATEGY_SIMPLE}, the latter to avoid the
+ * text "dancing" when being edited.
+ *
+ * @attr ref android.R.styleable#TextView_breakStrategy
+ * @see #getBreakStrategy()
+ */
+ public void setBreakStrategy(@Layout.BreakStrategy int breakStrategy) {
+ mBreakStrategy = breakStrategy;
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * @return the currently set break strategy.
+ *
+ * @attr ref android.R.styleable#TextView_breakStrategy
+ * @see #setBreakStrategy(int)
+ */
+ @Layout.BreakStrategy
+ public int getBreakStrategy() {
+ return mBreakStrategy;
+ }
+
+ /**
* Sets font feature settings. The format is the same as the CSS
* font-feature-settings attribute:
* http://dev.w3.org/csswg/css-fonts/#propdef-font-feature-settings
@@ -6492,27 +6527,25 @@
hintBoring, mIncludePad, mEllipsize,
ellipsisWidth);
}
- } else if (shouldEllipsize) {
- mHintLayout = new StaticLayout(mHint,
- 0, mHint.length(),
- mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
- mSpacingAdd, mIncludePad, mEllipsize,
- ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
- } else {
- mHintLayout = new StaticLayout(mHint, mTextPaint,
- hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
- mIncludePad);
}
- } else if (shouldEllipsize) {
- mHintLayout = new StaticLayout(mHint,
- 0, mHint.length(),
- mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
- mSpacingAdd, mIncludePad, mEllipsize,
- ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
- } else {
- mHintLayout = new StaticLayout(mHint, mTextPaint,
- hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
- mIncludePad);
+ }
+ // TODO: code duplication with makeSingleLayout()
+ if (mHintLayout == null) {
+ StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
+ mHint.length(), hintWidth)
+ .setPaint(mTextPaint)
+ .setAlignment(alignment)
+ .setTextDir(mTextDir)
+ .setSpacingMult(mSpacingMult)
+ .setSpacingAdd(mSpacingAdd)
+ .setIncludePad(mIncludePad)
+ .setBreakStrategy(mBreakStrategy);
+ if (shouldEllipsize) {
+ builder.setEllipsize(mEllipsize)
+ .setEllipsizedWidth(ellipsisWidth)
+ .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
+ }
+ mHintLayout = builder.build();
}
}
@@ -6544,9 +6577,8 @@
Layout result = null;
if (mText instanceof Spannable) {
result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
- alignment, mTextDir, mSpacingMult,
- mSpacingAdd, mIncludePad, getKeyListener() == null ? effectiveEllipsize : null,
- ellipsisWidth);
+ alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, mBreakStrategy,
+ getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
} else {
if (boring == UNKNOWN_BORING) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
@@ -6583,29 +6615,27 @@
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
}
- } else if (shouldEllipsize) {
- result = new StaticLayout(mTransformed,
- 0, mTransformed.length(),
- mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
- mSpacingAdd, mIncludePad, effectiveEllipsize,
- ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
- } else {
- result = new StaticLayout(mTransformed, mTextPaint,
- wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
- mIncludePad);
}
- } else if (shouldEllipsize) {
- result = new StaticLayout(mTransformed,
- 0, mTransformed.length(),
- mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
- mSpacingAdd, mIncludePad, effectiveEllipsize,
- ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
- } else {
- result = new StaticLayout(mTransformed, mTextPaint,
- wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
- mIncludePad);
}
}
+ if (result == null) {
+ StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
+ 0, mTransformed.length(), wantWidth)
+ .setPaint(mTextPaint)
+ .setAlignment(alignment)
+ .setTextDir(mTextDir)
+ .setSpacingMult(mSpacingMult)
+ .setSpacingAdd(mSpacingAdd)
+ .setIncludePad(mIncludePad)
+ .setBreakStrategy(mBreakStrategy);
+ if (shouldEllipsize) {
+ builder.setEllipsize(effectiveEllipsize)
+ .setEllipsizedWidth(ellipsisWidth)
+ .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
+ }
+ // TODO: explore always setting maxLines
+ result = builder.build();
+ }
return result;
}
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index b6d32b2..3945222 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -4283,6 +4283,15 @@
<attr name="letterSpacing" />
<!-- Font feature settings. -->
<attr name="fontFeatureSettings" />
+ <!-- Break strategy (control over paragraph layout). -->
+ <attr name="breakStrategy">
+ <!-- Line breaking uses simple strategy. -->
+ <enum name="simple" value="0" />
+ <!-- Line breaking uses high-quality strategy, including hyphenation. -->
+ <enum name="high_quality" value="1" />
+ <!-- Line breaking stratgegy balances line lengths. -->
+ <enum name="balanced" value="2" />
+ </attr>
</declare-styleable>
<declare-styleable name="TextViewAppearance">
<!-- Base text color, typeface, size, and style. -->
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 5c7daf2..f59a4d8 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2656,5 +2656,6 @@
<!--IntentFilter auto verification -->
<public type="attr" name="autoVerify" />
+ <public type="attr" name="breakStrategy" />
</resources>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index cc64b43..3c3d286 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -497,6 +497,7 @@
<item name="textEditSideNoPasteWindowLayout">?attr/textEditSideNoPasteWindowLayout</item>
<item name="textEditSuggestionItemLayout">?attr/textEditSuggestionItemLayout</item>
<item name="textCursorDrawable">?attr/textCursorDrawable</item>
+ <item name="breakStrategy">high_quality</item>
</style>
<style name="Widget.CheckedTextView">
@@ -527,6 +528,7 @@
<item name="textAppearance">?attr/textAppearanceMediumInverse</item>
<item name="textColor">?attr/editTextColor</item>
<item name="gravity">center_vertical</item>
+ <item name="breakStrategy">simple</item>
</style>
<style name="Widget.ExpandableListView" parent="Widget.ListView">