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