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, null, 0); 412 } 413 414 final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, null, 0); 415 h += sameDirection ? segmentWidth : -segmentWidth; 416 417 if (targetIsInThisSegment) { 418 return h + measureRun(segStart, offset, j, runIsRtl, null, null, 0); 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 * Return the signed horizontal bounds of the characters in the line. 441 * 442 * The length of the returned array equals to 2 * mLen. The left bound of the i th character 443 * is stored at index 2 * i. And the right bound of the i th character is stored at index 444 * (2 * i + 1). 445 * 446 * Check the following examples. LX(e.g. L0, L1, ...) denotes a character which has LTR BiDi 447 * property. On the other hand, RX(e.g. R0, R1, ...) denotes a character which has RTL BiDi 448 * property. Assuming all character has 1em width. 449 * 450 * Example 1: All LTR chars within LTR context 451 * Input Text (logical) : L0 L1 L2 L3 452 * Input Text (visual) : L0 L1 L2 L3 453 * Output : [0em, 1em, 1em, 2em, 2em, 3em, 3em, 4em] 454 * 455 * Example 2: All RTL chars within RTL context. 456 * Input Text (logical) : R0 R1 R2 R3 457 * Input Text (visual) : R3 R2 R1 R0 458 * Output : [-1em, 0em, -2em, -1em, -3em, -2em, -4em, -3em] 459 460 * 461 * Example 3: BiDi chars within LTR context. 462 * Input Text (logical) : L0 L1 R2 R3 L4 L5 463 * Input Text (visual) : L0 L1 R3 R2 L4 L5 464 * Output : [0em, 1em, 1em, 2em, 3em, 4em, 2em, 3em, 4em, 5em, 5em, 6em] 465 466 * 467 * Example 4: BiDi chars within RTL context. 468 * Input Text (logical) : L0 L1 R2 R3 L4 L5 469 * Input Text (visual) : L4 L5 R3 R2 L0 L1 470 * Output : [-2em, -1em, -1em, 0em, -3em, -2em, -4em, -3em, -6em, -5em, -5em, -4em] 471 * 472 * @param bounds the array to receive the character bounds data. Its length should be at least 473 * 2 times of the line length. 474 * @param advances the array to receive the character advance data, nullable. If provided, its 475 * length should be equal or larger than the line length. 476 * 477 * @throws IllegalArgumentException if the given {@code bounds} is null. 478 * @throws IndexOutOfBoundsException if the given {@code bounds} or {@code advances} doesn't 479 * have enough space to hold the result. 480 */ 481 public void measureAllBounds(@NonNull float[] bounds, @Nullable float[] advances) { 482 if (bounds == null) { 483 throw new IllegalArgumentException("bounds can't be null"); 484 } 485 if (bounds.length < 2 * mLen) { 486 throw new IndexOutOfBoundsException("bounds doesn't have enough space to receive the " 487 + "result, needed: " + (2 * mLen) + " had: " + bounds.length); 488 } 489 if (advances == null) { 490 advances = new float[mLen]; 491 } 492 if (advances.length < mLen) { 493 throw new IndexOutOfBoundsException("advance doesn't have enough space to receive the " 494 + "result, needed: " + mLen + " had: " + advances.length); 495 } 496 float h = 0; 497 for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) { 498 final int runStart = mDirections.getRunStart(runIndex); 499 if (runStart > mLen) break; 500 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); 501 final boolean runIsRtl = mDirections.isRunRtl(runIndex); 502 503 int segStart = runStart; 504 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { 505 if (j == runLimit || charAt(j) == TAB_CHAR) { 506 final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; 507 508 final float segmentWidth = 509 measureRun(segStart, j, j, runIsRtl, null, advances, segStart); 510 511 final float oldh = h; 512 h += sameDirection ? segmentWidth : -segmentWidth; 513 float currh = sameDirection ? oldh : h; 514 for (int offset = segStart; offset < j && offset < mLen; ++offset) { 515 if (runIsRtl) { 516 bounds[2 * offset + 1] = currh; 517 currh -= advances[offset]; 518 bounds[2 * offset] = currh; 519 } else { 520 bounds[2 * offset] = currh; 521 currh += advances[offset]; 522 bounds[2 * offset + 1] = currh; 523 } 524 } 525 526 if (j != runLimit) { // charAt(j) == TAB_CHAR 527 final float leftX; 528 final float rightX; 529 if (runIsRtl) { 530 rightX = h; 531 h = mDir * nextTab(h * mDir); 532 leftX = h; 533 } else { 534 leftX = h; 535 h = mDir * nextTab(h * mDir); 536 rightX = h; 537 } 538 bounds[2 * j] = leftX; 539 bounds[2 * j + 1] = rightX; 540 advances[j] = rightX - leftX; 541 } 542 543 segStart = j + 1; 544 } 545 } 546 } 547 } 548 549 /** 550 * @see #measure(int, boolean, FontMetricsInt) 551 * @return The measure results for all possible offsets 552 */ 553 @VisibleForTesting 554 public float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) { 555 float[] measurement = new float[mLen + 1]; 556 if (trailing[0]) { 557 measurement[0] = 0; 558 } 559 560 float horizontal = 0; 561 for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) { 562 final int runStart = mDirections.getRunStart(runIndex); 563 if (runStart > mLen) break; 564 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); 565 final boolean runIsRtl = mDirections.isRunRtl(runIndex); 566 567 int segStart = runStart; 568 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) { 569 if (j == runLimit || charAt(j) == TAB_CHAR) { 570 final float oldHorizontal = horizontal; 571 final boolean sameDirection = 572 (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; 573 574 // We are using measurement to receive character advance here. So that it 575 // doesn't need to allocate a new array. 576 // But be aware that when trailing[segStart] is true, measurement[segStart] 577 // will be computed in the previous run. And we need to store it first in case 578 // measureRun overwrites the result. 579 final float previousSegEndHorizontal = measurement[segStart]; 580 final float width = 581 measureRun(segStart, j, j, runIsRtl, fmi, measurement, segStart); 582 horizontal += sameDirection ? width : -width; 583 584 float currHorizontal = sameDirection ? oldHorizontal : horizontal; 585 final int segLimit = Math.min(j, mLen); 586 587 for (int offset = segStart; offset <= segLimit; ++offset) { 588 float advance = 0f; 589 // When offset == segLimit, advance is meaningless. 590 if (offset < segLimit) { 591 advance = runIsRtl ? -measurement[offset] : measurement[offset]; 592 } 593 594 if (offset == segStart && trailing[offset]) { 595 // If offset == segStart and trailing[segStart] is true, restore the 596 // value of measurement[segStart] from the previous run. 597 measurement[offset] = previousSegEndHorizontal; 598 } else if (offset != segLimit || trailing[offset]) { 599 measurement[offset] = currHorizontal; 600 } 601 602 currHorizontal += advance; 603 } 604 605 if (j != runLimit) { // charAt(j) == TAB_CHAR 606 if (!trailing[j]) { 607 measurement[j] = horizontal; 608 } 609 horizontal = mDir * nextTab(horizontal * mDir); 610 if (trailing[j + 1]) { 611 measurement[j + 1] = horizontal; 612 } 613 } 614 615 segStart = j + 1; 616 } 617 } 618 } 619 if (!trailing[mLen]) { 620 measurement[mLen] = horizontal; 621 } 622 return measurement; 623 } 624 625 /** 626 * Draws a unidirectional (but possibly multi-styled) run of text. 627 * 628 * 629 * @param c the canvas to draw on 630 * @param start the line-relative start 631 * @param limit the line-relative limit 632 * @param runIsRtl true if the run is right-to-left 633 * @param x the position of the run that is closest to the leading margin 634 * @param top the top of the line 635 * @param y the baseline 636 * @param bottom the bottom of the line 637 * @param needWidth true if the width value is required. 638 * @return the signed width of the run, based on the paragraph direction. 639 * Only valid if needWidth is true. 640 */ 641 private float drawRun(Canvas c, int start, 642 int limit, boolean runIsRtl, float x, int top, int y, int bottom, 643 boolean needWidth) { 644 645 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { 646 float w = -measureRun(start, limit, limit, runIsRtl, null, null, 0); 647 handleRun(start, limit, limit, runIsRtl, c, null, x + w, top, 648 y, bottom, null, false, null, 0); 649 return w; 650 } 651 652 return handleRun(start, limit, limit, runIsRtl, c, null, x, top, 653 y, bottom, null, needWidth, null, 0); 654 } 655 656 /** 657 * Measures a unidirectional (but possibly multi-styled) run of text. 658 * 659 * 660 * @param start the line-relative start of the run 661 * @param offset the offset to measure to, between start and limit inclusive 662 * @param limit the line-relative limit of the run 663 * @param runIsRtl true if the run is right-to-left 664 * @param fmi receives metrics information about the requested 665 * run, can be null. 666 * @param advances receives the advance information about the requested run, can be null. 667 * @param advancesIndex the start index to fill in the advance information. 668 * @return the signed width from the start of the run to the leading edge 669 * of the character at offset, based on the run (not paragraph) direction 670 */ 671 private float measureRun(int start, int offset, int limit, boolean runIsRtl, 672 @Nullable FontMetricsInt fmi, @Nullable float[] advances, int advancesIndex) { 673 return handleRun(start, offset, limit, runIsRtl, null, null, 0, 0, 0, 0, fmi, true, 674 advances, advancesIndex); 675 } 676 677 /** 678 * Shape a unidirectional (but possibly multi-styled) run of text. 679 * 680 * @param consumer the consumer of the shape result 681 * @param start the line-relative start 682 * @param limit the line-relative limit 683 * @param runIsRtl true if the run is right-to-left 684 * @param x the position of the run that is closest to the leading margin 685 * @param needWidth true if the width value is required. 686 * @return the signed width of the run, based on the paragraph direction. 687 * Only valid if needWidth is true. 688 */ 689 private float shapeRun(TextShaper.GlyphsConsumer consumer, int start, 690 int limit, boolean runIsRtl, float x, boolean needWidth) { 691 692 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { 693 float w = -measureRun(start, limit, limit, runIsRtl, null, null, 0); 694 handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, 695 false, null, 0); 696 return w; 697 } 698 699 return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, 700 needWidth, null, 0); 701 } 702 703 704 /** 705 * Walk the cursor through this line, skipping conjuncts and 706 * zero-width characters. 707 * 708 * <p>This function cannot properly walk the cursor off the ends of the line 709 * since it does not know about any shaping on the previous/following line 710 * that might affect the cursor position. Callers must either avoid these 711 * situations or handle the result specially. 712 * 713 * @param cursor the starting position of the cursor, between 0 and the 714 * length of the line, inclusive 715 * @param toLeft true if the caret is moving to the left. 716 * @return the new offset. If it is less than 0 or greater than the length 717 * of the line, the previous/following line should be examined to get the 718 * actual offset. 719 */ 720 int getOffsetToLeftRightOf(int cursor, boolean toLeft) { 721 // 1) The caret marks the leading edge of a character. The character 722 // logically before it might be on a different level, and the active caret 723 // position is on the character at the lower level. If that character 724 // was the previous character, the caret is on its trailing edge. 725 // 2) Take this character/edge and move it in the indicated direction. 726 // This gives you a new character and a new edge. 727 // 3) This position is between two visually adjacent characters. One of 728 // these might be at a lower level. The active position is on the 729 // character at the lower level. 730 // 4) If the active position is on the trailing edge of the character, 731 // the new caret position is the following logical character, else it 732 // is the character. 733 734 int lineStart = 0; 735 int lineEnd = mLen; 736 boolean paraIsRtl = mDir == -1; 737 int[] runs = mDirections.mDirections; 738 739 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1; 740 boolean trailing = false; 741 742 if (cursor == lineStart) { 743 runIndex = -2; 744 } else if (cursor == lineEnd) { 745 runIndex = runs.length; 746 } else { 747 // First, get information about the run containing the character with 748 // the active caret. 749 for (runIndex = 0; runIndex < runs.length; runIndex += 2) { 750 runStart = lineStart + runs[runIndex]; 751 if (cursor >= runStart) { 752 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK); 753 if (runLimit > lineEnd) { 754 runLimit = lineEnd; 755 } 756 if (cursor < runLimit) { 757 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & 758 Layout.RUN_LEVEL_MASK; 759 if (cursor == runStart) { 760 // The caret is on a run boundary, see if we should 761 // use the position on the trailing edge of the previous 762 // logical character instead. 763 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit; 764 int pos = cursor - 1; 765 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) { 766 prevRunStart = lineStart + runs[prevRunIndex]; 767 if (pos >= prevRunStart) { 768 prevRunLimit = prevRunStart + 769 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK); 770 if (prevRunLimit > lineEnd) { 771 prevRunLimit = lineEnd; 772 } 773 if (pos < prevRunLimit) { 774 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) 775 & Layout.RUN_LEVEL_MASK; 776 if (prevRunLevel < runLevel) { 777 // Start from logically previous character. 778 runIndex = prevRunIndex; 779 runLevel = prevRunLevel; 780 runStart = prevRunStart; 781 runLimit = prevRunLimit; 782 trailing = true; 783 break; 784 } 785 } 786 } 787 } 788 } 789 break; 790 } 791 } 792 } 793 794 // caret might be == lineEnd. This is generally a space or paragraph 795 // separator and has an associated run, but might be the end of 796 // text, in which case it doesn't. If that happens, we ran off the 797 // end of the run list, and runIndex == runs.length. In this case, 798 // we are at a run boundary so we skip the below test. 799 if (runIndex != runs.length) { 800 boolean runIsRtl = (runLevel & 0x1) != 0; 801 boolean advance = toLeft == runIsRtl; 802 if (cursor != (advance ? runLimit : runStart) || advance != trailing) { 803 // Moving within or into the run, so we can move logically. 804 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit, 805 runIsRtl, cursor, advance); 806 // If the new position is internal to the run, we're at the strong 807 // position already so we're finished. 808 if (newCaret != (advance ? runLimit : runStart)) { 809 return newCaret; 810 } 811 } 812 } 813 } 814 815 // If newCaret is -1, we're starting at a run boundary and crossing 816 // into another run. Otherwise we've arrived at a run boundary, and 817 // need to figure out which character to attach to. Note we might 818 // need to run this twice, if we cross a run boundary and end up at 819 // another run boundary. 820 while (true) { 821 boolean advance = toLeft == paraIsRtl; 822 int otherRunIndex = runIndex + (advance ? 2 : -2); 823 if (otherRunIndex >= 0 && otherRunIndex < runs.length) { 824 int otherRunStart = lineStart + runs[otherRunIndex]; 825 int otherRunLimit = otherRunStart + 826 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK); 827 if (otherRunLimit > lineEnd) { 828 otherRunLimit = lineEnd; 829 } 830 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & 831 Layout.RUN_LEVEL_MASK; 832 boolean otherRunIsRtl = (otherRunLevel & 1) != 0; 833 834 advance = toLeft == otherRunIsRtl; 835 if (newCaret == -1) { 836 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart, 837 otherRunLimit, otherRunIsRtl, 838 advance ? otherRunStart : otherRunLimit, advance); 839 if (newCaret == (advance ? otherRunLimit : otherRunStart)) { 840 // Crossed and ended up at a new boundary, 841 // repeat a second and final time. 842 runIndex = otherRunIndex; 843 runLevel = otherRunLevel; 844 continue; 845 } 846 break; 847 } 848 849 // The new caret is at a boundary. 850 if (otherRunLevel < runLevel) { 851 // The strong character is in the other run. 852 newCaret = advance ? otherRunStart : otherRunLimit; 853 } 854 break; 855 } 856 857 if (newCaret == -1) { 858 // We're walking off the end of the line. The paragraph 859 // level is always equal to or lower than any internal level, so 860 // the boundaries get the strong caret. 861 newCaret = advance ? mLen + 1 : -1; 862 break; 863 } 864 865 // Else we've arrived at the end of the line. That's a strong position. 866 // We might have arrived here by crossing over a run with no internal 867 // breaks and dropping out of the above loop before advancing one final 868 // time, so reset the caret. 869 // Note, we use '<=' below to handle a situation where the only run 870 // on the line is a counter-directional run. If we're not advancing, 871 // we can end up at the 'lineEnd' position but the caret we want is at 872 // the lineStart. 873 if (newCaret <= lineEnd) { 874 newCaret = advance ? lineEnd : lineStart; 875 } 876 break; 877 } 878 879 return newCaret; 880 } 881 882 /** 883 * Returns the next valid offset within this directional run, skipping 884 * conjuncts and zero-width characters. This should not be called to walk 885 * off the end of the line, since the returned values might not be valid 886 * on neighboring lines. If the returned offset is less than zero or 887 * greater than the line length, the offset should be recomputed on the 888 * preceding or following line, respectively. 889 * 890 * @param runIndex the run index 891 * @param runStart the start of the run 892 * @param runLimit the limit of the run 893 * @param runIsRtl true if the run is right-to-left 894 * @param offset the offset 895 * @param after true if the new offset should logically follow the provided 896 * offset 897 * @return the new offset 898 */ getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, boolean runIsRtl, int offset, boolean after)899 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, 900 boolean runIsRtl, int offset, boolean after) { 901 902 if (runIndex < 0 || offset == (after ? mLen : 0)) { 903 // Walking off end of line. Since we don't know 904 // what cursor positions are available on other lines, we can't 905 // return accurate values. These are a guess. 906 if (after) { 907 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart; 908 } 909 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart; 910 } 911 912 TextPaint wp = mWorkPaint; 913 wp.set(mPaint); 914 if (mIsJustifying) { 915 wp.setWordSpacing(mAddedWidthForJustify); 916 } 917 918 int spanStart = runStart; 919 int spanLimit; 920 if (mSpanned == null || runStart == runLimit) { 921 spanLimit = runLimit; 922 } else { 923 int target = after ? offset + 1 : offset; 924 int limit = mStart + runLimit; 925 while (true) { 926 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit, 927 MetricAffectingSpan.class) - mStart; 928 if (spanLimit >= target) { 929 break; 930 } 931 spanStart = spanLimit; 932 } 933 934 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart, 935 mStart + spanLimit, MetricAffectingSpan.class); 936 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); 937 938 if (spans.length > 0) { 939 ReplacementSpan replacement = null; 940 for (int j = 0; j < spans.length; j++) { 941 MetricAffectingSpan span = spans[j]; 942 if (span instanceof ReplacementSpan) { 943 replacement = (ReplacementSpan)span; 944 } else { 945 span.updateMeasureState(wp); 946 } 947 } 948 949 if (replacement != null) { 950 // If we have a replacement span, we're moving either to 951 // the start or end of this span. 952 return after ? spanLimit : spanStart; 953 } 954 } 955 } 956 957 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE; 958 if (mCharsValid) { 959 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart, 960 runIsRtl, offset, cursorOpt); 961 } else { 962 return wp.getTextRunCursor(mText, mStart + spanStart, 963 mStart + spanLimit, runIsRtl, mStart + offset, cursorOpt) - mStart; 964 } 965 } 966 967 /** 968 * @param wp 969 */ expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp)970 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) { 971 final int previousTop = fmi.top; 972 final int previousAscent = fmi.ascent; 973 final int previousDescent = fmi.descent; 974 final int previousBottom = fmi.bottom; 975 final int previousLeading = fmi.leading; 976 977 wp.getFontMetricsInt(fmi); 978 979 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 980 previousLeading); 981 } 982 expandMetricsFromPaint(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, FontMetricsInt fmi)983 private void expandMetricsFromPaint(TextPaint wp, int start, int end, 984 int contextStart, int contextEnd, boolean runIsRtl, FontMetricsInt fmi) { 985 986 final int previousTop = fmi.top; 987 final int previousAscent = fmi.ascent; 988 final int previousDescent = fmi.descent; 989 final int previousBottom = fmi.bottom; 990 final int previousLeading = fmi.leading; 991 992 int count = end - start; 993 int contextCount = contextEnd - contextStart; 994 if (mCharsValid) { 995 wp.getFontMetricsInt(mChars, start, count, contextStart, contextCount, runIsRtl, 996 fmi); 997 } else { 998 if (mComputed == null) { 999 wp.getFontMetricsInt(mText, mStart + start, count, mStart + contextStart, 1000 contextCount, runIsRtl, fmi); 1001 } else { 1002 mComputed.getFontMetricsInt(mStart + start, mStart + end, fmi); 1003 } 1004 } 1005 1006 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 1007 previousLeading); 1008 } 1009 1010 updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, int previousDescent, int previousBottom, int previousLeading)1011 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, 1012 int previousDescent, int previousBottom, int previousLeading) { 1013 fmi.top = Math.min(fmi.top, previousTop); 1014 fmi.ascent = Math.min(fmi.ascent, previousAscent); 1015 fmi.descent = Math.max(fmi.descent, previousDescent); 1016 fmi.bottom = Math.max(fmi.bottom, previousBottom); 1017 fmi.leading = Math.max(fmi.leading, previousLeading); 1018 } 1019 drawStroke(TextPaint wp, Canvas c, int color, float position, float thickness, float xleft, float xright, float baseline)1020 private static void drawStroke(TextPaint wp, Canvas c, int color, float position, 1021 float thickness, float xleft, float xright, float baseline) { 1022 final float strokeTop = baseline + wp.baselineShift + position; 1023 1024 final int previousColor = wp.getColor(); 1025 final Paint.Style previousStyle = wp.getStyle(); 1026 final boolean previousAntiAlias = wp.isAntiAlias(); 1027 1028 wp.setStyle(Paint.Style.FILL); 1029 wp.setAntiAlias(true); 1030 1031 wp.setColor(color); 1032 c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp); 1033 1034 wp.setStyle(previousStyle); 1035 wp.setColor(previousColor); 1036 wp.setAntiAlias(previousAntiAlias); 1037 } 1038 getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex)1039 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd, 1040 boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex) { 1041 if (mCharsValid) { 1042 return wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd, 1043 runIsRtl, offset, advances, advancesIndex); 1044 } else { 1045 final int delta = mStart; 1046 if (mComputed == null || advances != null) { 1047 return wp.getRunCharacterAdvance(mText, delta + start, delta + end, 1048 delta + contextStart, delta + contextEnd, runIsRtl, 1049 delta + offset, advances, advancesIndex); 1050 } else { 1051 return mComputed.getWidth(start + delta, end + delta); 1052 } 1053 } 1054 } 1055 1056 /** 1057 * Utility function for measuring and rendering text. The text must 1058 * not include a tab. 1059 * 1060 * @param wp the working paint 1061 * @param start the start of the text 1062 * @param end the end of the text 1063 * @param runIsRtl true if the run is right-to-left 1064 * @param c the canvas, can be null if rendering is not needed 1065 * @param consumer the output positioned glyph list, can be null if not necessary 1066 * @param x the edge of the run closest to the leading margin 1067 * @param top the top of the line 1068 * @param y the baseline 1069 * @param bottom the bottom of the line 1070 * @param fmi receives metrics information, can be null 1071 * @param needWidth true if the width of the run is needed 1072 * @param offset the offset for the purpose of measuring 1073 * @param decorations the list of locations and paremeters for drawing decorations 1074 * @param advances receives the advance information about the requested run, can be null. 1075 * @param advancesIndex the start index to fill in the advance information. 1076 * @return the signed width of the run based on the run direction; only 1077 * valid if needWidth is true 1078 */ handleText(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth, int offset, @Nullable ArrayList<DecorationInfo> decorations, @Nullable float[] advances, int advancesIndex)1079 private float handleText(TextPaint wp, int start, int end, 1080 int contextStart, int contextEnd, boolean runIsRtl, 1081 Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, 1082 FontMetricsInt fmi, boolean needWidth, int offset, 1083 @Nullable ArrayList<DecorationInfo> decorations, 1084 @Nullable float[] advances, int advancesIndex) { 1085 1086 if (mIsJustifying) { 1087 wp.setWordSpacing(mAddedWidthForJustify); 1088 } 1089 // Get metrics first (even for empty strings or "0" width runs) 1090 if (fmi != null) { 1091 expandMetricsFromPaint(fmi, wp); 1092 } 1093 1094 // No need to do anything if the run width is "0" 1095 if (end == start) { 1096 return 0f; 1097 } 1098 1099 float totalWidth = 0; 1100 1101 final int numDecorations = decorations == null ? 0 : decorations.size(); 1102 if (needWidth || ((c != null || consumer != null) && (wp.bgColor != 0 1103 || numDecorations != 0 || runIsRtl))) { 1104 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset, 1105 advances, advancesIndex); 1106 } 1107 1108 final float leftX, rightX; 1109 if (runIsRtl) { 1110 leftX = x - totalWidth; 1111 rightX = x; 1112 } else { 1113 leftX = x; 1114 rightX = x + totalWidth; 1115 } 1116 1117 if (consumer != null) { 1118 shapeTextRun(consumer, wp, start, end, contextStart, contextEnd, runIsRtl, leftX); 1119 } 1120 1121 if (mUseFallbackExtent && fmi != null) { 1122 expandMetricsFromPaint(wp, start, end, contextStart, contextEnd, runIsRtl, fmi); 1123 } 1124 1125 if (c != null) { 1126 if (wp.bgColor != 0) { 1127 int previousColor = wp.getColor(); 1128 Paint.Style previousStyle = wp.getStyle(); 1129 1130 wp.setColor(wp.bgColor); 1131 wp.setStyle(Paint.Style.FILL); 1132 c.drawRect(leftX, top, rightX, bottom, wp); 1133 1134 wp.setStyle(previousStyle); 1135 wp.setColor(previousColor); 1136 } 1137 1138 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, 1139 leftX, y + wp.baselineShift); 1140 1141 if (numDecorations != 0) { 1142 for (int i = 0; i < numDecorations; i++) { 1143 final DecorationInfo info = decorations.get(i); 1144 1145 final int decorationStart = Math.max(info.start, start); 1146 final int decorationEnd = Math.min(info.end, offset); 1147 float decorationStartAdvance = getRunAdvance(wp, start, end, contextStart, 1148 contextEnd, runIsRtl, decorationStart, null, 0); 1149 float decorationEndAdvance = getRunAdvance(wp, start, end, contextStart, 1150 contextEnd, runIsRtl, decorationEnd, null, 0); 1151 final float decorationXLeft, decorationXRight; 1152 if (runIsRtl) { 1153 decorationXLeft = rightX - decorationEndAdvance; 1154 decorationXRight = rightX - decorationStartAdvance; 1155 } else { 1156 decorationXLeft = leftX + decorationStartAdvance; 1157 decorationXRight = leftX + decorationEndAdvance; 1158 } 1159 1160 // Theoretically, there could be cases where both Paint's and TextPaint's 1161 // setUnderLineText() are called. For backward compatibility, we need to draw 1162 // both underlines, the one with custom color first. 1163 if (info.underlineColor != 0) { 1164 drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(), 1165 info.underlineThickness, decorationXLeft, decorationXRight, y); 1166 } 1167 if (info.isUnderlineText) { 1168 final float thickness = 1169 Math.max(wp.getUnderlineThickness(), 1.0f); 1170 drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness, 1171 decorationXLeft, decorationXRight, y); 1172 } 1173 1174 if (info.isStrikeThruText) { 1175 final float thickness = 1176 Math.max(wp.getStrikeThruThickness(), 1.0f); 1177 drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness, 1178 decorationXLeft, decorationXRight, y); 1179 } 1180 } 1181 } 1182 1183 } 1184 1185 return runIsRtl ? -totalWidth : totalWidth; 1186 } 1187 1188 /** 1189 * Utility function for measuring and rendering a replacement. 1190 * 1191 * 1192 * @param replacement the replacement 1193 * @param wp the work paint 1194 * @param start the start of the run 1195 * @param limit the limit of the run 1196 * @param runIsRtl true if the run is right-to-left 1197 * @param c the canvas, can be null if not rendering 1198 * @param x the edge of the replacement closest to the leading margin 1199 * @param top the top of the line 1200 * @param y the baseline 1201 * @param bottom the bottom of the line 1202 * @param fmi receives metrics information, can be null 1203 * @param needWidth true if the width of the replacement is needed 1204 * @return the signed width of the run based on the run direction; only 1205 * valid if needWidth is true 1206 */ handleReplacement(ReplacementSpan replacement, TextPaint wp, int start, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth)1207 private float handleReplacement(ReplacementSpan replacement, TextPaint wp, 1208 int start, int limit, boolean runIsRtl, Canvas c, 1209 float x, int top, int y, int bottom, FontMetricsInt fmi, 1210 boolean needWidth) { 1211 1212 float ret = 0; 1213 1214 int textStart = mStart + start; 1215 int textLimit = mStart + limit; 1216 1217 if (needWidth || (c != null && runIsRtl)) { 1218 int previousTop = 0; 1219 int previousAscent = 0; 1220 int previousDescent = 0; 1221 int previousBottom = 0; 1222 int previousLeading = 0; 1223 1224 boolean needUpdateMetrics = (fmi != null); 1225 1226 if (needUpdateMetrics) { 1227 previousTop = fmi.top; 1228 previousAscent = fmi.ascent; 1229 previousDescent = fmi.descent; 1230 previousBottom = fmi.bottom; 1231 previousLeading = fmi.leading; 1232 } 1233 1234 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); 1235 1236 if (needUpdateMetrics) { 1237 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 1238 previousLeading); 1239 } 1240 } 1241 1242 if (c != null) { 1243 if (runIsRtl) { 1244 x -= ret; 1245 } 1246 replacement.draw(c, mText, textStart, textLimit, 1247 x, top, y, bottom, wp); 1248 } 1249 1250 return runIsRtl ? -ret : ret; 1251 } 1252 adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit)1253 private int adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit) { 1254 // Only draw hyphens on first in line. Disable them otherwise. 1255 return start > 0 ? Paint.START_HYPHEN_EDIT_NO_EDIT : startHyphenEdit; 1256 } 1257 adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit)1258 private int adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit) { 1259 // Only draw hyphens on last run in line. Disable them otherwise. 1260 return limit < mLen ? Paint.END_HYPHEN_EDIT_NO_EDIT : endHyphenEdit; 1261 } 1262 1263 private static final class DecorationInfo { 1264 public boolean isStrikeThruText; 1265 public boolean isUnderlineText; 1266 public int underlineColor; 1267 public float underlineThickness; 1268 public int start = -1; 1269 public int end = -1; 1270 hasDecoration()1271 public boolean hasDecoration() { 1272 return isStrikeThruText || isUnderlineText || underlineColor != 0; 1273 } 1274 1275 // Copies the info, but not the start and end range. copyInfo()1276 public DecorationInfo copyInfo() { 1277 final DecorationInfo copy = new DecorationInfo(); 1278 copy.isStrikeThruText = isStrikeThruText; 1279 copy.isUnderlineText = isUnderlineText; 1280 copy.underlineColor = underlineColor; 1281 copy.underlineThickness = underlineThickness; 1282 return copy; 1283 } 1284 } 1285 extractDecorationInfo(@onNull TextPaint paint, @NonNull DecorationInfo info)1286 private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) { 1287 info.isStrikeThruText = paint.isStrikeThruText(); 1288 if (info.isStrikeThruText) { 1289 paint.setStrikeThruText(false); 1290 } 1291 info.isUnderlineText = paint.isUnderlineText(); 1292 if (info.isUnderlineText) { 1293 paint.setUnderlineText(false); 1294 } 1295 info.underlineColor = paint.underlineColor; 1296 info.underlineThickness = paint.underlineThickness; 1297 paint.setUnderlineText(0, 0.0f); 1298 } 1299 1300 /** 1301 * Utility function for handling a unidirectional run. The run must not 1302 * contain tabs but can contain styles. 1303 * 1304 * 1305 * @param start the line-relative start of the run 1306 * @param measureLimit the offset to measure to, between start and limit inclusive 1307 * @param limit the limit of the run 1308 * @param runIsRtl true if the run is right-to-left 1309 * @param c the canvas, can be null 1310 * @param consumer the output positioned glyphs, can be null 1311 * @param x the end of the run closest to the leading margin 1312 * @param top the top of the line 1313 * @param y the baseline 1314 * @param bottom the bottom of the line 1315 * @param fmi receives metrics information, can be null 1316 * @param needWidth true if the width is required 1317 * @param advances receives the advance information about the requested run, can be null. 1318 * @param advancesIndex the start index to fill in the advance information. 1319 * @return the signed width of the run based on the run direction; only 1320 * valid if needWidth is true 1321 */ handleRun(int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth, @Nullable float[] advances, int advancesIndex)1322 private float handleRun(int start, int measureLimit, 1323 int limit, boolean runIsRtl, Canvas c, 1324 TextShaper.GlyphsConsumer consumer, float x, int top, int y, 1325 int bottom, FontMetricsInt fmi, boolean needWidth, 1326 @Nullable float[] advances, int advancesIndex) { 1327 1328 if (measureLimit < start || measureLimit > limit) { 1329 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of " 1330 + "start (" + start + ") and limit (" + limit + ") bounds"); 1331 } 1332 1333 if (advances != null && advances.length - advancesIndex < measureLimit - start) { 1334 throw new IndexOutOfBoundsException("advances doesn't have enough space to receive the " 1335 + "result"); 1336 } 1337 1338 // Case of an empty line, make sure we update fmi according to mPaint 1339 if (start == measureLimit) { 1340 final TextPaint wp = mWorkPaint; 1341 wp.set(mPaint); 1342 if (fmi != null) { 1343 expandMetricsFromPaint(fmi, wp); 1344 } 1345 return 0f; 1346 } 1347 1348 final boolean needsSpanMeasurement; 1349 if (mSpanned == null) { 1350 needsSpanMeasurement = false; 1351 } else { 1352 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit); 1353 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit); 1354 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0 1355 || mCharacterStyleSpanSet.numberOfSpans != 0; 1356 } 1357 1358 if (!needsSpanMeasurement) { 1359 final TextPaint wp = mWorkPaint; 1360 wp.set(mPaint); 1361 wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit())); 1362 wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit())); 1363 return handleText(wp, start, limit, start, limit, runIsRtl, c, consumer, x, top, 1364 y, bottom, fmi, needWidth, measureLimit, null, advances, advancesIndex); 1365 } 1366 1367 // Shaping needs to take into account context up to metric boundaries, 1368 // but rendering needs to take into account character style boundaries. 1369 // So we iterate through metric runs to get metric bounds, 1370 // then within each metric run iterate through character style runs 1371 // for the run bounds. 1372 final float originalX = x; 1373 for (int i = start, inext; i < measureLimit; i = inext) { 1374 final TextPaint wp = mWorkPaint; 1375 wp.set(mPaint); 1376 1377 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) - 1378 mStart; 1379 int mlimit = Math.min(inext, measureLimit); 1380 1381 ReplacementSpan replacement = null; 1382 1383 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) { 1384 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT 1385 // empty by construction. This special case in getSpans() explains the >= & <= tests 1386 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) 1387 || (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue; 1388 1389 boolean insideEllipsis = 1390 mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j] 1391 && mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd; 1392 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j]; 1393 if (span instanceof ReplacementSpan) { 1394 replacement = !insideEllipsis ? (ReplacementSpan) span : null; 1395 } else { 1396 // We might have a replacement that uses the draw 1397 // state, otherwise measure state would suffice. 1398 span.updateDrawState(wp); 1399 } 1400 } 1401 1402 if (replacement != null) { 1403 final float width = handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, 1404 x, top, y, bottom, fmi, needWidth || mlimit < measureLimit); 1405 x += width; 1406 if (advances != null) { 1407 // For replacement, the entire width is assigned to the first character. 1408 advances[advancesIndex + i - start] = runIsRtl ? -width : width; 1409 for (int j = i + 1; j < mlimit; ++j) { 1410 advances[advancesIndex + j - start] = 0.0f; 1411 } 1412 } 1413 continue; 1414 } 1415 1416 final TextPaint activePaint = mActivePaint; 1417 activePaint.set(mPaint); 1418 int activeStart = i; 1419 int activeEnd = mlimit; 1420 final DecorationInfo decorationInfo = mDecorationInfo; 1421 mDecorations.clear(); 1422 for (int j = i, jnext; j < mlimit; j = jnext) { 1423 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) - 1424 mStart; 1425 1426 final int offset = Math.min(jnext, mlimit); 1427 wp.set(mPaint); 1428 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { 1429 // Intentionally using >= and <= as explained above 1430 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) || 1431 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue; 1432 1433 final CharacterStyle span = mCharacterStyleSpanSet.spans[k]; 1434 span.updateDrawState(wp); 1435 } 1436 1437 extractDecorationInfo(wp, decorationInfo); 1438 1439 if (j == i) { 1440 // First chunk of text. We can't handle it yet, since we may need to merge it 1441 // with the next chunk. So we just save the TextPaint for future comparisons 1442 // and use. 1443 activePaint.set(wp); 1444 } else if (!equalAttributes(wp, activePaint)) { 1445 // The style of the present chunk of text is substantially different from the 1446 // style of the previous chunk. We need to handle the active piece of text 1447 // and restart with the present chunk. 1448 activePaint.setStartHyphenEdit( 1449 adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit())); 1450 activePaint.setEndHyphenEdit( 1451 adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit())); 1452 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, 1453 consumer, x, top, y, bottom, fmi, needWidth || activeEnd < measureLimit, 1454 Math.min(activeEnd, mlimit), mDecorations, 1455 advances, advancesIndex + activeStart - start); 1456 1457 activeStart = j; 1458 activePaint.set(wp); 1459 mDecorations.clear(); 1460 } else { 1461 // The present TextPaint is substantially equal to the last TextPaint except 1462 // perhaps for decorations. We just need to expand the active piece of text to 1463 // include the present chunk, which we always do anyway. We don't need to save 1464 // wp to activePaint, since they are already equal. 1465 } 1466 1467 activeEnd = jnext; 1468 if (decorationInfo.hasDecoration()) { 1469 final DecorationInfo copy = decorationInfo.copyInfo(); 1470 copy.start = j; 1471 copy.end = jnext; 1472 mDecorations.add(copy); 1473 } 1474 } 1475 // Handle the final piece of text. 1476 activePaint.setStartHyphenEdit( 1477 adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit())); 1478 activePaint.setEndHyphenEdit( 1479 adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit())); 1480 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x, 1481 top, y, bottom, fmi, needWidth || activeEnd < measureLimit, 1482 Math.min(activeEnd, mlimit), mDecorations, 1483 advances, advancesIndex + activeStart - start); 1484 } 1485 1486 return x - originalX; 1487 } 1488 1489 /** 1490 * Render a text run with the set-up paint. 1491 * 1492 * @param c the canvas 1493 * @param wp the paint used to render the text 1494 * @param start the start of the run 1495 * @param end the end of the run 1496 * @param contextStart the start of context for the run 1497 * @param contextEnd the end of the context for the run 1498 * @param runIsRtl true if the run is right-to-left 1499 * @param x the x position of the left edge of the run 1500 * @param y the baseline of the run 1501 */ drawTextRun(Canvas c, TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x, int y)1502 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 1503 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 1504 1505 if (mCharsValid) { 1506 int count = end - start; 1507 int contextCount = contextEnd - contextStart; 1508 c.drawTextRun(mChars, start, count, contextStart, contextCount, 1509 x, y, runIsRtl, wp); 1510 } else { 1511 int delta = mStart; 1512 c.drawTextRun(mText, delta + start, delta + end, 1513 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp); 1514 } 1515 } 1516 1517 /** 1518 * Shape a text run with the set-up paint. 1519 * 1520 * @param consumer the output positioned glyphs list 1521 * @param paint the paint used to render the text 1522 * @param start the start of the run 1523 * @param end the end of the run 1524 * @param contextStart the start of context for the run 1525 * @param contextEnd the end of the context for the run 1526 * @param runIsRtl true if the run is right-to-left 1527 * @param x the x position of the left edge of the run 1528 */ shapeTextRun(TextShaper.GlyphsConsumer consumer, TextPaint paint, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x)1529 private void shapeTextRun(TextShaper.GlyphsConsumer consumer, TextPaint paint, 1530 int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x) { 1531 1532 int count = end - start; 1533 int contextCount = contextEnd - contextStart; 1534 PositionedGlyphs glyphs; 1535 if (mCharsValid) { 1536 glyphs = TextRunShaper.shapeTextRun( 1537 mChars, 1538 start, count, 1539 contextStart, contextCount, 1540 x, 0f, 1541 runIsRtl, 1542 paint 1543 ); 1544 } else { 1545 glyphs = TextRunShaper.shapeTextRun( 1546 mText, 1547 mStart + start, count, 1548 mStart + contextStart, contextCount, 1549 x, 0f, 1550 runIsRtl, 1551 paint 1552 ); 1553 } 1554 consumer.accept(start, count, glyphs, paint); 1555 } 1556 1557 1558 /** 1559 * Returns the next tab position. 1560 * 1561 * @param h the (unsigned) offset from the leading margin 1562 * @return the (unsigned) tab position after this offset 1563 */ nextTab(float h)1564 float nextTab(float h) { 1565 if (mTabs != null) { 1566 return mTabs.nextTab(h); 1567 } 1568 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 1569 } 1570 isStretchableWhitespace(int ch)1571 private boolean isStretchableWhitespace(int ch) { 1572 // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709). 1573 return ch == 0x0020; 1574 } 1575 1576 /* Return the number of spaces in the text line, for the purpose of justification */ countStretchableSpaces(int start, int end)1577 private int countStretchableSpaces(int start, int end) { 1578 int count = 0; 1579 for (int i = start; i < end; i++) { 1580 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart); 1581 if (isStretchableWhitespace(c)) { 1582 count++; 1583 } 1584 } 1585 return count; 1586 } 1587 1588 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace() isLineEndSpace(char ch)1589 public static boolean isLineEndSpace(char ch) { 1590 return ch == ' ' || ch == '\t' || ch == 0x1680 1591 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007) 1592 || ch == 0x205F || ch == 0x3000; 1593 } 1594 1595 private static final int TAB_INCREMENT = 20; 1596 equalAttributes(@onNull TextPaint lp, @NonNull TextPaint rp)1597 private static boolean equalAttributes(@NonNull TextPaint lp, @NonNull TextPaint rp) { 1598 return lp.getColorFilter() == rp.getColorFilter() 1599 && lp.getMaskFilter() == rp.getMaskFilter() 1600 && lp.getShader() == rp.getShader() 1601 && lp.getTypeface() == rp.getTypeface() 1602 && lp.getXfermode() == rp.getXfermode() 1603 && lp.getTextLocales().equals(rp.getTextLocales()) 1604 && TextUtils.equals(lp.getFontFeatureSettings(), rp.getFontFeatureSettings()) 1605 && TextUtils.equals(lp.getFontVariationSettings(), rp.getFontVariationSettings()) 1606 && lp.getShadowLayerRadius() == rp.getShadowLayerRadius() 1607 && lp.getShadowLayerDx() == rp.getShadowLayerDx() 1608 && lp.getShadowLayerDy() == rp.getShadowLayerDy() 1609 && lp.getShadowLayerColor() == rp.getShadowLayerColor() 1610 && lp.getFlags() == rp.getFlags() 1611 && lp.getHinting() == rp.getHinting() 1612 && lp.getStyle() == rp.getStyle() 1613 && lp.getColor() == rp.getColor() 1614 && lp.getStrokeWidth() == rp.getStrokeWidth() 1615 && lp.getStrokeMiter() == rp.getStrokeMiter() 1616 && lp.getStrokeCap() == rp.getStrokeCap() 1617 && lp.getStrokeJoin() == rp.getStrokeJoin() 1618 && lp.getTextAlign() == rp.getTextAlign() 1619 && lp.isElegantTextHeight() == rp.isElegantTextHeight() 1620 && lp.getTextSize() == rp.getTextSize() 1621 && lp.getTextScaleX() == rp.getTextScaleX() 1622 && lp.getTextSkewX() == rp.getTextSkewX() 1623 && lp.getLetterSpacing() == rp.getLetterSpacing() 1624 && lp.getWordSpacing() == rp.getWordSpacing() 1625 && lp.getStartHyphenEdit() == rp.getStartHyphenEdit() 1626 && lp.getEndHyphenEdit() == rp.getEndHyphenEdit() 1627 && lp.bgColor == rp.bgColor 1628 && lp.baselineShift == rp.baselineShift 1629 && lp.linkColor == rp.linkColor 1630 && lp.drawableState == rp.drawableState 1631 && lp.density == rp.density 1632 && lp.underlineColor == rp.underlineColor 1633 && lp.underlineThickness == rp.underlineThickness; 1634 } 1635 } 1636