summaryrefslogtreecommitdiff
path: root/core/java/android/text/TextLine.java
diff options
context:
space:
mode:
authorRoozbeh Pournader <roozbeh@google.com>2017-05-24 09:07:52 -0700
committerRoozbeh Pournader <roozbeh@google.com>2017-06-01 16:45:33 -0700
commit538de5bb3b3662c51c17cd9dd24e660c85e88b78 (patch)
tree7ad94ecd9e84f072e55b2356d6227c38dafe05e0 /core/java/android/text/TextLine.java
parent1cdf223a0e968ead15d37cfb83f9c1ea88a54d6c (diff)
Make underlining spans independent of actual class
Previously, we had singled out UnderlineSpan for special treatment for proper kerning at span boundaries. That had the limitation that any other span that used underlining, or even subclasses of UnderlineSpan were not getting the fix. With this CL, we actually look at all the effects of applying a span's updateDrawState() and if the changes were only in underlining, we merge the pieces and call handleText() on the merged chunk of text. Change-Id: I829162c758192b80594f164d1105699224b627ba Bug: 32907446 Test: cts-tradefed run cts-dev --module CtsTextTestCases Test: cts-tradefed run cts-dev --module CtsWidgetTestCases Test: manual
Diffstat (limited to 'core/java/android/text/TextLine.java')
-rw-r--r--core/java/android/text/TextLine.java215
1 files changed, 172 insertions, 43 deletions
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index 756e9a0e2f2a..97e281894395 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -16,6 +16,8 @@
package android.text;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
@@ -28,6 +30,8 @@ import android.util.Log;
import com.android.internal.util.ArrayUtils;
+import java.util.ArrayList;
+
/**
* Represents a line of styled text, for measuring in visual order and
* for rendering.
@@ -58,7 +62,9 @@ class TextLine {
// Additional width of whitespace for justification. This value is per whitespace, thus
// the line width will increase by mAddedWidth x (number of stretchable whitespaces).
private float mAddedWidth;
+
private final TextPaint mWorkPaint = new TextPaint();
+ private final TextPaint mActivePaint = new TextPaint();
private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
@@ -66,6 +72,9 @@ class TextLine {
private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
new SpanSet<ReplacementSpan>(ReplacementSpan.class);
+ private final UnderlineInfo mUnderlineInfo = new UnderlineInfo();
+ private final ArrayList<UnderlineInfo> mUnderlines = new ArrayList();
+
private static final TextLine[] sCached = new TextLine[3];
/**
@@ -695,6 +704,37 @@ class TextLine {
fmi.leading = Math.max(fmi.leading, previousLeading);
}
+ private static void drawUnderline(TextPaint wp, Canvas c, int color, float thickness,
+ float xleft, float xright, int baseline) {
+ // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
+ final float underlineTop = baseline + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
+
+ final int previousColor = wp.getColor();
+ final Paint.Style previousStyle = wp.getStyle();
+ final boolean previousAntiAlias = wp.isAntiAlias();
+
+ wp.setStyle(Paint.Style.FILL);
+ wp.setAntiAlias(true);
+
+ wp.setColor(color);
+ c.drawRect(xleft, underlineTop, xright, underlineTop + thickness, wp);
+
+ wp.setStyle(previousStyle);
+ wp.setColor(previousColor);
+ wp.setAntiAlias(previousAntiAlias);
+ }
+
+ private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
+ boolean runIsRtl, int offset) {
+ if (mCharsValid) {
+ return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
+ } else {
+ final int delta = mStart;
+ return wp.getRunAdvance(mText, delta + start, delta + end,
+ delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
+ }
+ }
+
/**
* Utility function for measuring and rendering text. The text must
* not include a tab.
@@ -711,13 +751,15 @@ class TextLine {
* @param fmi receives metrics information, can be null
* @param needWidth true if the width of the run is needed
* @param offset the offset for the purpose of measuring
+ * @param underlines the list of locations and paremeters for drawing underlines
* @return the signed width of the run based on the run direction; only
* valid if needWidth is true
*/
private float handleText(TextPaint wp, int start, int end,
int contextStart, int contextEnd, boolean runIsRtl,
Canvas c, float x, int top, int y, int bottom,
- FontMetricsInt fmi, boolean needWidth, int offset) {
+ FontMetricsInt fmi, boolean needWidth, int offset,
+ @Nullable ArrayList<UnderlineInfo> underlines) {
wp.setWordSpacing(mAddedWidth);
// Get metrics first (even for empty strings or "0" width runs)
@@ -725,28 +767,26 @@ class TextLine {
expandMetricsFromPaint(fmi, wp);
}
- int runLen = end - start;
// No need to do anything if the run width is "0"
- if (runLen == 0) {
+ if (end == start) {
return 0f;
}
- float ret = 0;
+ float totalWidth = 0;
- if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {
- if (mCharsValid) {
- ret = wp.getRunAdvance(mChars, start, end, contextStart, contextEnd,
- runIsRtl, offset);
- } else {
- int delta = mStart;
- ret = wp.getRunAdvance(mText, delta + start, delta + end,
- delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
- }
+ final int numUnderlines = underlines == null ? 0 : underlines.size();
+ if (needWidth || (c != null && (wp.bgColor != 0 || numUnderlines != 0 || runIsRtl))) {
+ totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
}
if (c != null) {
+ final float leftX, rightX;
if (runIsRtl) {
- x -= ret;
+ leftX = x - totalWidth;
+ rightX = x;
+ } else {
+ leftX = x;
+ rightX = x + totalWidth;
}
if (wp.bgColor != 0) {
@@ -755,36 +795,52 @@ class TextLine {
wp.setColor(wp.bgColor);
wp.setStyle(Paint.Style.FILL);
- c.drawRect(x, top, x + ret, bottom, wp);
+ c.drawRect(leftX, top, rightX, bottom, wp);
wp.setStyle(previousStyle);
wp.setColor(previousColor);
}
- if (wp.underlineColor != 0) {
- // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
- float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
-
- int previousColor = wp.getColor();
- Paint.Style previousStyle = wp.getStyle();
- boolean previousAntiAlias = wp.isAntiAlias();
-
- wp.setStyle(Paint.Style.FILL);
- wp.setAntiAlias(true);
-
- wp.setColor(wp.underlineColor);
- c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);
+ if (numUnderlines != 0) {
+ // kStdUnderline_Thickness = 1/18, defined in SkTextFormatParams.h
+ final float defaultThickness = (1.0f / 18.0f) * wp.getTextSize();
+ for (int i = 0; i < numUnderlines; i++) {
+ final UnderlineInfo info = underlines.get(i);
+
+ final int underlineStart = Math.max(info.start, start);
+ final int underlineEnd = Math.min(info.end, offset);
+ float underlineStartAdvance = getRunAdvance(
+ wp, start, end, contextStart, contextEnd, runIsRtl, underlineStart);
+ float underlineEndAdvance = getRunAdvance(
+ wp, start, end, contextStart, contextEnd, runIsRtl, underlineEnd);
+ final float underlineXLeft, underlineXRight;
+ if (runIsRtl) {
+ underlineXLeft = rightX - underlineEndAdvance;
+ underlineXRight = rightX - underlineStartAdvance;
+ } else {
+ underlineXLeft = leftX + underlineStartAdvance;
+ underlineXRight = leftX + underlineEndAdvance;
+ }
- wp.setStyle(previousStyle);
- wp.setColor(previousColor);
- wp.setAntiAlias(previousAntiAlias);
+ // Theoretically, there could be cases where both Paint's and TextPaint's
+ // setUnderLineText() are called. For backward compatibility, we need to draw
+ // both underlines, the one with custom color first.
+ if (info.underlineColor != 0) {
+ drawUnderline(wp, c, wp.underlineColor, wp.underlineThickness,
+ underlineXLeft, underlineXRight, y);
+ }
+ if (info.isUnderlineText) {
+ drawUnderline(wp, c, wp.getColor(), defaultThickness,
+ underlineXLeft, underlineXRight, y);
+ }
+ }
}
drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
- x, y + wp.baselineShift);
+ leftX, y + wp.baselineShift);
}
- return runIsRtl ? -ret : ret;
+ return runIsRtl ? -totalWidth : totalWidth;
}
/**
@@ -864,6 +920,37 @@ class TextLine {
return result;
}
+ private static final class UnderlineInfo {
+ public boolean isUnderlineText;
+ public int underlineColor;
+ public float underlineThickness;
+ public int start = -1;
+ public int end = -1;
+
+ public boolean hasUnderline() {
+ return isUnderlineText || underlineColor != 0;
+ }
+
+ // Copies the info, but not the start and end range.
+ public UnderlineInfo copyInfo() {
+ final UnderlineInfo copy = new UnderlineInfo();
+ copy.isUnderlineText = isUnderlineText;
+ copy.underlineColor = underlineColor;
+ copy.underlineThickness = underlineThickness;
+ return copy;
+ }
+ }
+
+ private void extractUnderlineInfo(@NonNull TextPaint paint, @NonNull UnderlineInfo info) {
+ info.isUnderlineText = paint.isUnderlineText();
+ if (info.isUnderlineText) {
+ paint.setUnderlineText(false);
+ }
+ info.underlineColor = paint.underlineColor;
+ info.underlineThickness = paint.underlineThickness;
+ paint.setUnderlineText(0, 0.0f);
+ }
+
/**
* Utility function for handling a unidirectional run. The run must not
* contain tabs but can contain styles.
@@ -894,7 +981,7 @@ class TextLine {
// Case of an empty line, make sure we update fmi according to mPaint
if (start == measureLimit) {
- TextPaint wp = mWorkPaint;
+ final TextPaint wp = mWorkPaint;
wp.set(mPaint);
if (fmi != null) {
expandMetricsFromPaint(fmi, wp);
@@ -903,11 +990,11 @@ class TextLine {
}
if (mSpanned == null) {
- TextPaint wp = mWorkPaint;
+ final TextPaint wp = mWorkPaint;
wp.set(mPaint);
wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
- y, bottom, fmi, needWidth, measureLimit);
+ y, bottom, fmi, needWidth, measureLimit, null);
}
mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
@@ -920,7 +1007,7 @@ class TextLine {
// for the run bounds.
final float originalX = x;
for (int i = start, inext; i < measureLimit; i = inext) {
- TextPaint wp = mWorkPaint;
+ final TextPaint wp = mWorkPaint;
wp.set(mPaint);
inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
@@ -934,7 +1021,7 @@ class TextLine {
// empty by construction. This special case in getSpans() explains the >= & <= tests
if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
(mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
- MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
+ final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
if (span instanceof ReplacementSpan) {
replacement = (ReplacementSpan)span;
} else {
@@ -950,26 +1037,68 @@ class TextLine {
continue;
}
+ final TextPaint activePaint = mActivePaint;
+ activePaint.set(mPaint);
+ int activeStart = i;
+ int activeEnd = mlimit;
+ final UnderlineInfo underlineInfo = mUnderlineInfo;
+ mUnderlines.clear();
for (int j = i, jnext; j < mlimit; j = jnext) {
jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
mStart;
- int offset = Math.min(jnext, mlimit);
+ final int offset = Math.min(jnext, mlimit);
wp.set(mPaint);
for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
// Intentionally using >= and <= as explained above
if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
(mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
- CharacterStyle span = mCharacterStyleSpanSet.spans[k];
+ final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
span.updateDrawState(wp);
}
- wp.setHyphenEdit(adjustHyphenEdit(j, jnext, wp.getHyphenEdit()));
+ extractUnderlineInfo(wp, underlineInfo);
+
+ if (j == i) {
+ // First chunk of text. We can't handle it yet, since we may need to merge it
+ // with the next chunk. So we just save the TextPaint for future comparisons
+ // and use.
+ activePaint.set(wp);
+ } else if (!wp.hasEqualAttributes(activePaint)) {
+ // The style of the present chunk of text is substantially different from the
+ // style of the previous chunk. We need to handle the active piece of text
+ // and restart with the present chunk.
+ activePaint.setHyphenEdit(adjustHyphenEdit(
+ activeStart, activeEnd, mPaint.getHyphenEdit()));
+ x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
+ top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
+ Math.min(activeEnd, mlimit), mUnderlines);
+
+ activeStart = j;
+ activePaint.set(wp);
+ mUnderlines.clear();
+ } else {
+ // The present TextPaint is substantially equal to the last TextPaint except
+ // perhaps for underlines. We just need to expand the active piece of text to
+ // include the present chunk, which we always do anyway. We don't need to save
+ // wp to activePaint, since they are already equal.
+ }
- x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
- top, y, bottom, fmi, needWidth || jnext < measureLimit, offset);
+ activeEnd = jnext;
+ if (underlineInfo.hasUnderline()) {
+ final UnderlineInfo copy = underlineInfo.copyInfo();
+ copy.start = j;
+ copy.end = jnext;
+ mUnderlines.add(copy);
+ }
}
+ // Handle the final piece of text.
+ activePaint.setHyphenEdit(adjustHyphenEdit(
+ activeStart, activeEnd, mPaint.getHyphenEdit()));
+ x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
+ top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
+ Math.min(activeEnd, mlimit), mUnderlines);
}
return x - originalX;