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