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.graphics.Bitmap;
20 import android.graphics.Canvas;
21 import android.graphics.Paint;
22 import android.graphics.Paint.FontMetricsInt;
23 import android.graphics.RectF;
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.util.ArrayUtils;
34 * Represents a line of styled text, for measuring in visual order and
37 * <p>Get a new instance using obtain(), and when finished with it, return it
38 * to the pool using recycle().
40 * <p>Call set to prepare the instance for use, then either draw, measure,
41 * metrics, or caretToLeftRightOf.
46 private static final boolean DEBUG = false;
48 private TextPaint mPaint;
49 private CharSequence mText;
53 private Directions mDirections;
54 private boolean mHasTabs;
55 private TabStops mTabs;
56 private char[] mChars;
57 private boolean mCharsValid;
58 private Spanned mSpanned;
59 private final TextPaint mWorkPaint = new TextPaint();
60 private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
61 new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
62 private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
63 new SpanSet<CharacterStyle>(CharacterStyle.class);
64 private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
65 new SpanSet<ReplacementSpan>(ReplacementSpan.class);
67 private static final TextLine[] sCached = new TextLine[3];
70 * Returns a new TextLine from the shared pool.
72 * @return an uninitialized TextLine
74 static TextLine obtain() {
76 synchronized (sCached) {
77 for (int i = sCached.length; --i >= 0;) {
78 if (sCached[i] != null) {
87 Log.v("TLINE", "new: " + tl);
93 * Puts a TextLine back into the shared pool. Do not use this TextLine once
94 * it has been returned.
95 * @param tl the textLine
96 * @return null, as a convenience from clearing references to the provided
99 static TextLine recycle(TextLine tl) {
102 tl.mDirections = null;
107 tl.mMetricAffectingSpanSpanSet.recycle();
108 tl.mCharacterStyleSpanSet.recycle();
109 tl.mReplacementSpanSpanSet.recycle();
111 synchronized(sCached) {
112 for (int i = 0; i < sCached.length; ++i) {
113 if (sCached[i] == null) {
123 * Initializes a TextLine and prepares it for use.
125 * @param paint the base paint for the line
126 * @param text the text, can be Styled
127 * @param start the start of the line relative to the text
128 * @param limit the limit of the line relative to the text
129 * @param dir the paragraph direction of this line
130 * @param directions the directions information of this line
131 * @param hasTabs true if the line might contain tabs
132 * @param tabStops the tabStops. Can be null.
134 void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
135 Directions directions, boolean hasTabs, TabStops tabStops) {
139 mLen = limit - start;
141 mDirections = directions;
142 if (mDirections == null) {
143 throw new IllegalArgumentException("Directions cannot be null");
148 boolean hasReplacement = false;
149 if (text instanceof Spanned) {
150 mSpanned = (Spanned) text;
151 mReplacementSpanSpanSet.init(mSpanned, start, limit);
152 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
155 mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
158 if (mChars == null || mChars.length < mLen) {
159 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
161 TextUtils.getChars(text, start, limit, mChars, 0);
162 if (hasReplacement) {
163 // Handle these all at once so we don't have to do it as we go.
164 // Replace the first character of each replacement run with the
165 // object-replacement character and the remainder with zero width
166 // non-break space aka BOM. Cursor movement code skips these
167 // zero-width characters.
168 char[] chars = mChars;
169 for (int i = start, inext; i < limit; i = inext) {
170 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
171 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
172 // transition into a span
173 chars[i - start] = '\ufffc';
174 for (int j = i - start + 1, e = inext - start; j < e; ++j) {
175 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
185 * Renders the TextLine.
187 * @param c the canvas to render on
188 * @param x the leading margin position
189 * @param top the top of the line
190 * @param y the baseline
191 * @param bottom the bottom of the line
193 void draw(Canvas c, float x, int top, int y, int bottom) {
195 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
196 drawRun(c, 0, mLen, false, x, top, y, bottom, false);
199 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
200 drawRun(c, 0, mLen, true, x, top, y, bottom, false);
206 int[] runs = mDirections.mDirections;
208 int lastRunIndex = runs.length - 2;
209 for (int i = 0; i < runs.length; i += 2) {
210 int runStart = runs[i];
211 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
212 if (runLimit > mLen) {
215 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
217 int segstart = runStart;
218 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
220 if (mHasTabs && j < runLimit) {
222 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
223 codept = Character.codePointAt(mChars, j);
224 if (codept > 0xFFFF) {
231 if (j == runLimit || codept == '\t') {
232 h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
233 i != lastRunIndex || j != mLen);
235 if (codept == '\t') {
236 h = mDir * nextTab(h * mDir);
245 * Returns metrics information for the entire line.
247 * @param fmi receives font metrics information, can be null
248 * @return the signed width of the line
250 float metrics(FontMetricsInt fmi) {
251 return measure(mLen, false, fmi);
255 * Returns information about a position on the line.
257 * @param offset the line-relative character offset, between 0 and the
258 * line length, inclusive
259 * @param trailing true to measure the trailing edge of the character
260 * before offset, false to measure the leading edge of the character
262 * @param fmi receives metrics information about the requested
263 * character, can be null.
264 * @return the signed offset from the leading margin to the requested
267 float measure(int offset, boolean trailing, FontMetricsInt fmi) {
268 int target = trailing ? offset - 1 : offset;
276 if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
277 return measureRun(0, offset, mLen, false, fmi);
279 if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
280 return measureRun(0, offset, mLen, true, fmi);
284 char[] chars = mChars;
285 int[] runs = mDirections.mDirections;
286 for (int i = 0; i < runs.length; i += 2) {
287 int runStart = runs[i];
288 int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
289 if (runLimit > mLen) {
292 boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
294 int segstart = runStart;
295 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
297 if (mHasTabs && j < runLimit) {
299 if (codept >= 0xD800 && codept < 0xDC00 && j + 1 < runLimit) {
300 codept = Character.codePointAt(chars, j);
301 if (codept > 0xFFFF) {
308 if (j == runLimit || codept == '\t') {
309 boolean inSegment = target >= segstart && target < j;
311 boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
312 if (inSegment && advance) {
313 return h += measureRun(segstart, offset, j, runIsRtl, fmi);
316 float w = measureRun(segstart, j, j, runIsRtl, fmi);
317 h += advance ? w : -w;
320 return h += measureRun(segstart, offset, j, runIsRtl, null);
323 if (codept == '\t') {
327 h = mDir * nextTab(h * mDir);
342 * Draws a unidirectional (but possibly multi-styled) run of text.
345 * @param c the canvas to draw on
346 * @param start the line-relative start
347 * @param limit the line-relative limit
348 * @param runIsRtl true if the run is right-to-left
349 * @param x the position of the run that is closest to the leading margin
350 * @param top the top of the line
351 * @param y the baseline
352 * @param bottom the bottom of the line
353 * @param needWidth true if the width value is required.
354 * @return the signed width of the run, based on the paragraph direction.
355 * Only valid if needWidth is true.
357 private float drawRun(Canvas c, int start,
358 int limit, boolean runIsRtl, float x, int top, int y, int bottom,
361 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
362 float w = -measureRun(start, limit, limit, runIsRtl, null);
363 handleRun(start, limit, limit, runIsRtl, c, x + w, top,
364 y, bottom, null, false);
368 return handleRun(start, limit, limit, runIsRtl, c, x, top,
369 y, bottom, null, needWidth);
373 * Measures a unidirectional (but possibly multi-styled) run of text.
376 * @param start the line-relative start of the run
377 * @param offset the offset to measure to, between start and limit inclusive
378 * @param limit the line-relative limit of the run
379 * @param runIsRtl true if the run is right-to-left
380 * @param fmi receives metrics information about the requested
382 * @return the signed width from the start of the run to the leading edge
383 * of the character at offset, based on the run (not paragraph) direction
385 private float measureRun(int start, int offset, int limit, boolean runIsRtl,
386 FontMetricsInt fmi) {
387 return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
391 * Walk the cursor through this line, skipping conjuncts and
392 * zero-width characters.
394 * <p>This function cannot properly walk the cursor off the ends of the line
395 * since it does not know about any shaping on the previous/following line
396 * that might affect the cursor position. Callers must either avoid these
397 * situations or handle the result specially.
399 * @param cursor the starting position of the cursor, between 0 and the
400 * length of the line, inclusive
401 * @param toLeft true if the caret is moving to the left.
402 * @return the new offset. If it is less than 0 or greater than the length
403 * of the line, the previous/following line should be examined to get the
406 int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
407 // 1) The caret marks the leading edge of a character. The character
408 // logically before it might be on a different level, and the active caret
409 // position is on the character at the lower level. If that character
410 // was the previous character, the caret is on its trailing edge.
411 // 2) Take this character/edge and move it in the indicated direction.
412 // This gives you a new character and a new edge.
413 // 3) This position is between two visually adjacent characters. One of
414 // these might be at a lower level. The active position is on the
415 // character at the lower level.
416 // 4) If the active position is on the trailing edge of the character,
417 // the new caret position is the following logical character, else it
422 boolean paraIsRtl = mDir == -1;
423 int[] runs = mDirections.mDirections;
425 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
426 boolean trailing = false;
428 if (cursor == lineStart) {
430 } else if (cursor == lineEnd) {
431 runIndex = runs.length;
433 // First, get information about the run containing the character with
435 for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
436 runStart = lineStart + runs[runIndex];
437 if (cursor >= runStart) {
438 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
439 if (runLimit > lineEnd) {
442 if (cursor < runLimit) {
443 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
444 Layout.RUN_LEVEL_MASK;
445 if (cursor == runStart) {
446 // The caret is on a run boundary, see if we should
447 // use the position on the trailing edge of the previous
448 // logical character instead.
449 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
450 int pos = cursor - 1;
451 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
452 prevRunStart = lineStart + runs[prevRunIndex];
453 if (pos >= prevRunStart) {
454 prevRunLimit = prevRunStart +
455 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
456 if (prevRunLimit > lineEnd) {
457 prevRunLimit = lineEnd;
459 if (pos < prevRunLimit) {
460 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
461 & Layout.RUN_LEVEL_MASK;
462 if (prevRunLevel < runLevel) {
463 // Start from logically previous character.
464 runIndex = prevRunIndex;
465 runLevel = prevRunLevel;
466 runStart = prevRunStart;
467 runLimit = prevRunLimit;
480 // caret might be == lineEnd. This is generally a space or paragraph
481 // separator and has an associated run, but might be the end of
482 // text, in which case it doesn't. If that happens, we ran off the
483 // end of the run list, and runIndex == runs.length. In this case,
484 // we are at a run boundary so we skip the below test.
485 if (runIndex != runs.length) {
486 boolean runIsRtl = (runLevel & 0x1) != 0;
487 boolean advance = toLeft == runIsRtl;
488 if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
489 // Moving within or into the run, so we can move logically.
490 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
491 runIsRtl, cursor, advance);
492 // If the new position is internal to the run, we're at the strong
493 // position already so we're finished.
494 if (newCaret != (advance ? runLimit : runStart)) {
501 // If newCaret is -1, we're starting at a run boundary and crossing
502 // into another run. Otherwise we've arrived at a run boundary, and
503 // need to figure out which character to attach to. Note we might
504 // need to run this twice, if we cross a run boundary and end up at
505 // another run boundary.
507 boolean advance = toLeft == paraIsRtl;
508 int otherRunIndex = runIndex + (advance ? 2 : -2);
509 if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
510 int otherRunStart = lineStart + runs[otherRunIndex];
511 int otherRunLimit = otherRunStart +
512 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
513 if (otherRunLimit > lineEnd) {
514 otherRunLimit = lineEnd;
516 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
517 Layout.RUN_LEVEL_MASK;
518 boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
520 advance = toLeft == otherRunIsRtl;
521 if (newCaret == -1) {
522 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
523 otherRunLimit, otherRunIsRtl,
524 advance ? otherRunStart : otherRunLimit, advance);
525 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
526 // Crossed and ended up at a new boundary,
527 // repeat a second and final time.
528 runIndex = otherRunIndex;
529 runLevel = otherRunLevel;
535 // The new caret is at a boundary.
536 if (otherRunLevel < runLevel) {
537 // The strong character is in the other run.
538 newCaret = advance ? otherRunStart : otherRunLimit;
543 if (newCaret == -1) {
544 // We're walking off the end of the line. The paragraph
545 // level is always equal to or lower than any internal level, so
546 // the boundaries get the strong caret.
547 newCaret = advance ? mLen + 1 : -1;
551 // Else we've arrived at the end of the line. That's a strong position.
552 // We might have arrived here by crossing over a run with no internal
553 // breaks and dropping out of the above loop before advancing one final
554 // time, so reset the caret.
555 // Note, we use '<=' below to handle a situation where the only run
556 // on the line is a counter-directional run. If we're not advancing,
557 // we can end up at the 'lineEnd' position but the caret we want is at
559 if (newCaret <= lineEnd) {
560 newCaret = advance ? lineEnd : lineStart;
569 * Returns the next valid offset within this directional run, skipping
570 * conjuncts and zero-width characters. This should not be called to walk
571 * off the end of the line, since the returned values might not be valid
572 * on neighboring lines. If the returned offset is less than zero or
573 * greater than the line length, the offset should be recomputed on the
574 * preceding or following line, respectively.
576 * @param runIndex the run index
577 * @param runStart the start of the run
578 * @param runLimit the limit of the run
579 * @param runIsRtl true if the run is right-to-left
580 * @param offset the offset
581 * @param after true if the new offset should logically follow the provided
583 * @return the new offset
585 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
586 boolean runIsRtl, int offset, boolean after) {
588 if (runIndex < 0 || offset == (after ? mLen : 0)) {
589 // Walking off end of line. Since we don't know
590 // what cursor positions are available on other lines, we can't
591 // return accurate values. These are a guess.
593 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
595 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
598 TextPaint wp = mWorkPaint;
601 int spanStart = runStart;
603 if (mSpanned == null) {
604 spanLimit = runLimit;
606 int target = after ? offset + 1 : offset;
607 int limit = mStart + runLimit;
609 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
610 MetricAffectingSpan.class) - mStart;
611 if (spanLimit >= target) {
614 spanStart = spanLimit;
617 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
618 mStart + spanLimit, MetricAffectingSpan.class);
619 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
621 if (spans.length > 0) {
622 ReplacementSpan replacement = null;
623 for (int j = 0; j < spans.length; j++) {
624 MetricAffectingSpan span = spans[j];
625 if (span instanceof ReplacementSpan) {
626 replacement = (ReplacementSpan)span;
628 span.updateMeasureState(wp);
632 if (replacement != null) {
633 // If we have a replacement span, we're moving either to
634 // the start or end of this span.
635 return after ? spanLimit : spanStart;
640 int dir = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
641 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
643 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
644 dir, offset, cursorOpt);
646 return wp.getTextRunCursor(mText, mStart + spanStart,
647 mStart + spanLimit, dir, mStart + offset, cursorOpt) - mStart;
654 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
655 final int previousTop = fmi.top;
656 final int previousAscent = fmi.ascent;
657 final int previousDescent = fmi.descent;
658 final int previousBottom = fmi.bottom;
659 final int previousLeading = fmi.leading;
661 wp.getFontMetricsInt(fmi);
663 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
667 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
668 int previousDescent, int previousBottom, int previousLeading) {
669 fmi.top = Math.min(fmi.top, previousTop);
670 fmi.ascent = Math.min(fmi.ascent, previousAscent);
671 fmi.descent = Math.max(fmi.descent, previousDescent);
672 fmi.bottom = Math.max(fmi.bottom, previousBottom);
673 fmi.leading = Math.max(fmi.leading, previousLeading);
677 * Utility function for measuring and rendering text. The text must
680 * @param wp the working paint
681 * @param start the start of the text
682 * @param end the end of the text
683 * @param runIsRtl true if the run is right-to-left
684 * @param c the canvas, can be null if rendering is not needed
685 * @param x the edge of the run closest to the leading margin
686 * @param top the top of the line
687 * @param y the baseline
688 * @param bottom the bottom of the line
689 * @param fmi receives metrics information, can be null
690 * @param needWidth true if the width of the run is needed
691 * @param offset the offset for the purpose of measuring
692 * @return the signed width of the run based on the run direction; only
693 * valid if needWidth is true
695 private float handleText(TextPaint wp, int start, int end,
696 int contextStart, int contextEnd, boolean runIsRtl,
697 Canvas c, float x, int top, int y, int bottom,
698 FontMetricsInt fmi, boolean needWidth, int offset) {
700 // Get metrics first (even for empty strings or "0" width runs)
702 expandMetricsFromPaint(fmi, wp);
705 int runLen = end - start;
706 // No need to do anything if the run width is "0"
713 if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {
715 ret = wp.getRunAdvance(mChars, start, end, contextStart, contextEnd,
719 ret = wp.getRunAdvance(mText, delta + start, delta + end,
720 delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
729 if (wp.bgColor != 0) {
730 int previousColor = wp.getColor();
731 Paint.Style previousStyle = wp.getStyle();
733 wp.setColor(wp.bgColor);
734 wp.setStyle(Paint.Style.FILL);
735 c.drawRect(x, top, x + ret, bottom, wp);
737 wp.setStyle(previousStyle);
738 wp.setColor(previousColor);
741 if (wp.underlineColor != 0) {
742 // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
743 float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
745 int previousColor = wp.getColor();
746 Paint.Style previousStyle = wp.getStyle();
747 boolean previousAntiAlias = wp.isAntiAlias();
749 wp.setStyle(Paint.Style.FILL);
750 wp.setAntiAlias(true);
752 wp.setColor(wp.underlineColor);
753 c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);
755 wp.setStyle(previousStyle);
756 wp.setColor(previousColor);
757 wp.setAntiAlias(previousAntiAlias);
760 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
761 x, y + wp.baselineShift);
764 return runIsRtl ? -ret : ret;
768 * Utility function for measuring and rendering a replacement.
771 * @param replacement the replacement
772 * @param wp the work paint
773 * @param start the start of the run
774 * @param limit the limit of the run
775 * @param runIsRtl true if the run is right-to-left
776 * @param c the canvas, can be null if not rendering
777 * @param x the edge of the replacement closest to the leading margin
778 * @param top the top of the line
779 * @param y the baseline
780 * @param bottom the bottom of the line
781 * @param fmi receives metrics information, can be null
782 * @param needWidth true if the width of the replacement is needed
783 * @return the signed width of the run based on the run direction; only
784 * valid if needWidth is true
786 private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
787 int start, int limit, boolean runIsRtl, Canvas c,
788 float x, int top, int y, int bottom, FontMetricsInt fmi,
793 int textStart = mStart + start;
794 int textLimit = mStart + limit;
796 if (needWidth || (c != null && runIsRtl)) {
798 int previousAscent = 0;
799 int previousDescent = 0;
800 int previousBottom = 0;
801 int previousLeading = 0;
803 boolean needUpdateMetrics = (fmi != null);
805 if (needUpdateMetrics) {
806 previousTop = fmi.top;
807 previousAscent = fmi.ascent;
808 previousDescent = fmi.descent;
809 previousBottom = fmi.bottom;
810 previousLeading = fmi.leading;
813 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
815 if (needUpdateMetrics) {
816 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
825 replacement.draw(c, mText, textStart, textLimit,
826 x, top, y, bottom, wp);
829 return runIsRtl ? -ret : ret;
833 * Utility function for handling a unidirectional run. The run must not
834 * contain tabs but can contain styles.
837 * @param start the line-relative start of the run
838 * @param measureLimit the offset to measure to, between start and limit inclusive
839 * @param limit the limit of the run
840 * @param runIsRtl true if the run is right-to-left
841 * @param c the canvas, can be null
842 * @param x the end of the run closest to the leading margin
843 * @param top the top of the line
844 * @param y the baseline
845 * @param bottom the bottom of the line
846 * @param fmi receives metrics information, can be null
847 * @param needWidth true if the width is required
848 * @return the signed width of the run based on the run direction; only
849 * valid if needWidth is true
851 private float handleRun(int start, int measureLimit,
852 int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
853 int bottom, FontMetricsInt fmi, boolean needWidth) {
855 // Case of an empty line, make sure we update fmi according to mPaint
856 if (start == measureLimit) {
857 TextPaint wp = mWorkPaint;
860 expandMetricsFromPaint(fmi, wp);
865 if (mSpanned == null) {
866 TextPaint wp = mWorkPaint;
868 final int mlimit = measureLimit;
869 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
870 y, bottom, fmi, needWidth || mlimit < measureLimit, mlimit);
873 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
874 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
876 // Shaping needs to take into account context up to metric boundaries,
877 // but rendering needs to take into account character style boundaries.
878 // So we iterate through metric runs to get metric bounds,
879 // then within each metric run iterate through character style runs
880 // for the run bounds.
881 final float originalX = x;
882 for (int i = start, inext; i < measureLimit; i = inext) {
883 TextPaint wp = mWorkPaint;
886 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
888 int mlimit = Math.min(inext, measureLimit);
890 ReplacementSpan replacement = null;
892 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
893 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
894 // empty by construction. This special case in getSpans() explains the >= & <= tests
895 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
896 (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
897 MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
898 if (span instanceof ReplacementSpan) {
899 replacement = (ReplacementSpan)span;
901 // We might have a replacement that uses the draw
902 // state, otherwise measure state would suffice.
903 span.updateDrawState(wp);
907 if (replacement != null) {
908 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
909 bottom, fmi, needWidth || mlimit < measureLimit);
913 for (int j = i, jnext; j < mlimit; j = jnext) {
914 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
916 int offset = Math.min(jnext, mlimit);
919 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
920 // Intentionally using >= and <= as explained above
921 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
922 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
924 CharacterStyle span = mCharacterStyleSpanSet.spans[k];
925 span.updateDrawState(wp);
928 // Only draw hyphen on last run in line
932 x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
933 top, y, bottom, fmi, needWidth || jnext < measureLimit, offset);
937 return x - originalX;
941 * Render a text run with the set-up paint.
943 * @param c the canvas
944 * @param wp the paint used to render the text
945 * @param start the start of the run
946 * @param end the end of the run
947 * @param contextStart the start of context for the run
948 * @param contextEnd the end of the context for the run
949 * @param runIsRtl true if the run is right-to-left
950 * @param x the x position of the left edge of the run
951 * @param y the baseline of the run
953 private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
954 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
957 int count = end - start;
958 int contextCount = contextEnd - contextStart;
959 c.drawTextRun(mChars, start, count, contextStart, contextCount,
963 c.drawTextRun(mText, delta + start, delta + end,
964 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
969 * Returns the next tab position.
971 * @param h the (unsigned) offset from the leading margin
972 * @return the (unsigned) tab position after this offset
974 float nextTab(float h) {
976 return mTabs.nextTab(h);
978 return TabStops.nextDefaultStop(h, TAB_INCREMENT);
981 private static final int TAB_INCREMENT = 20;