2 * Copyright (C) 2010 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.graphics.Canvas;
22 import android.graphics.Paint;
23 import android.graphics.Paint.FontMetricsInt;
24 import android.text.Layout.Directions;
25 import android.text.Layout.TabStops;
26 import android.text.style.CharacterStyle;
27 import android.text.style.MetricAffectingSpan;
28 import android.text.style.ReplacementSpan;
29 import android.util.Log;
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.internal.util.ArrayUtils;
34 import java.util.ArrayList;
37 * Represents a line of styled text, for measuring in visual order and
40 * <p>Get a new instance using obtain(), and when finished with it, return it
41 * to the pool using recycle().
43 * <p>Call set to prepare the instance for use, then either draw, measure,
44 * metrics, or caretToLeftRightOf.
48 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
49 public class TextLine {
50 private static final boolean DEBUG = false;
52 private TextPaint mPaint;
53 private CharSequence mText;
57 private Directions mDirections;
58 private boolean mHasTabs;
59 private TabStops mTabs;
60 private char[] mChars;
61 private boolean mCharsValid;
62 private Spanned mSpanned;
63 private PrecomputedText mComputed;
65 // Additional width of whitespace for justification. This value is per whitespace, thus
66 // the line width will increase by mAddedWidth x (number of stretchable whitespaces).
67 private float mAddedWidth;
69 private final TextPaint mWorkPaint = new TextPaint();
70 private final TextPaint mActivePaint = new TextPaint();
71 private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
72 new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
73 private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
74 new SpanSet<CharacterStyle>(CharacterStyle.class);
75 private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
76 new SpanSet<ReplacementSpan>(ReplacementSpan.class);
78 private final DecorationInfo mDecorationInfo = new DecorationInfo();
79 private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>();
81 private static final TextLine[] sCached = new TextLine[3];
84 * Returns a new TextLine from the shared pool.
86 * @return an uninitialized TextLine
88 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
89 public static TextLine obtain() {
91 synchronized (sCached) {
92 for (int i = sCached.length; --i >= 0;) {
93 if (sCached[i] != null) {
102 Log.v("TLINE", "new: " + tl);
108 * Puts a TextLine back into the shared pool. Do not use this TextLine once
109 * it has been returned.
110 * @param tl the textLine
111 * @return null, as a convenience from clearing references to the provided
114 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
115 public static TextLine recycle(TextLine tl) {
118 tl.mDirections = null;
124 tl.mMetricAffectingSpanSpanSet.recycle();
125 tl.mCharacterStyleSpanSet.recycle();
126 tl.mReplacementSpanSpanSet.recycle();
128 synchronized(sCached) {
129 for (int i = 0; i < sCached.length; ++i) {
130 if (sCached[i] == null) {
140 * Initializes a TextLine and prepares it for use.
142 * @param paint the base paint for the line
143 * @param text the text, can be Styled
144 * @param start the start of the line relative to the text
145 * @param limit the limit of the line relative to the text
146 * @param dir the paragraph direction of this line
147 * @param directions the directions information of this line
148 * @param hasTabs true if the line might contain tabs
149 * @param tabStops the tabStops. Can be null.
151 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
152 public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
153 Directions directions, boolean hasTabs, TabStops tabStops) {
157 mLen = limit - start;
159 mDirections = directions;
160 if (mDirections == null) {
161 throw new IllegalArgumentException("Directions cannot be null");
166 boolean hasReplacement = false;
167 if (text instanceof Spanned) {
168 mSpanned = (Spanned) text;
169 mReplacementSpanSpanSet.init(mSpanned, start, limit);
170 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
174 if (text instanceof PrecomputedText) {
175 // Here, no need to check line break strategy or hyphenation frequency since there is no
176 // line break concept here.
177 mComputed = (PrecomputedText) text;
178 if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
183 mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
186 if (mChars == null || mChars.length < mLen) {
187 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
189 TextUtils.getChars(text, start, limit, mChars, 0);
190 if (hasReplacement) {
191 // Handle these all at once so we don't have to do it as we go.
192 // Replace the first character of each replacement run with the
193 // object-replacement character and the remainder with zero width
194 // non-break space aka BOM. Cursor movement code skips these
195 // zero-width characters.
196 char[] chars = mChars;
197 for (int i = start, inext; i < limit; i = inext) {
198 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
199 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
200 // transition into a span
201 chars[i - start] = '\ufffc';
202 for (int j = i - start + 1, e = inext - start; j < e; ++j) {
203 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
214 * Justify the line to the given width.
216 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
217 public void justify(float justifyWidth) {
219 while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
222 final int spaces = countStretchableSpaces(0, end);
224 // There are no stretchable spaces, so we can't help the justification by adding any
228 final float width = Math.abs(measure(end, false, null));
229 mAddedWidth = (justifyWidth - width) / spaces;
233 * Renders the TextLine.
235 * @param c the canvas to render on
236 * @param x the leading margin position
237 * @param top the top of the line
238 * @param y the baseline
239 * @param bottom the bottom of the line
241 void draw(Canvas c, float x, int top, int y, int bottom) {
243 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
244 drawRun(c, 0, mLen, false, x, top, y, bottom, false);
247 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
248 drawRun(c, 0, mLen, true, x, top, y, bottom, false);
254 int[] runs = mDirections.mDirections;
256 int lastRunIndex = runs.length - 2;
257 for (int i = 0; i < runs.length; i += 2) {
258 int runStart = runs[i];
259 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
260 if (runLimit > mLen) {
263 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
265 int segstart = runStart;
266 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
268 if (mHasTabs && j < runLimit) {
270 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
271 codept = Character.codePointAt(mChars, j);
272 if (codept > 0xFFFF) {
279 if (j == runLimit || codept == '\t') {
280 h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
281 i != lastRunIndex || j != mLen);
283 if (codept == '\t') {
284 h = mDir * nextTab(h * mDir);
293 * Returns metrics information for the entire line.
295 * @param fmi receives font metrics information, can be null
296 * @return the signed width of the line
298 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
299 public float metrics(FontMetricsInt fmi) {
300 return measure(mLen, false, fmi);
304 * Returns information about a position on the line.
306 * @param offset the line-relative character offset, between 0 and the
307 * line length, inclusive
308 * @param trailing true to measure the trailing edge of the character
309 * before offset, false to measure the leading edge of the character
311 * @param fmi receives metrics information about the requested
312 * character, can be null.
313 * @return the signed offset from the leading margin to the requested
316 float measure(int offset, boolean trailing, FontMetricsInt fmi) {
317 int target = trailing ? offset - 1 : offset;
325 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
326 return measureRun(0, offset, mLen, false, fmi);
328 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
329 return measureRun(0, offset, mLen, true, fmi);
333 char[] chars = mChars;
334 int[] runs = mDirections.mDirections;
335 for (int i = 0; i < runs.length; i += 2) {
336 int runStart = runs[i];
337 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
338 if (runLimit > mLen) {
341 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
343 int segstart = runStart;
344 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
346 if (mHasTabs && j < runLimit) {
348 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
349 codept = Character.codePointAt(chars, j);
350 if (codept > 0xFFFF) {
357 if (j == runLimit || codept == '\t') {
358 boolean inSegment = target >= segstart && target < j;
360 boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
361 if (inSegment && advance) {
362 return h + measureRun(segstart, offset, j, runIsRtl, fmi);
365 float w = measureRun(segstart, j, j, runIsRtl, fmi);
366 h += advance ? w : -w;
369 return h + measureRun(segstart, offset, j, runIsRtl, null);
372 if (codept == '\t') {
376 h = mDir * nextTab(h * mDir);
391 * Draws a unidirectional (but possibly multi-styled) run of text.
394 * @param c the canvas to draw on
395 * @param start the line-relative start
396 * @param limit the line-relative limit
397 * @param runIsRtl true if the run is right-to-left
398 * @param x the position of the run that is closest to the leading margin
399 * @param top the top of the line
400 * @param y the baseline
401 * @param bottom the bottom of the line
402 * @param needWidth true if the width value is required.
403 * @return the signed width of the run, based on the paragraph direction.
404 * Only valid if needWidth is true.
406 private float drawRun(Canvas c, int start,
407 int limit, boolean runIsRtl, float x, int top, int y, int bottom,
410 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
411 float w = -measureRun(start, limit, limit, runIsRtl, null);
412 handleRun(start, limit, limit, runIsRtl, c, x + w, top,
413 y, bottom, null, false);
417 return handleRun(start, limit, limit, runIsRtl, c, x, top,
418 y, bottom, null, needWidth);
422 * Measures a unidirectional (but possibly multi-styled) run of text.
425 * @param start the line-relative start of the run
426 * @param offset the offset to measure to, between start and limit inclusive
427 * @param limit the line-relative limit of the run
428 * @param runIsRtl true if the run is right-to-left
429 * @param fmi receives metrics information about the requested
431 * @return the signed width from the start of the run to the leading edge
432 * of the character at offset, based on the run (not paragraph) direction
434 private float measureRun(int start, int offset, int limit, boolean runIsRtl,
435 FontMetricsInt fmi) {
436 return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
440 * Walk the cursor through this line, skipping conjuncts and
441 * zero-width characters.
443 * <p>This function cannot properly walk the cursor off the ends of the line
444 * since it does not know about any shaping on the previous/following line
445 * that might affect the cursor position. Callers must either avoid these
446 * situations or handle the result specially.
448 * @param cursor the starting position of the cursor, between 0 and the
449 * length of the line, inclusive
450 * @param toLeft true if the caret is moving to the left.
451 * @return the new offset. If it is less than 0 or greater than the length
452 * of the line, the previous/following line should be examined to get the
455 int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
456 // 1) The caret marks the leading edge of a character. The character
457 // logically before it might be on a different level, and the active caret
458 // position is on the character at the lower level. If that character
459 // was the previous character, the caret is on its trailing edge.
460 // 2) Take this character/edge and move it in the indicated direction.
461 // This gives you a new character and a new edge.
462 // 3) This position is between two visually adjacent characters. One of
463 // these might be at a lower level. The active position is on the
464 // character at the lower level.
465 // 4) If the active position is on the trailing edge of the character,
466 // the new caret position is the following logical character, else it
471 boolean paraIsRtl = mDir == -1;
472 int[] runs = mDirections.mDirections;
474 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
475 boolean trailing = false;
477 if (cursor == lineStart) {
479 } else if (cursor == lineEnd) {
480 runIndex = runs.length;
482 // First, get information about the run containing the character with
484 for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
485 runStart = lineStart + runs[runIndex];
486 if (cursor >= runStart) {
487 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
488 if (runLimit > lineEnd) {
491 if (cursor < runLimit) {
492 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
493 Layout.RUN_LEVEL_MASK;
494 if (cursor == runStart) {
495 // The caret is on a run boundary, see if we should
496 // use the position on the trailing edge of the previous
497 // logical character instead.
498 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
499 int pos = cursor - 1;
500 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
501 prevRunStart = lineStart + runs[prevRunIndex];
502 if (pos >= prevRunStart) {
503 prevRunLimit = prevRunStart +
504 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
505 if (prevRunLimit > lineEnd) {
506 prevRunLimit = lineEnd;
508 if (pos < prevRunLimit) {
509 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
510 & Layout.RUN_LEVEL_MASK;
511 if (prevRunLevel < runLevel) {
512 // Start from logically previous character.
513 runIndex = prevRunIndex;
514 runLevel = prevRunLevel;
515 runStart = prevRunStart;
516 runLimit = prevRunLimit;
529 // caret might be == lineEnd. This is generally a space or paragraph
530 // separator and has an associated run, but might be the end of
531 // text, in which case it doesn't. If that happens, we ran off the
532 // end of the run list, and runIndex == runs.length. In this case,
533 // we are at a run boundary so we skip the below test.
534 if (runIndex != runs.length) {
535 boolean runIsRtl = (runLevel & 0x1) != 0;
536 boolean advance = toLeft == runIsRtl;
537 if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
538 // Moving within or into the run, so we can move logically.
539 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
540 runIsRtl, cursor, advance);
541 // If the new position is internal to the run, we're at the strong
542 // position already so we're finished.
543 if (newCaret != (advance ? runLimit : runStart)) {
550 // If newCaret is -1, we're starting at a run boundary and crossing
551 // into another run. Otherwise we've arrived at a run boundary, and
552 // need to figure out which character to attach to. Note we might
553 // need to run this twice, if we cross a run boundary and end up at
554 // another run boundary.
556 boolean advance = toLeft == paraIsRtl;
557 int otherRunIndex = runIndex + (advance ? 2 : -2);
558 if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
559 int otherRunStart = lineStart + runs[otherRunIndex];
560 int otherRunLimit = otherRunStart +
561 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
562 if (otherRunLimit > lineEnd) {
563 otherRunLimit = lineEnd;
565 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
566 Layout.RUN_LEVEL_MASK;
567 boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
569 advance = toLeft == otherRunIsRtl;
570 if (newCaret == -1) {
571 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
572 otherRunLimit, otherRunIsRtl,
573 advance ? otherRunStart : otherRunLimit, advance);
574 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
575 // Crossed and ended up at a new boundary,
576 // repeat a second and final time.
577 runIndex = otherRunIndex;
578 runLevel = otherRunLevel;
584 // The new caret is at a boundary.
585 if (otherRunLevel < runLevel) {
586 // The strong character is in the other run.
587 newCaret = advance ? otherRunStart : otherRunLimit;
592 if (newCaret == -1) {
593 // We're walking off the end of the line. The paragraph
594 // level is always equal to or lower than any internal level, so
595 // the boundaries get the strong caret.
596 newCaret = advance ? mLen + 1 : -1;
600 // Else we've arrived at the end of the line. That's a strong position.
601 // We might have arrived here by crossing over a run with no internal
602 // breaks and dropping out of the above loop before advancing one final
603 // time, so reset the caret.
604 // Note, we use '<=' below to handle a situation where the only run
605 // on the line is a counter-directional run. If we're not advancing,
606 // we can end up at the 'lineEnd' position but the caret we want is at
608 if (newCaret <= lineEnd) {
609 newCaret = advance ? lineEnd : lineStart;
618 * Returns the next valid offset within this directional run, skipping
619 * conjuncts and zero-width characters. This should not be called to walk
620 * off the end of the line, since the returned values might not be valid
621 * on neighboring lines. If the returned offset is less than zero or
622 * greater than the line length, the offset should be recomputed on the
623 * preceding or following line, respectively.
625 * @param runIndex the run index
626 * @param runStart the start of the run
627 * @param runLimit the limit of the run
628 * @param runIsRtl true if the run is right-to-left
629 * @param offset the offset
630 * @param after true if the new offset should logically follow the provided
632 * @return the new offset
634 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
635 boolean runIsRtl, int offset, boolean after) {
637 if (runIndex < 0 || offset == (after ? mLen : 0)) {
638 // Walking off end of line. Since we don't know
639 // what cursor positions are available on other lines, we can't
640 // return accurate values. These are a guess.
642 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
644 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
647 TextPaint wp = mWorkPaint;
649 wp.setWordSpacing(mAddedWidth);
651 int spanStart = runStart;
653 if (mSpanned == null) {
654 spanLimit = runLimit;
656 int target = after ? offset + 1 : offset;
657 int limit = mStart + runLimit;
659 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
660 MetricAffectingSpan.class) - mStart;
661 if (spanLimit >= target) {
664 spanStart = spanLimit;
667 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
668 mStart + spanLimit, MetricAffectingSpan.class);
669 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
671 if (spans.length > 0) {
672 ReplacementSpan replacement = null;
673 for (int j = 0; j < spans.length; j++) {
674 MetricAffectingSpan span = spans[j];
675 if (span instanceof ReplacementSpan) {
676 replacement = (ReplacementSpan)span;
678 span.updateMeasureState(wp);
682 if (replacement != null) {
683 // If we have a replacement span, we're moving either to
684 // the start or end of this span.
685 return after ? spanLimit : spanStart;
690 int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
691 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
693 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
694 dir, offset, cursorOpt);
696 return wp.getTextRunCursor(mText, mStart + spanStart,
697 mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart;
704 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
705 final int previousTop = fmi.top;
706 final int previousAscent = fmi.ascent;
707 final int previousDescent = fmi.descent;
708 final int previousBottom = fmi.bottom;
709 final int previousLeading = fmi.leading;
711 wp.getFontMetricsInt(fmi);
713 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
717 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
718 int previousDescent, int previousBottom, int previousLeading) {
719 fmi.top = Math.min(fmi.top, previousTop);
720 fmi.ascent = Math.min(fmi.ascent, previousAscent);
721 fmi.descent = Math.max(fmi.descent, previousDescent);
722 fmi.bottom = Math.max(fmi.bottom, previousBottom);
723 fmi.leading = Math.max(fmi.leading, previousLeading);
726 private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
727 float thickness, float xleft, float xright, float baseline) {
728 final float strokeTop = baseline + wp.baselineShift + position;
730 final int previousColor = wp.getColor();
731 final Paint.Style previousStyle = wp.getStyle();
732 final boolean previousAntiAlias = wp.isAntiAlias();
734 wp.setStyle(Paint.Style.FILL);
735 wp.setAntiAlias(true);
738 c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
740 wp.setStyle(previousStyle);
741 wp.setColor(previousColor);
742 wp.setAntiAlias(previousAntiAlias);
745 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
746 boolean runIsRtl, int offset) {
748 return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
750 final int delta = mStart;
751 if (mComputed == null) {
752 // TODO: Enable measured getRunAdvance for ReplacementSpan and RTL text.
753 return wp.getRunAdvance(mText, delta + start, delta + end,
754 delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
756 return mComputed.getWidth(start + delta, end + delta);
762 * Utility function for measuring and rendering text. The text must
765 * @param wp the working paint
766 * @param start the start of the text
767 * @param end the end of the text
768 * @param runIsRtl true if the run is right-to-left
769 * @param c the canvas, can be null if rendering is not needed
770 * @param x the edge of the run closest to the leading margin
771 * @param top the top of the line
772 * @param y the baseline
773 * @param bottom the bottom of the line
774 * @param fmi receives metrics information, can be null
775 * @param needWidth true if the width of the run is needed
776 * @param offset the offset for the purpose of measuring
777 * @param decorations the list of locations and paremeters for drawing decorations
778 * @return the signed width of the run based on the run direction; only
779 * valid if needWidth is true
781 private float handleText(TextPaint wp, int start, int end,
782 int contextStart, int contextEnd, boolean runIsRtl,
783 Canvas c, float x, int top, int y, int bottom,
784 FontMetricsInt fmi, boolean needWidth, int offset,
785 @Nullable ArrayList<DecorationInfo> decorations) {
787 wp.setWordSpacing(mAddedWidth);
788 // Get metrics first (even for empty strings or "0" width runs)
790 expandMetricsFromPaint(fmi, wp);
793 // No need to do anything if the run width is "0"
798 float totalWidth = 0;
800 final int numDecorations = decorations == null ? 0 : decorations.size();
801 if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
802 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
806 final float leftX, rightX;
808 leftX = x - totalWidth;
812 rightX = x + totalWidth;
815 if (wp.bgColor != 0) {
816 int previousColor = wp.getColor();
817 Paint.Style previousStyle = wp.getStyle();
819 wp.setColor(wp.bgColor);
820 wp.setStyle(Paint.Style.FILL);
821 c.drawRect(leftX, top, rightX, bottom, wp);
823 wp.setStyle(previousStyle);
824 wp.setColor(previousColor);
827 if (numDecorations != 0) {
828 for (int i = 0; i < numDecorations; i++) {
829 final DecorationInfo info = decorations.get(i);
831 final int decorationStart = Math.max(info.start, start);
832 final int decorationEnd = Math.min(info.end, offset);
833 float decorationStartAdvance = getRunAdvance(
834 wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
835 float decorationEndAdvance = getRunAdvance(
836 wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
837 final float decorationXLeft, decorationXRight;
839 decorationXLeft = rightX - decorationEndAdvance;
840 decorationXRight = rightX - decorationStartAdvance;
842 decorationXLeft = leftX + decorationStartAdvance;
843 decorationXRight = leftX + decorationEndAdvance;
846 // Theoretically, there could be cases where both Paint's and TextPaint's
847 // setUnderLineText() are called. For backward compatibility, we need to draw
848 // both underlines, the one with custom color first.
849 if (info.underlineColor != 0) {
850 drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
851 info.underlineThickness, decorationXLeft, decorationXRight, y);
853 if (info.isUnderlineText) {
854 final float thickness =
855 Math.max(wp.getUnderlineThickness(), 1.0f);
856 drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
857 decorationXLeft, decorationXRight, y);
860 if (info.isStrikeThruText) {
861 final float thickness =
862 Math.max(wp.getStrikeThruThickness(), 1.0f);
863 drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
864 decorationXLeft, decorationXRight, y);
869 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
870 leftX, y + wp.baselineShift);
873 return runIsRtl ? -totalWidth : totalWidth;
877 * Utility function for measuring and rendering a replacement.
880 * @param replacement the replacement
881 * @param wp the work paint
882 * @param start the start of the run
883 * @param limit the limit of the run
884 * @param runIsRtl true if the run is right-to-left
885 * @param c the canvas, can be null if not rendering
886 * @param x the edge of the replacement closest to the leading margin
887 * @param top the top of the line
888 * @param y the baseline
889 * @param bottom the bottom of the line
890 * @param fmi receives metrics information, can be null
891 * @param needWidth true if the width of the replacement is needed
892 * @return the signed width of the run based on the run direction; only
893 * valid if needWidth is true
895 private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
896 int start, int limit, boolean runIsRtl, Canvas c,
897 float x, int top, int y, int bottom, FontMetricsInt fmi,
902 int textStart = mStart + start;
903 int textLimit = mStart + limit;
905 if (needWidth || (c != null && runIsRtl)) {
907 int previousAscent = 0;
908 int previousDescent = 0;
909 int previousBottom = 0;
910 int previousLeading = 0;
912 boolean needUpdateMetrics = (fmi != null);
914 if (needUpdateMetrics) {
915 previousTop = fmi.top;
916 previousAscent = fmi.ascent;
917 previousDescent = fmi.descent;
918 previousBottom = fmi.bottom;
919 previousLeading = fmi.leading;
922 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
924 if (needUpdateMetrics) {
925 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
934 replacement.draw(c, mText, textStart, textLimit,
935 x, top, y, bottom, wp);
938 return runIsRtl ? -ret : ret;
941 private int adjustHyphenEdit(int start, int limit, int hyphenEdit) {
942 int result = hyphenEdit;
943 // Only draw hyphens on first or last run in line. Disable them otherwise.
944 if (start > 0) { // not the first run
945 result &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
947 if (limit < mLen) { // not the last run
948 result &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
953 private static final class DecorationInfo {
954 public boolean isStrikeThruText;
955 public boolean isUnderlineText;
956 public int underlineColor;
957 public float underlineThickness;
958 public int start = -1;
961 public boolean hasDecoration() {
962 return isStrikeThruText || isUnderlineText || underlineColor != 0;
965 // Copies the info, but not the start and end range.
966 public DecorationInfo copyInfo() {
967 final DecorationInfo copy = new DecorationInfo();
968 copy.isStrikeThruText = isStrikeThruText;
969 copy.isUnderlineText = isUnderlineText;
970 copy.underlineColor = underlineColor;
971 copy.underlineThickness = underlineThickness;
976 private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
977 info.isStrikeThruText = paint.isStrikeThruText();
978 if (info.isStrikeThruText) {
979 paint.setStrikeThruText(false);
981 info.isUnderlineText = paint.isUnderlineText();
982 if (info.isUnderlineText) {
983 paint.setUnderlineText(false);
985 info.underlineColor = paint.underlineColor;
986 info.underlineThickness = paint.underlineThickness;
987 paint.setUnderlineText(0, 0.0f);
991 * Utility function for handling a unidirectional run. The run must not
992 * contain tabs but can contain styles.
995 * @param start the line-relative start of the run
996 * @param measureLimit the offset to measure to, between start and limit inclusive
997 * @param limit the limit of the run
998 * @param runIsRtl true if the run is right-to-left
999 * @param c the canvas, can be null
1000 * @param x the end of the run closest to the leading margin
1001 * @param top the top of the line
1002 * @param y the baseline
1003 * @param bottom the bottom of the line
1004 * @param fmi receives metrics information, can be null
1005 * @param needWidth true if the width is required
1006 * @return the signed width of the run based on the run direction; only
1007 * valid if needWidth is true
1009 private float handleRun(int start, int measureLimit,
1010 int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
1011 int bottom, FontMetricsInt fmi, boolean needWidth) {
1013 if (measureLimit < start || measureLimit > limit) {
1014 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
1015 + "start (" + start + ") and limit (" + limit + ") bounds");
1018 // Case of an empty line, make sure we update fmi according to mPaint
1019 if (start == measureLimit) {
1020 final TextPaint wp = mWorkPaint;
1023 expandMetricsFromPaint(fmi, wp);
1028 final boolean needsSpanMeasurement;
1029 if (mSpanned == null) {
1030 needsSpanMeasurement = false;
1032 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
1033 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
1034 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
1035 || mCharacterStyleSpanSet.numberOfSpans != 0;
1038 if (!needsSpanMeasurement) {
1039 final TextPaint wp = mWorkPaint;
1041 wp.setHyphenEdit(adjustHyphenEdit(start, limit, wp.getHyphenEdit()));
1042 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
1043 y, bottom, fmi, needWidth, measureLimit, null);
1046 // Shaping needs to take into account context up to metric boundaries,
1047 // but rendering needs to take into account character style boundaries.
1048 // So we iterate through metric runs to get metric bounds,
1049 // then within each metric run iterate through character style runs
1050 // for the run bounds.
1051 final float originalX = x;
1052 for (int i = start, inext; i < measureLimit; i = inext) {
1053 final TextPaint wp = mWorkPaint;
1056 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1058 int mlimit = Math.min(inext, measureLimit);
1060 ReplacementSpan replacement = null;
1062 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
1063 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1064 // empty by construction. This special case in getSpans() explains the >= & <= tests
1065 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
1066 (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
1067 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
1068 if (span instanceof ReplacementSpan) {
1069 replacement = (ReplacementSpan)span;
1071 // We might have a replacement that uses the draw
1072 // state, otherwise measure state would suffice.
1073 span.updateDrawState(wp);
1077 if (replacement != null) {
1078 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1079 bottom, fmi, needWidth || mlimit < measureLimit);
1083 final TextPaint activePaint = mActivePaint;
1084 activePaint.set(mPaint);
1085 int activeStart = i;
1086 int activeEnd = mlimit;
1087 final DecorationInfo decorationInfo = mDecorationInfo;
1088 mDecorations.clear();
1089 for (int j = i, jnext; j < mlimit; j = jnext) {
1090 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1093 final int offset = Math.min(jnext, mlimit);
1095 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1096 // Intentionally using >= and <= as explained above
1097 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1098 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
1100 final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
1101 span.updateDrawState(wp);
1104 extractDecorationInfo(wp, decorationInfo);
1107 // First chunk of text. We can't handle it yet, since we may need to merge it
1108 // with the next chunk. So we just save the TextPaint for future comparisons
1110 activePaint.set(wp);
1111 } else if (!wp.hasEqualAttributes(activePaint)) {
1112 // The style of the present chunk of text is substantially different from the
1113 // style of the previous chunk. We need to handle the active piece of text
1114 // and restart with the present chunk.
1115 activePaint.setHyphenEdit(adjustHyphenEdit(
1116 activeStart, activeEnd, mPaint.getHyphenEdit()));
1117 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1118 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
1119 Math.min(activeEnd, mlimit), mDecorations);
1122 activePaint.set(wp);
1123 mDecorations.clear();
1125 // The present TextPaint is substantially equal to the last TextPaint except
1126 // perhaps for decorations. We just need to expand the active piece of text to
1127 // include the present chunk, which we always do anyway. We don't need to save
1128 // wp to activePaint, since they are already equal.
1132 if (decorationInfo.hasDecoration()) {
1133 final DecorationInfo copy = decorationInfo.copyInfo();
1136 mDecorations.add(copy);
1139 // Handle the final piece of text.
1140 activePaint.setHyphenEdit(adjustHyphenEdit(
1141 activeStart, activeEnd, mPaint.getHyphenEdit()));
1142 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1143 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
1144 Math.min(activeEnd, mlimit), mDecorations);
1147 return x - originalX;
1151 * Render a text run with the set-up paint.
1153 * @param c the canvas
1154 * @param wp the paint used to render the text
1155 * @param start the start of the run
1156 * @param end the end of the run
1157 * @param contextStart the start of context for the run
1158 * @param contextEnd the end of the context for the run
1159 * @param runIsRtl true if the run is right-to-left
1160 * @param x the x position of the left edge of the run
1161 * @param y the baseline of the run
1163 private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
1164 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
1167 int count = end - start;
1168 int contextCount = contextEnd - contextStart;
1169 c.drawTextRun(mChars, start, count, contextStart, contextCount,
1170 x, y, runIsRtl, wp);
1173 c.drawTextRun(mText, delta + start, delta + end,
1174 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
1179 * Returns the next tab position.
1181 * @param h the (unsigned) offset from the leading margin
1182 * @return the (unsigned) tab position after this offset
1184 float nextTab(float h) {
1185 if (mTabs != null) {
1186 return mTabs.nextTab(h);
1188 return TabStops.nextDefaultStop(h, TAB_INCREMENT);
1191 private boolean isStretchableWhitespace(int ch) {
1192 // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
1193 return ch == 0x0020;
1196 /* Return the number of spaces in the text line, for the purpose of justification */
1197 private int countStretchableSpaces(int start, int end) {
1199 for (int i = start; i < end; i++) {
1200 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1201 if (isStretchableWhitespace(c)) {
1208 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1209 public static boolean isLineEndSpace(char ch) {
1210 return ch == ' ' || ch == '\t' || ch == 0x1680
1211 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1212 || ch == 0x205F || ch == 0x3000;
1215 private static final int TAB_INCREMENT = 20;