blob: 86651060a394686d3e1aa9d408adf8fc237f6e79 [file] [log] [blame]
Rahul Ravikumar05336002019-10-14 15:04:32 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17package android.text;
18
19import android.annotation.IntRange;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.UnsupportedAppUsage;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Paint.FontMetricsInt;
26import android.os.Build;
27import android.text.Layout.Directions;
28import android.text.Layout.TabStops;
29import android.text.style.CharacterStyle;
30import android.text.style.MetricAffectingSpan;
31import android.text.style.ReplacementSpan;
32import android.util.Log;
33
34import com.android.internal.annotations.VisibleForTesting;
35import com.android.internal.util.ArrayUtils;
36
37import java.util.ArrayList;
38
39/**
40 * Represents a line of styled text, for measuring in visual order and
41 * for rendering.
42 *
43 * <p>Get a new instance using obtain(), and when finished with it, return it
44 * to the pool using recycle().
45 *
46 * <p>Call set to prepare the instance for use, then either draw, measure,
47 * metrics, or caretToLeftRightOf.
48 *
49 * @hide
50 */
51@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
52public class TextLine {
53 private static final boolean DEBUG = false;
54
55 private static final char TAB_CHAR = '\t';
56
57 private TextPaint mPaint;
58 @UnsupportedAppUsage
59 private CharSequence mText;
60 private int mStart;
61 private int mLen;
62 private int mDir;
63 private Directions mDirections;
64 private boolean mHasTabs;
65 private TabStops mTabs;
66 private char[] mChars;
67 private boolean mCharsValid;
68 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
69 private Spanned mSpanned;
70 private PrecomputedText mComputed;
71
72 // The start and end of a potentially existing ellipsis on this text line.
73 // We use them to filter out replacement and metric affecting spans on ellipsized away chars.
74 private int mEllipsisStart;
75 private int mEllipsisEnd;
76
77 // Additional width of whitespace for justification. This value is per whitespace, thus
78 // the line width will increase by mAddedWidthForJustify x (number of stretchable whitespaces).
79 private float mAddedWidthForJustify;
80 private boolean mIsJustifying;
81
82 private final TextPaint mWorkPaint = new TextPaint();
83 private final TextPaint mActivePaint = new TextPaint();
84 @UnsupportedAppUsage
85 private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
86 new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
87 @UnsupportedAppUsage
88 private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
89 new SpanSet<CharacterStyle>(CharacterStyle.class);
90 @UnsupportedAppUsage
91 private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
92 new SpanSet<ReplacementSpan>(ReplacementSpan.class);
93
94 private final DecorationInfo mDecorationInfo = new DecorationInfo();
95 private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>();
96
97 /** Not allowed to access. If it's for memory leak workaround, it was already fixed M. */
98 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
99 private static final TextLine[] sCached = new TextLine[3];
100
101 /**
102 * Returns a new TextLine from the shared pool.
103 *
104 * @return an uninitialized TextLine
105 */
106 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
107 @UnsupportedAppUsage
108 public static TextLine obtain() {
109 TextLine tl;
110 synchronized (sCached) {
111 for (int i = sCached.length; --i >= 0;) {
112 if (sCached[i] != null) {
113 tl = sCached[i];
114 sCached[i] = null;
115 return tl;
116 }
117 }
118 }
119 tl = new TextLine();
120 if (DEBUG) {
121 Log.v("TLINE", "new: " + tl);
122 }
123 return tl;
124 }
125
126 /**
127 * Puts a TextLine back into the shared pool. Do not use this TextLine once
128 * it has been returned.
129 * @param tl the textLine
130 * @return null, as a convenience from clearing references to the provided
131 * TextLine
132 */
133 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
134 public static TextLine recycle(TextLine tl) {
135 tl.mText = null;
136 tl.mPaint = null;
137 tl.mDirections = null;
138 tl.mSpanned = null;
139 tl.mTabs = null;
140 tl.mChars = null;
141 tl.mComputed = null;
142
143 tl.mMetricAffectingSpanSpanSet.recycle();
144 tl.mCharacterStyleSpanSet.recycle();
145 tl.mReplacementSpanSpanSet.recycle();
146
147 synchronized(sCached) {
148 for (int i = 0; i < sCached.length; ++i) {
149 if (sCached[i] == null) {
150 sCached[i] = tl;
151 break;
152 }
153 }
154 }
155 return null;
156 }
157
158 /**
159 * Initializes a TextLine and prepares it for use.
160 *
161 * @param paint the base paint for the line
162 * @param text the text, can be Styled
163 * @param start the start of the line relative to the text
164 * @param limit the limit of the line relative to the text
165 * @param dir the paragraph direction of this line
166 * @param directions the directions information of this line
167 * @param hasTabs true if the line might contain tabs
168 * @param tabStops the tabStops. Can be null
169 * @param ellipsisStart the start of the ellipsis relative to the line
170 * @param ellipsisEnd the end of the ellipsis relative to the line. When there
171 * is no ellipsis, this should be equal to ellipsisStart.
172 */
173 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
174 public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
175 Directions directions, boolean hasTabs, TabStops tabStops,
176 int ellipsisStart, int ellipsisEnd) {
177 mPaint = paint;
178 mText = text;
179 mStart = start;
180 mLen = limit - start;
181 mDir = dir;
182 mDirections = directions;
183 if (mDirections == null) {
184 throw new IllegalArgumentException("Directions cannot be null");
185 }
186 mHasTabs = hasTabs;
187 mSpanned = null;
188
189 boolean hasReplacement = false;
190 if (text instanceof Spanned) {
191 mSpanned = (Spanned) text;
192 mReplacementSpanSpanSet.init(mSpanned, start, limit);
193 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
194 }
195
196 mComputed = null;
197 if (text instanceof PrecomputedText) {
198 // Here, no need to check line break strategy or hyphenation frequency since there is no
199 // line break concept here.
200 mComputed = (PrecomputedText) text;
201 if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
202 mComputed = null;
203 }
204 }
205
206 mCharsValid = hasReplacement;
207
208 if (mCharsValid) {
209 if (mChars == null || mChars.length < mLen) {
210 mChars = ArrayUtils.newUnpaddedCharArray(mLen);
211 }
212 TextUtils.getChars(text, start, limit, mChars, 0);
213 if (hasReplacement) {
214 // Handle these all at once so we don't have to do it as we go.
215 // Replace the first character of each replacement run with the
216 // object-replacement character and the remainder with zero width
217 // non-break space aka BOM. Cursor movement code skips these
218 // zero-width characters.
219 char[] chars = mChars;
220 for (int i = start, inext; i < limit; i = inext) {
221 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
222 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)
223 && (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) {
224 // transition into a span
225 chars[i - start] = '\ufffc';
226 for (int j = i - start + 1, e = inext - start; j < e; ++j) {
227 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
228 }
229 }
230 }
231 }
232 }
233 mTabs = tabStops;
234 mAddedWidthForJustify = 0;
235 mIsJustifying = false;
236
237 mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0;
238 mEllipsisEnd = ellipsisStart != ellipsisEnd ? ellipsisEnd : 0;
239 }
240
241 private char charAt(int i) {
242 return mCharsValid ? mChars[i] : mText.charAt(i + mStart);
243 }
244
245 /**
246 * Justify the line to the given width.
247 */
248 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
249 public void justify(float justifyWidth) {
250 int end = mLen;
251 while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
252 end--;
253 }
254 final int spaces = countStretchableSpaces(0, end);
255 if (spaces == 0) {
256 // There are no stretchable spaces, so we can't help the justification by adding any
257 // width.
258 return;
259 }
260 final float width = Math.abs(measure(end, false, null));
261 mAddedWidthForJustify = (justifyWidth - width) / spaces;
262 mIsJustifying = true;
263 }
264
265 /**
266 * Renders the TextLine.
267 *
268 * @param c the canvas to render on
269 * @param x the leading margin position
270 * @param top the top of the line
271 * @param y the baseline
272 * @param bottom the bottom of the line
273 */
274 void draw(Canvas c, float x, int top, int y, int bottom) {
275 float h = 0;
276 final int runCount = mDirections.getRunCount();
277 for (int runIndex = 0; runIndex < runCount; runIndex++) {
278 final int runStart = mDirections.getRunStart(runIndex);
279 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
280 final boolean runIsRtl = mDirections.isRunRtl(runIndex);
281
282 int segStart = runStart;
283 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
284 if (j == runLimit || charAt(j) == TAB_CHAR) {
285 h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom,
286 runIndex != (runCount - 1) || j != mLen);
287
288 if (j != runLimit) { // charAt(j) == TAB_CHAR
289 h = mDir * nextTab(h * mDir);
290 }
291 segStart = j + 1;
292 }
293 }
294 }
295 }
296
297 /**
298 * Returns metrics information for the entire line.
299 *
300 * @param fmi receives font metrics information, can be null
301 * @return the signed width of the line
302 */
303 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
304 public float metrics(FontMetricsInt fmi) {
305 return measure(mLen, false, fmi);
306 }
307
308 /**
309 * Returns the signed graphical offset from the leading margin.
310 *
311 * Following examples are all for measuring offset=3. LX(e.g. L0, L1, ...) denotes a
312 * character which has LTR BiDi property. On the other hand, RX(e.g. R0, R1, ...) denotes a
313 * character which has RTL BiDi property. Assuming all character has 1em width.
314 *
315 * Example 1: All LTR chars within LTR context
316 * Input Text (logical) : L0 L1 L2 L3 L4 L5 L6 L7 L8
317 * Input Text (visual) : L0 L1 L2 L3 L4 L5 L6 L7 L8
318 * Output(trailing=true) : |--------| (Returns 3em)
319 * Output(trailing=false): |--------| (Returns 3em)
320 *
321 * Example 2: All RTL chars within RTL context.
322 * Input Text (logical) : R0 R1 R2 R3 R4 R5 R6 R7 R8
323 * Input Text (visual) : R8 R7 R6 R5 R4 R3 R2 R1 R0
324 * Output(trailing=true) : |--------| (Returns -3em)
325 * Output(trailing=false): |--------| (Returns -3em)
326 *
327 * Example 3: BiDi chars within LTR context.
328 * Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8
329 * Input Text (visual) : L0 L1 L2 R5 R4 R3 L6 L7 L8
330 * Output(trailing=true) : |-----------------| (Returns 6em)
331 * Output(trailing=false): |--------| (Returns 3em)
332 *
333 * Example 4: BiDi chars within RTL context.
334 * Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8
335 * Input Text (visual) : L6 L7 L8 R5 R4 R3 L0 L1 L2
336 * Output(trailing=true) : |-----------------| (Returns -6em)
337 * Output(trailing=false): |--------| (Returns -3em)
338 *
339 * @param offset the line-relative character offset, between 0 and the line length, inclusive
340 * @param trailing no effect if the offset is not on the BiDi transition offset. If the offset
341 * is on the BiDi transition offset and true is passed, the offset is regarded
342 * as the edge of the trailing run's edge. If false, the offset is regarded as
343 * the edge of the preceding run's edge. See example above.
344 * @param fmi receives metrics information about the requested character, can be null
345 * @return the signed graphical offset from the leading margin to the requested character edge.
346 * The positive value means the offset is right from the leading edge. The negative
347 * value means the offset is left from the leading edge.
348 */
349 public float measure(@IntRange(from = 0) int offset, boolean trailing,
350 @NonNull FontMetricsInt fmi) {
351 if (offset > mLen) {
352 throw new IndexOutOfBoundsException(
353 "offset(" + offset + ") should be less than line limit(" + mLen + ")");
354 }
355 final int target = trailing ? offset - 1 : offset;
356 if (target < 0) {
357 return 0;
358 }
359
360 float h = 0;
361 for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) {
362 final int runStart = mDirections.getRunStart(runIndex);
363 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
364 final boolean runIsRtl = mDirections.isRunRtl(runIndex);
365
366 int segStart = runStart;
367 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
368 if (j == runLimit || charAt(j) == TAB_CHAR) {
369 final boolean targetIsInThisSegment = target >= segStart && target < j;
370 final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
371
372 if (targetIsInThisSegment && sameDirection) {
373 return h + measureRun(segStart, offset, j, runIsRtl, fmi);
374 }
375
376 final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi);
377 h += sameDirection ? segmentWidth : -segmentWidth;
378
379 if (targetIsInThisSegment) {
380 return h + measureRun(segStart, offset, j, runIsRtl, null);
381 }
382
383 if (j != runLimit) { // charAt(j) == TAB_CHAR
384 if (offset == j) {
385 return h;
386 }
387 h = mDir * nextTab(h * mDir);
388 if (target == j) {
389 return h;
390 }
391 }
392
393 segStart = j + 1;
394 }
395 }
396 }
397
398 return h;
399 }
400
401 /**
402 * @see #measure(int, boolean, FontMetricsInt)
403 * @return The measure results for all possible offsets
404 */
405 @VisibleForTesting
406 public float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) {
407 float[] measurement = new float[mLen + 1];
408
409 int[] target = new int[mLen + 1];
410 for (int offset = 0; offset < target.length; ++offset) {
411 target[offset] = trailing[offset] ? offset - 1 : offset;
412 }
413 if (target[0] < 0) {
414 measurement[0] = 0;
415 }
416
417 float h = 0;
418 for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) {
419 final int runStart = mDirections.getRunStart(runIndex);
420 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
421 final boolean runIsRtl = mDirections.isRunRtl(runIndex);
422
423 int segStart = runStart;
424 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) {
425 if (j == runLimit || charAt(j) == TAB_CHAR) {
426 final float oldh = h;
427 final boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
428 final float w = measureRun(segStart, j, j, runIsRtl, fmi);
429 h += advance ? w : -w;
430
431 final float baseh = advance ? oldh : h;
432 FontMetricsInt crtfmi = advance ? fmi : null;
433 for (int offset = segStart; offset <= j && offset <= mLen; ++offset) {
434 if (target[offset] >= segStart && target[offset] < j) {
435 measurement[offset] =
436 baseh + measureRun(segStart, offset, j, runIsRtl, crtfmi);
437 }
438 }
439
440 if (j != runLimit) { // charAt(j) == TAB_CHAR
441 if (target[j] == j) {
442 measurement[j] = h;
443 }
444 h = mDir * nextTab(h * mDir);
445 if (target[j + 1] == j) {
446 measurement[j + 1] = h;
447 }
448 }
449
450 segStart = j + 1;
451 }
452 }
453 }
454 if (target[mLen] == mLen) {
455 measurement[mLen] = h;
456 }
457
458 return measurement;
459 }
460
461 /**
462 * Draws a unidirectional (but possibly multi-styled) run of text.
463 *
464 *
465 * @param c the canvas to draw on
466 * @param start the line-relative start
467 * @param limit the line-relative limit
468 * @param runIsRtl true if the run is right-to-left
469 * @param x the position of the run that is closest to the leading margin
470 * @param top the top of the line
471 * @param y the baseline
472 * @param bottom the bottom of the line
473 * @param needWidth true if the width value is required.
474 * @return the signed width of the run, based on the paragraph direction.
475 * Only valid if needWidth is true.
476 */
477 private float drawRun(Canvas c, int start,
478 int limit, boolean runIsRtl, float x, int top, int y, int bottom,
479 boolean needWidth) {
480
481 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
482 float w = -measureRun(start, limit, limit, runIsRtl, null);
483 handleRun(start, limit, limit, runIsRtl, c, x + w, top,
484 y, bottom, null, false);
485 return w;
486 }
487
488 return handleRun(start, limit, limit, runIsRtl, c, x, top,
489 y, bottom, null, needWidth);
490 }
491
492 /**
493 * Measures a unidirectional (but possibly multi-styled) run of text.
494 *
495 *
496 * @param start the line-relative start of the run
497 * @param offset the offset to measure to, between start and limit inclusive
498 * @param limit the line-relative limit of the run
499 * @param runIsRtl true if the run is right-to-left
500 * @param fmi receives metrics information about the requested
501 * run, can be null.
502 * @return the signed width from the start of the run to the leading edge
503 * of the character at offset, based on the run (not paragraph) direction
504 */
505 private float measureRun(int start, int offset, int limit, boolean runIsRtl,
506 FontMetricsInt fmi) {
507 return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
508 }
509
510 /**
511 * Walk the cursor through this line, skipping conjuncts and
512 * zero-width characters.
513 *
514 * <p>This function cannot properly walk the cursor off the ends of the line
515 * since it does not know about any shaping on the previous/following line
516 * that might affect the cursor position. Callers must either avoid these
517 * situations or handle the result specially.
518 *
519 * @param cursor the starting position of the cursor, between 0 and the
520 * length of the line, inclusive
521 * @param toLeft true if the caret is moving to the left.
522 * @return the new offset. If it is less than 0 or greater than the length
523 * of the line, the previous/following line should be examined to get the
524 * actual offset.
525 */
526 int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
527 // 1) The caret marks the leading edge of a character. The character
528 // logically before it might be on a different level, and the active caret
529 // position is on the character at the lower level. If that character
530 // was the previous character, the caret is on its trailing edge.
531 // 2) Take this character/edge and move it in the indicated direction.
532 // This gives you a new character and a new edge.
533 // 3) This position is between two visually adjacent characters. One of
534 // these might be at a lower level. The active position is on the
535 // character at the lower level.
536 // 4) If the active position is on the trailing edge of the character,
537 // the new caret position is the following logical character, else it
538 // is the character.
539
540 int lineStart = 0;
541 int lineEnd = mLen;
542 boolean paraIsRtl = mDir == -1;
543 int[] runs = mDirections.mDirections;
544
545 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
546 boolean trailing = false;
547
548 if (cursor == lineStart) {
549 runIndex = -2;
550 } else if (cursor == lineEnd) {
551 runIndex = runs.length;
552 } else {
553 // First, get information about the run containing the character with
554 // the active caret.
555 for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
556 runStart = lineStart + runs[runIndex];
557 if (cursor >= runStart) {
558 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
559 if (runLimit > lineEnd) {
560 runLimit = lineEnd;
561 }
562 if (cursor < runLimit) {
563 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
564 Layout.RUN_LEVEL_MASK;
565 if (cursor == runStart) {
566 // The caret is on a run boundary, see if we should
567 // use the position on the trailing edge of the previous
568 // logical character instead.
569 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
570 int pos = cursor - 1;
571 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
572 prevRunStart = lineStart + runs[prevRunIndex];
573 if (pos >= prevRunStart) {
574 prevRunLimit = prevRunStart +
575 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
576 if (prevRunLimit > lineEnd) {
577 prevRunLimit = lineEnd;
578 }
579 if (pos < prevRunLimit) {
580 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
581 & Layout.RUN_LEVEL_MASK;
582 if (prevRunLevel < runLevel) {
583 // Start from logically previous character.
584 runIndex = prevRunIndex;
585 runLevel = prevRunLevel;
586 runStart = prevRunStart;
587 runLimit = prevRunLimit;
588 trailing = true;
589 break;
590 }
591 }
592 }
593 }
594 }
595 break;
596 }
597 }
598 }
599
600 // caret might be == lineEnd. This is generally a space or paragraph
601 // separator and has an associated run, but might be the end of
602 // text, in which case it doesn't. If that happens, we ran off the
603 // end of the run list, and runIndex == runs.length. In this case,
604 // we are at a run boundary so we skip the below test.
605 if (runIndex != runs.length) {
606 boolean runIsRtl = (runLevel & 0x1) != 0;
607 boolean advance = toLeft == runIsRtl;
608 if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
609 // Moving within or into the run, so we can move logically.
610 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
611 runIsRtl, cursor, advance);
612 // If the new position is internal to the run, we're at the strong
613 // position already so we're finished.
614 if (newCaret != (advance ? runLimit : runStart)) {
615 return newCaret;
616 }
617 }
618 }
619 }
620
621 // If newCaret is -1, we're starting at a run boundary and crossing
622 // into another run. Otherwise we've arrived at a run boundary, and
623 // need to figure out which character to attach to. Note we might
624 // need to run this twice, if we cross a run boundary and end up at
625 // another run boundary.
626 while (true) {
627 boolean advance = toLeft == paraIsRtl;
628 int otherRunIndex = runIndex + (advance ? 2 : -2);
629 if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
630 int otherRunStart = lineStart + runs[otherRunIndex];
631 int otherRunLimit = otherRunStart +
632 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
633 if (otherRunLimit > lineEnd) {
634 otherRunLimit = lineEnd;
635 }
636 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
637 Layout.RUN_LEVEL_MASK;
638 boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
639
640 advance = toLeft == otherRunIsRtl;
641 if (newCaret == -1) {
642 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
643 otherRunLimit, otherRunIsRtl,
644 advance ? otherRunStart : otherRunLimit, advance);
645 if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
646 // Crossed and ended up at a new boundary,
647 // repeat a second and final time.
648 runIndex = otherRunIndex;
649 runLevel = otherRunLevel;
650 continue;
651 }
652 break;
653 }
654
655 // The new caret is at a boundary.
656 if (otherRunLevel < runLevel) {
657 // The strong character is in the other run.
658 newCaret = advance ? otherRunStart : otherRunLimit;
659 }
660 break;
661 }
662
663 if (newCaret == -1) {
664 // We're walking off the end of the line. The paragraph
665 // level is always equal to or lower than any internal level, so
666 // the boundaries get the strong caret.
667 newCaret = advance ? mLen + 1 : -1;
668 break;
669 }
670
671 // Else we've arrived at the end of the line. That's a strong position.
672 // We might have arrived here by crossing over a run with no internal
673 // breaks and dropping out of the above loop before advancing one final
674 // time, so reset the caret.
675 // Note, we use '<=' below to handle a situation where the only run
676 // on the line is a counter-directional run. If we're not advancing,
677 // we can end up at the 'lineEnd' position but the caret we want is at
678 // the lineStart.
679 if (newCaret <= lineEnd) {
680 newCaret = advance ? lineEnd : lineStart;
681 }
682 break;
683 }
684
685 return newCaret;
686 }
687
688 /**
689 * Returns the next valid offset within this directional run, skipping
690 * conjuncts and zero-width characters. This should not be called to walk
691 * off the end of the line, since the returned values might not be valid
692 * on neighboring lines. If the returned offset is less than zero or
693 * greater than the line length, the offset should be recomputed on the
694 * preceding or following line, respectively.
695 *
696 * @param runIndex the run index
697 * @param runStart the start of the run
698 * @param runLimit the limit of the run
699 * @param runIsRtl true if the run is right-to-left
700 * @param offset the offset
701 * @param after true if the new offset should logically follow the provided
702 * offset
703 * @return the new offset
704 */
705 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
706 boolean runIsRtl, int offset, boolean after) {
707
708 if (runIndex < 0 || offset == (after ? mLen : 0)) {
709 // Walking off end of line. Since we don't know
710 // what cursor positions are available on other lines, we can't
711 // return accurate values. These are a guess.
712 if (after) {
713 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
714 }
715 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
716 }
717
718 TextPaint wp = mWorkPaint;
719 wp.set(mPaint);
720 if (mIsJustifying) {
721 wp.setWordSpacing(mAddedWidthForJustify);
722 }
723
724 int spanStart = runStart;
725 int spanLimit;
726 if (mSpanned == null) {
727 spanLimit = runLimit;
728 } else {
729 int target = after ? offset + 1 : offset;
730 int limit = mStart + runLimit;
731 while (true) {
732 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
733 MetricAffectingSpan.class) - mStart;
734 if (spanLimit >= target) {
735 break;
736 }
737 spanStart = spanLimit;
738 }
739
740 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
741 mStart + spanLimit, MetricAffectingSpan.class);
742 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
743
744 if (spans.length > 0) {
745 ReplacementSpan replacement = null;
746 for (int j = 0; j < spans.length; j++) {
747 MetricAffectingSpan span = spans[j];
748 if (span instanceof ReplacementSpan) {
749 replacement = (ReplacementSpan)span;
750 } else {
751 span.updateMeasureState(wp);
752 }
753 }
754
755 if (replacement != null) {
756 // If we have a replacement span, we're moving either to
757 // the start or end of this span.
758 return after ? spanLimit : spanStart;
759 }
760 }
761 }
762
763 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
764 if (mCharsValid) {
765 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
766 runIsRtl, offset, cursorOpt);
767 } else {
768 return wp.getTextRunCursor(mText, mStart + spanStart,
769 mStart + spanLimit, runIsRtl, mStart + offset, cursorOpt) - mStart;
770 }
771 }
772
773 /**
774 * @param wp
775 */
776 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
777 final int previousTop = fmi.top;
778 final int previousAscent = fmi.ascent;
779 final int previousDescent = fmi.descent;
780 final int previousBottom = fmi.bottom;
781 final int previousLeading = fmi.leading;
782
783 wp.getFontMetricsInt(fmi);
784
785 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
786 previousLeading);
787 }
788
789 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
790 int previousDescent, int previousBottom, int previousLeading) {
791 fmi.top = Math.min(fmi.top, previousTop);
792 fmi.ascent = Math.min(fmi.ascent, previousAscent);
793 fmi.descent = Math.max(fmi.descent, previousDescent);
794 fmi.bottom = Math.max(fmi.bottom, previousBottom);
795 fmi.leading = Math.max(fmi.leading, previousLeading);
796 }
797
798 private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
799 float thickness, float xleft, float xright, float baseline) {
800 final float strokeTop = baseline + wp.baselineShift + position;
801
802 final int previousColor = wp.getColor();
803 final Paint.Style previousStyle = wp.getStyle();
804 final boolean previousAntiAlias = wp.isAntiAlias();
805
806 wp.setStyle(Paint.Style.FILL);
807 wp.setAntiAlias(true);
808
809 wp.setColor(color);
810 c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
811
812 wp.setStyle(previousStyle);
813 wp.setColor(previousColor);
814 wp.setAntiAlias(previousAntiAlias);
815 }
816
817 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
818 boolean runIsRtl, int offset) {
819 if (mCharsValid) {
820 return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
821 } else {
822 final int delta = mStart;
823 if (mComputed == null) {
824 return wp.getRunAdvance(mText, delta + start, delta + end,
825 delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
826 } else {
827 return mComputed.getWidth(start + delta, end + delta);
828 }
829 }
830 }
831
832 /**
833 * Utility function for measuring and rendering text. The text must
834 * not include a tab.
835 *
836 * @param wp the working paint
837 * @param start the start of the text
838 * @param end the end of the text
839 * @param runIsRtl true if the run is right-to-left
840 * @param c the canvas, can be null if rendering is not needed
841 * @param x the edge of the run closest to the leading margin
842 * @param top the top of the line
843 * @param y the baseline
844 * @param bottom the bottom of the line
845 * @param fmi receives metrics information, can be null
846 * @param needWidth true if the width of the run is needed
847 * @param offset the offset for the purpose of measuring
848 * @param decorations the list of locations and paremeters for drawing decorations
849 * @return the signed width of the run based on the run direction; only
850 * valid if needWidth is true
851 */
852 private float handleText(TextPaint wp, int start, int end,
853 int contextStart, int contextEnd, boolean runIsRtl,
854 Canvas c, float x, int top, int y, int bottom,
855 FontMetricsInt fmi, boolean needWidth, int offset,
856 @Nullable ArrayList<DecorationInfo> decorations) {
857
858 if (mIsJustifying) {
859 wp.setWordSpacing(mAddedWidthForJustify);
860 }
861 // Get metrics first (even for empty strings or "0" width runs)
862 if (fmi != null) {
863 expandMetricsFromPaint(fmi, wp);
864 }
865
866 // No need to do anything if the run width is "0"
867 if (end == start) {
868 return 0f;
869 }
870
871 float totalWidth = 0;
872
873 final int numDecorations = decorations == null ? 0 : decorations.size();
874 if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
875 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
876 }
877
878 if (c != null) {
879 final float leftX, rightX;
880 if (runIsRtl) {
881 leftX = x - totalWidth;
882 rightX = x;
883 } else {
884 leftX = x;
885 rightX = x + totalWidth;
886 }
887
888 if (wp.bgColor != 0) {
889 int previousColor = wp.getColor();
890 Paint.Style previousStyle = wp.getStyle();
891
892 wp.setColor(wp.bgColor);
893 wp.setStyle(Paint.Style.FILL);
894 c.drawRect(leftX, top, rightX, bottom, wp);
895
896 wp.setStyle(previousStyle);
897 wp.setColor(previousColor);
898 }
899
900 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
901 leftX, y + wp.baselineShift);
902
903 if (numDecorations != 0) {
904 for (int i = 0; i < numDecorations; i++) {
905 final DecorationInfo info = decorations.get(i);
906
907 final int decorationStart = Math.max(info.start, start);
908 final int decorationEnd = Math.min(info.end, offset);
909 float decorationStartAdvance = getRunAdvance(
910 wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
911 float decorationEndAdvance = getRunAdvance(
912 wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
913 final float decorationXLeft, decorationXRight;
914 if (runIsRtl) {
915 decorationXLeft = rightX - decorationEndAdvance;
916 decorationXRight = rightX - decorationStartAdvance;
917 } else {
918 decorationXLeft = leftX + decorationStartAdvance;
919 decorationXRight = leftX + decorationEndAdvance;
920 }
921
922 // Theoretically, there could be cases where both Paint's and TextPaint's
923 // setUnderLineText() are called. For backward compatibility, we need to draw
924 // both underlines, the one with custom color first.
925 if (info.underlineColor != 0) {
926 drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
927 info.underlineThickness, decorationXLeft, decorationXRight, y);
928 }
929 if (info.isUnderlineText) {
930 final float thickness =
931 Math.max(wp.getUnderlineThickness(), 1.0f);
932 drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
933 decorationXLeft, decorationXRight, y);
934 }
935
936 if (info.isStrikeThruText) {
937 final float thickness =
938 Math.max(wp.getStrikeThruThickness(), 1.0f);
939 drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
940 decorationXLeft, decorationXRight, y);
941 }
942 }
943 }
944
945 }
946
947 return runIsRtl ? -totalWidth : totalWidth;
948 }
949
950 /**
951 * Utility function for measuring and rendering a replacement.
952 *
953 *
954 * @param replacement the replacement
955 * @param wp the work paint
956 * @param start the start of the run
957 * @param limit the limit of the run
958 * @param runIsRtl true if the run is right-to-left
959 * @param c the canvas, can be null if not rendering
960 * @param x the edge of the replacement closest to the leading margin
961 * @param top the top of the line
962 * @param y the baseline
963 * @param bottom the bottom of the line
964 * @param fmi receives metrics information, can be null
965 * @param needWidth true if the width of the replacement is needed
966 * @return the signed width of the run based on the run direction; only
967 * valid if needWidth is true
968 */
969 private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
970 int start, int limit, boolean runIsRtl, Canvas c,
971 float x, int top, int y, int bottom, FontMetricsInt fmi,
972 boolean needWidth) {
973
974 float ret = 0;
975
976 int textStart = mStart + start;
977 int textLimit = mStart + limit;
978
979 if (needWidth || (c != null && runIsRtl)) {
980 int previousTop = 0;
981 int previousAscent = 0;
982 int previousDescent = 0;
983 int previousBottom = 0;
984 int previousLeading = 0;
985
986 boolean needUpdateMetrics = (fmi != null);
987
988 if (needUpdateMetrics) {
989 previousTop = fmi.top;
990 previousAscent = fmi.ascent;
991 previousDescent = fmi.descent;
992 previousBottom = fmi.bottom;
993 previousLeading = fmi.leading;
994 }
995
996 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
997
998 if (needUpdateMetrics) {
999 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
1000 previousLeading);
1001 }
1002 }
1003
1004 if (c != null) {
1005 if (runIsRtl) {
1006 x -= ret;
1007 }
1008 replacement.draw(c, mText, textStart, textLimit,
1009 x, top, y, bottom, wp);
1010 }
1011
1012 return runIsRtl ? -ret : ret;
1013 }
1014
1015 private int adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit) {
1016 // Only draw hyphens on first in line. Disable them otherwise.
1017 return start > 0 ? Paint.START_HYPHEN_EDIT_NO_EDIT : startHyphenEdit;
1018 }
1019
1020 private int adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit) {
1021 // Only draw hyphens on last run in line. Disable them otherwise.
1022 return limit < mLen ? Paint.END_HYPHEN_EDIT_NO_EDIT : endHyphenEdit;
1023 }
1024
1025 private static final class DecorationInfo {
1026 public boolean isStrikeThruText;
1027 public boolean isUnderlineText;
1028 public int underlineColor;
1029 public float underlineThickness;
1030 public int start = -1;
1031 public int end = -1;
1032
1033 public boolean hasDecoration() {
1034 return isStrikeThruText || isUnderlineText || underlineColor != 0;
1035 }
1036
1037 // Copies the info, but not the start and end range.
1038 public DecorationInfo copyInfo() {
1039 final DecorationInfo copy = new DecorationInfo();
1040 copy.isStrikeThruText = isStrikeThruText;
1041 copy.isUnderlineText = isUnderlineText;
1042 copy.underlineColor = underlineColor;
1043 copy.underlineThickness = underlineThickness;
1044 return copy;
1045 }
1046 }
1047
1048 private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
1049 info.isStrikeThruText = paint.isStrikeThruText();
1050 if (info.isStrikeThruText) {
1051 paint.setStrikeThruText(false);
1052 }
1053 info.isUnderlineText = paint.isUnderlineText();
1054 if (info.isUnderlineText) {
1055 paint.setUnderlineText(false);
1056 }
1057 info.underlineColor = paint.underlineColor;
1058 info.underlineThickness = paint.underlineThickness;
1059 paint.setUnderlineText(0, 0.0f);
1060 }
1061
1062 /**
1063 * Utility function for handling a unidirectional run. The run must not
1064 * contain tabs but can contain styles.
1065 *
1066 *
1067 * @param start the line-relative start of the run
1068 * @param measureLimit the offset to measure to, between start and limit inclusive
1069 * @param limit the limit of the run
1070 * @param runIsRtl true if the run is right-to-left
1071 * @param c the canvas, can be null
1072 * @param x the end of the run closest to the leading margin
1073 * @param top the top of the line
1074 * @param y the baseline
1075 * @param bottom the bottom of the line
1076 * @param fmi receives metrics information, can be null
1077 * @param needWidth true if the width is required
1078 * @return the signed width of the run based on the run direction; only
1079 * valid if needWidth is true
1080 */
1081 private float handleRun(int start, int measureLimit,
1082 int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
1083 int bottom, FontMetricsInt fmi, boolean needWidth) {
1084
1085 if (measureLimit < start || measureLimit > limit) {
1086 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
1087 + "start (" + start + ") and limit (" + limit + ") bounds");
1088 }
1089
1090 // Case of an empty line, make sure we update fmi according to mPaint
1091 if (start == measureLimit) {
1092 final TextPaint wp = mWorkPaint;
1093 wp.set(mPaint);
1094 if (fmi != null) {
1095 expandMetricsFromPaint(fmi, wp);
1096 }
1097 return 0f;
1098 }
1099
1100 final boolean needsSpanMeasurement;
1101 if (mSpanned == null) {
1102 needsSpanMeasurement = false;
1103 } else {
1104 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
1105 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
1106 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
1107 || mCharacterStyleSpanSet.numberOfSpans != 0;
1108 }
1109
1110 if (!needsSpanMeasurement) {
1111 final TextPaint wp = mWorkPaint;
1112 wp.set(mPaint);
1113 wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit()));
1114 wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit()));
1115 return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
1116 y, bottom, fmi, needWidth, measureLimit, null);
1117 }
1118
1119 // Shaping needs to take into account context up to metric boundaries,
1120 // but rendering needs to take into account character style boundaries.
1121 // So we iterate through metric runs to get metric bounds,
1122 // then within each metric run iterate through character style runs
1123 // for the run bounds.
1124 final float originalX = x;
1125 for (int i = start, inext; i < measureLimit; i = inext) {
1126 final TextPaint wp = mWorkPaint;
1127 wp.set(mPaint);
1128
1129 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
1130 mStart;
1131 int mlimit = Math.min(inext, measureLimit);
1132
1133 ReplacementSpan replacement = null;
1134
1135 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
1136 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
1137 // empty by construction. This special case in getSpans() explains the >= & <= tests
1138 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit)
1139 || (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
1140
1141 boolean insideEllipsis =
1142 mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j]
1143 && mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd;
1144 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
1145 if (span instanceof ReplacementSpan) {
1146 replacement = !insideEllipsis ? (ReplacementSpan) span : null;
1147 } else {
1148 // We might have a replacement that uses the draw
1149 // state, otherwise measure state would suffice.
1150 span.updateDrawState(wp);
1151 }
1152 }
1153
1154 if (replacement != null) {
1155 x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
1156 bottom, fmi, needWidth || mlimit < measureLimit);
1157 continue;
1158 }
1159
1160 final TextPaint activePaint = mActivePaint;
1161 activePaint.set(mPaint);
1162 int activeStart = i;
1163 int activeEnd = mlimit;
1164 final DecorationInfo decorationInfo = mDecorationInfo;
1165 mDecorations.clear();
1166 for (int j = i, jnext; j < mlimit; j = jnext) {
1167 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
1168 mStart;
1169
1170 final int offset = Math.min(jnext, mlimit);
1171 wp.set(mPaint);
1172 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
1173 // Intentionally using >= and <= as explained above
1174 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
1175 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
1176
1177 final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
1178 span.updateDrawState(wp);
1179 }
1180
1181 extractDecorationInfo(wp, decorationInfo);
1182
1183 if (j == i) {
1184 // First chunk of text. We can't handle it yet, since we may need to merge it
1185 // with the next chunk. So we just save the TextPaint for future comparisons
1186 // and use.
1187 activePaint.set(wp);
1188 } else if (!equalAttributes(wp, activePaint)) {
1189 // The style of the present chunk of text is substantially different from the
1190 // style of the previous chunk. We need to handle the active piece of text
1191 // and restart with the present chunk.
1192 activePaint.setStartHyphenEdit(
1193 adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
1194 activePaint.setEndHyphenEdit(
1195 adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
1196 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1197 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
1198 Math.min(activeEnd, mlimit), mDecorations);
1199
1200 activeStart = j;
1201 activePaint.set(wp);
1202 mDecorations.clear();
1203 } else {
1204 // The present TextPaint is substantially equal to the last TextPaint except
1205 // perhaps for decorations. We just need to expand the active piece of text to
1206 // include the present chunk, which we always do anyway. We don't need to save
1207 // wp to activePaint, since they are already equal.
1208 }
1209
1210 activeEnd = jnext;
1211 if (decorationInfo.hasDecoration()) {
1212 final DecorationInfo copy = decorationInfo.copyInfo();
1213 copy.start = j;
1214 copy.end = jnext;
1215 mDecorations.add(copy);
1216 }
1217 }
1218 // Handle the final piece of text.
1219 activePaint.setStartHyphenEdit(
1220 adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
1221 activePaint.setEndHyphenEdit(
1222 adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
1223 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
1224 top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
1225 Math.min(activeEnd, mlimit), mDecorations);
1226 }
1227
1228 return x - originalX;
1229 }
1230
1231 /**
1232 * Render a text run with the set-up paint.
1233 *
1234 * @param c the canvas
1235 * @param wp the paint used to render the text
1236 * @param start the start of the run
1237 * @param end the end of the run
1238 * @param contextStart the start of context for the run
1239 * @param contextEnd the end of the context for the run
1240 * @param runIsRtl true if the run is right-to-left
1241 * @param x the x position of the left edge of the run
1242 * @param y the baseline of the run
1243 */
1244 private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
1245 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
1246
1247 if (mCharsValid) {
1248 int count = end - start;
1249 int contextCount = contextEnd - contextStart;
1250 c.drawTextRun(mChars, start, count, contextStart, contextCount,
1251 x, y, runIsRtl, wp);
1252 } else {
1253 int delta = mStart;
1254 c.drawTextRun(mText, delta + start, delta + end,
1255 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
1256 }
1257 }
1258
1259 /**
1260 * Returns the next tab position.
1261 *
1262 * @param h the (unsigned) offset from the leading margin
1263 * @return the (unsigned) tab position after this offset
1264 */
1265 float nextTab(float h) {
1266 if (mTabs != null) {
1267 return mTabs.nextTab(h);
1268 }
1269 return TabStops.nextDefaultStop(h, TAB_INCREMENT);
1270 }
1271
1272 private boolean isStretchableWhitespace(int ch) {
1273 // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
1274 return ch == 0x0020;
1275 }
1276
1277 /* Return the number of spaces in the text line, for the purpose of justification */
1278 private int countStretchableSpaces(int start, int end) {
1279 int count = 0;
1280 for (int i = start; i < end; i++) {
1281 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
1282 if (isStretchableWhitespace(c)) {
1283 count++;
1284 }
1285 }
1286 return count;
1287 }
1288
1289 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
1290 public static boolean isLineEndSpace(char ch) {
1291 return ch == ' ' || ch == '\t' || ch == 0x1680
1292 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
1293 || ch == 0x205F || ch == 0x3000;
1294 }
1295
1296 private static final int TAB_INCREMENT = 20;
1297
1298 private static boolean equalAttributes(@NonNull TextPaint lp, @NonNull TextPaint rp) {
1299 return lp.getColorFilter() == rp.getColorFilter()
1300 && lp.getMaskFilter() == rp.getMaskFilter()
1301 && lp.getShader() == rp.getShader()
1302 && lp.getTypeface() == rp.getTypeface()
1303 && lp.getXfermode() == rp.getXfermode()
1304 && lp.getTextLocales().equals(rp.getTextLocales())
1305 && TextUtils.equals(lp.getFontFeatureSettings(), rp.getFontFeatureSettings())
1306 && TextUtils.equals(lp.getFontVariationSettings(), rp.getFontVariationSettings())
1307 && lp.getShadowLayerRadius() == rp.getShadowLayerRadius()
1308 && lp.getShadowLayerDx() == rp.getShadowLayerDx()
1309 && lp.getShadowLayerDy() == rp.getShadowLayerDy()
1310 && lp.getShadowLayerColor() == rp.getShadowLayerColor()
1311 && lp.getFlags() == rp.getFlags()
1312 && lp.getHinting() == rp.getHinting()
1313 && lp.getStyle() == rp.getStyle()
1314 && lp.getColor() == rp.getColor()
1315 && lp.getStrokeWidth() == rp.getStrokeWidth()
1316 && lp.getStrokeMiter() == rp.getStrokeMiter()
1317 && lp.getStrokeCap() == rp.getStrokeCap()
1318 && lp.getStrokeJoin() == rp.getStrokeJoin()
1319 && lp.getTextAlign() == rp.getTextAlign()
1320 && lp.isElegantTextHeight() == rp.isElegantTextHeight()
1321 && lp.getTextSize() == rp.getTextSize()
1322 && lp.getTextScaleX() == rp.getTextScaleX()
1323 && lp.getTextSkewX() == rp.getTextSkewX()
1324 && lp.getLetterSpacing() == rp.getLetterSpacing()
1325 && lp.getWordSpacing() == rp.getWordSpacing()
1326 && lp.getStartHyphenEdit() == rp.getStartHyphenEdit()
1327 && lp.getEndHyphenEdit() == rp.getEndHyphenEdit()
1328 && lp.bgColor == rp.bgColor
1329 && lp.baselineShift == rp.baselineShift
1330 && lp.linkColor == rp.linkColor
1331 && lp.drawableState == rp.drawableState
1332 && lp.density == rp.density
1333 && lp.underlineColor == rp.underlineColor
1334 && lp.underlineThickness == rp.underlineThickness;
1335 }
1336}