diff options
| author | Roozbeh Pournader <roozbeh@google.com> | 2017-05-24 09:07:52 -0700 |
|---|---|---|
| committer | Roozbeh Pournader <roozbeh@google.com> | 2017-06-01 16:45:33 -0700 |
| commit | 538de5bb3b3662c51c17cd9dd24e660c85e88b78 (patch) | |
| tree | 7ad94ecd9e84f072e55b2356d6227c38dafe05e0 /core/java/android/text/TextLine.java | |
| parent | 1cdf223a0e968ead15d37cfb83f9c1ea88a54d6c (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.java | 215 |
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; |
