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.Rect; 27 import android.graphics.RectF; 28 import android.graphics.text.PositionedGlyphs; 29 import android.graphics.text.TextRunShaper; 30 import android.os.Build; 31 import android.text.Layout.Directions; 32 import android.text.Layout.TabStops; 33 import android.text.style.CharacterStyle; 34 import android.text.style.MetricAffectingSpan; 35 import android.text.style.ReplacementSpan; 36 import android.util.Log; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.internal.util.ArrayUtils; 40 41 import java.util.ArrayList; 42 43 /** 44 * Represents a line of styled text, for measuring in visual order and 45 * for rendering. 46 * 47 * <p>Get a new instance using obtain(), and when finished with it, return it 48 * to the pool using recycle(). 49 * 50 * <p>Call set to prepare the instance for use, then either draw, measure, 51 * metrics, or caretToLeftRightOf. 52 * 53 * @hide 54 */ 55 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 56 public class TextLine { 57 private static final boolean DEBUG = false; 58 59 private static final char TAB_CHAR = '\t'; 60 61 private TextPaint mPaint; 62 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 63 private CharSequence mText; 64 private int mStart; 65 private int mLen; 66 private int mDir; 67 private Directions mDirections; 68 private boolean mHasTabs; 69 private TabStops mTabs; 70 private char[] mChars; 71 private boolean mCharsValid; 72 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 73 private Spanned mSpanned; 74 private PrecomputedText mComputed; 75 private RectF mTmpRectForMeasure; 76 private RectF mTmpRectForPaintAPI; 77 private Rect mTmpRectForPrecompute; 78 79 // Recycling object for Paint APIs. Do not use outside getRunAdvances method. 80 private Paint.RunInfo mRunInfo; 81 82 public static final class LineInfo { 83 private int mClusterCount; 84 getClusterCount()85 public int getClusterCount() { 86 return mClusterCount; 87 } 88 setClusterCount(int clusterCount)89 public void setClusterCount(int clusterCount) { 90 mClusterCount = clusterCount; 91 } 92 }; 93 94 private boolean mUseFallbackExtent = false; 95 96 // The start and end of a potentially existing ellipsis on this text line. 97 // We use them to filter out replacement and metric affecting spans on ellipsized away chars. 98 private int mEllipsisStart; 99 private int mEllipsisEnd; 100 101 // Additional width of whitespace for justification. This value is per whitespace, thus 102 // the line width will increase by mAddedWidthForJustify x (number of stretchable whitespaces). 103 private float mAddedWordSpacingInPx; 104 private float mAddedLetterSpacingInPx; 105 private boolean mIsJustifying; 106 107 @VisibleForTesting getAddedWordSpacingInPx()108 public float getAddedWordSpacingInPx() { 109 return mAddedWordSpacingInPx; 110 } 111 112 @VisibleForTesting getAddedLetterSpacingInPx()113 public float getAddedLetterSpacingInPx() { 114 return mAddedLetterSpacingInPx; 115 } 116 117 @VisibleForTesting isJustifying()118 public boolean isJustifying() { 119 return mIsJustifying; 120 } 121 122 private final TextPaint mWorkPaint = new TextPaint(); 123 private final TextPaint mActivePaint = new TextPaint(); 124 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 125 private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet = 126 new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class); 127 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 128 private final SpanSet<CharacterStyle> mCharacterStyleSpanSet = 129 new SpanSet<CharacterStyle>(CharacterStyle.class); 130 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 131 private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet = 132 new SpanSet<ReplacementSpan>(ReplacementSpan.class); 133 134 private final DecorationInfo mDecorationInfo = new DecorationInfo(); 135 private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>(); 136 137 /** Not allowed to access. If it's for memory leak workaround, it was already fixed M. */ 138 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 139 private static final TextLine[] sCached = new TextLine[3]; 140 141 /** 142 * Returns a new TextLine from the shared pool. 143 * 144 * @return an uninitialized TextLine 145 */ 146 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 147 @UnsupportedAppUsage obtain()148 public static TextLine obtain() { 149 TextLine tl; 150 synchronized (sCached) { 151 for (int i = sCached.length; --i >= 0;) { 152 if (sCached[i] != null) { 153 tl = sCached[i]; 154 sCached[i] = null; 155 return tl; 156 } 157 } 158 } 159 tl = new TextLine(); 160 if (DEBUG) { 161 Log.v("TLINE", "new: " + tl); 162 } 163 return tl; 164 } 165 166 /** 167 * Puts a TextLine back into the shared pool. Do not use this TextLine once 168 * it has been returned. 169 * @param tl the textLine 170 * @return null, as a convenience from clearing references to the provided 171 * TextLine 172 */ 173 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) recycle(TextLine tl)174 public static TextLine recycle(TextLine tl) { 175 tl.mText = null; 176 tl.mPaint = null; 177 tl.mDirections = null; 178 tl.mSpanned = null; 179 tl.mTabs = null; 180 tl.mChars = null; 181 tl.mComputed = null; 182 tl.mUseFallbackExtent = false; 183 184 tl.mMetricAffectingSpanSpanSet.recycle(); 185 tl.mCharacterStyleSpanSet.recycle(); 186 tl.mReplacementSpanSpanSet.recycle(); 187 188 synchronized(sCached) { 189 for (int i = 0; i < sCached.length; ++i) { 190 if (sCached[i] == null) { 191 sCached[i] = tl; 192 break; 193 } 194 } 195 } 196 return null; 197 } 198 199 /** 200 * Initializes a TextLine and prepares it for use. 201 * 202 * @param paint the base paint for the line 203 * @param text the text, can be Styled 204 * @param start the start of the line relative to the text 205 * @param limit the limit of the line relative to the text 206 * @param dir the paragraph direction of this line 207 * @param directions the directions information of this line 208 * @param hasTabs true if the line might contain tabs 209 * @param tabStops the tabStops. Can be null 210 * @param ellipsisStart the start of the ellipsis relative to the line 211 * @param ellipsisEnd the end of the ellipsis relative to the line. When there 212 * is no ellipsis, this should be equal to ellipsisStart. 213 * @param useFallbackLineSpacing true for enabling fallback line spacing. false for disabling 214 * fallback line spacing. 215 */ 216 @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)217 public void set(TextPaint paint, CharSequence text, int start, int limit, int dir, 218 Directions directions, boolean hasTabs, TabStops tabStops, 219 int ellipsisStart, int ellipsisEnd, boolean useFallbackLineSpacing) { 220 mPaint = paint; 221 mText = text; 222 mStart = start; 223 mLen = limit - start; 224 mDir = dir; 225 mDirections = directions; 226 mUseFallbackExtent = useFallbackLineSpacing; 227 if (mDirections == null) { 228 throw new IllegalArgumentException("Directions cannot be null"); 229 } 230 mHasTabs = hasTabs; 231 mSpanned = null; 232 233 boolean hasReplacement = false; 234 if (text instanceof Spanned) { 235 mSpanned = (Spanned) text; 236 mReplacementSpanSpanSet.init(mSpanned, start, limit); 237 hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0; 238 } 239 240 mComputed = null; 241 if (text instanceof PrecomputedText) { 242 // Here, no need to check line break strategy or hyphenation frequency since there is no 243 // line break concept here. 244 mComputed = (PrecomputedText) text; 245 if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) { 246 mComputed = null; 247 } 248 } 249 250 mCharsValid = hasReplacement; 251 252 if (mCharsValid) { 253 if (mChars == null || mChars.length < mLen) { 254 mChars = ArrayUtils.newUnpaddedCharArray(mLen); 255 } 256 TextUtils.getChars(text, start, limit, mChars, 0); 257 if (hasReplacement) { 258 // Handle these all at once so we don't have to do it as we go. 259 // Replace the first character of each replacement run with the 260 // object-replacement character and the remainder with zero width 261 // non-break space aka BOM. Cursor movement code skips these 262 // zero-width characters. 263 char[] chars = mChars; 264 for (int i = start, inext; i < limit; i = inext) { 265 inext = mReplacementSpanSpanSet.getNextTransition(i, limit); 266 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext) 267 && (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) { 268 // transition into a span 269 chars[i - start] = '\ufffc'; 270 for (int j = i - start + 1, e = inext - start; j < e; ++j) { 271 chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip 272 } 273 } 274 } 275 } 276 } 277 mTabs = tabStops; 278 mAddedWordSpacingInPx = 0; 279 mIsJustifying = false; 280 281 mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0; 282 mEllipsisEnd = ellipsisStart != ellipsisEnd ? ellipsisEnd : 0; 283 } 284 charAt(int i)285 private char charAt(int i) { 286 return mCharsValid ? mChars[i] : mText.charAt(i + mStart); 287 } 288 289 /** 290 * Justify the line to the given width. 291 */ 292 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) justify(@ayout.JustificationMode int justificationMode, float justifyWidth)293 public void justify(@Layout.JustificationMode int justificationMode, float justifyWidth) { 294 int end = mLen; 295 while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) { 296 end--; 297 } 298 if (justificationMode == Layout.JUSTIFICATION_MODE_INTER_WORD) { 299 float width = Math.abs(measure(end, false, null, null, null)); 300 final int spaces = countStretchableSpaces(0, end); 301 if (spaces == 0) { 302 // There are no stretchable spaces, so we can't help the justification by adding any 303 // width. 304 return; 305 } 306 mAddedWordSpacingInPx = (justifyWidth - width) / spaces; 307 mAddedLetterSpacingInPx = 0; 308 } else { // justificationMode == Layout.JUSTIFICATION_MODE_LETTER_SPACING 309 LineInfo lineInfo = new LineInfo(); 310 float width = Math.abs(measure(end, false, null, null, lineInfo)); 311 312 int lettersCount = lineInfo.getClusterCount(); 313 if (lettersCount < 2) { 314 return; 315 } 316 mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1); 317 if (mAddedLetterSpacingInPx > 0.03) { 318 // If the letter spacing is more than 0.03em, the ligatures are automatically 319 // disabled, so re-calculate everything without ligatures. 320 final String oldFontFeatures = mPaint.getFontFeatureSettings(); 321 mPaint.setFontFeatureSettings(oldFontFeatures + ", \"liga\" off, \"cliga\" off"); 322 width = Math.abs(measure(end, false, null, null, lineInfo)); 323 lettersCount = lineInfo.getClusterCount(); 324 mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1); 325 mPaint.setFontFeatureSettings(oldFontFeatures); 326 } 327 mAddedWordSpacingInPx = 0; 328 } 329 mIsJustifying = true; 330 } 331 332 /** 333 * Returns the run flag of at the given BiDi run. 334 * 335 * @param bidiRunIndex a BiDi run index. 336 * @return a run flag of the given BiDi run. 337 */ 338 @VisibleForTesting calculateRunFlag(int bidiRunIndex, int bidiRunCount, int lineDirection)339 public static int calculateRunFlag(int bidiRunIndex, int bidiRunCount, int lineDirection) { 340 if (bidiRunCount == 1) { 341 // Easy case. If there is only single run, it is most left and most right run. 342 return Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE; 343 } 344 if (bidiRunIndex != 0 && bidiRunIndex != (bidiRunCount - 1)) { 345 // Easy case. If the given run is the middle of the line, it is not the most left or 346 // the most right run. 347 return 0; 348 } 349 350 int runFlag = 0; 351 // For the historical reasons, the BiDi implementation of Android works differently 352 // from the Java BiDi APIs. The mDirections holds the BiDi runs in visual order, but 353 // it is reversed order if the paragraph direction is RTL. So, the first BiDi run of 354 // mDirections is located the most left of the line if the paragraph direction is LTR. 355 // If the paragraph direction is RTL, the first BiDi run is located the most right of 356 // the line. 357 if (bidiRunIndex == 0) { 358 if (lineDirection == Layout.DIR_LEFT_TO_RIGHT) { 359 runFlag |= Paint.TEXT_RUN_FLAG_LEFT_EDGE; 360 } else { 361 runFlag |= Paint.TEXT_RUN_FLAG_RIGHT_EDGE; 362 } 363 } 364 if (bidiRunIndex == (bidiRunCount - 1)) { 365 if (lineDirection == Layout.DIR_LEFT_TO_RIGHT) { 366 runFlag |= Paint.TEXT_RUN_FLAG_RIGHT_EDGE; 367 } else { 368 runFlag |= Paint.TEXT_RUN_FLAG_LEFT_EDGE; 369 } 370 } 371 return runFlag; 372 } 373 374 /** 375 * Resolve the runFlag for the inline span range. 376 * 377 * @param runFlag the runFlag of the current BiDi run. 378 * @param isRtlRun true for RTL run, false for LTR run. 379 * @param runStart the inclusive BiDi run start offset. 380 * @param runEnd the exclusive BiDi run end offset. 381 * @param spanStart the inclusive span start offset. 382 * @param spanEnd the exclusive span end offset. 383 * @return the resolved runFlag. 384 */ 385 @VisibleForTesting resolveRunFlagForSubSequence(int runFlag, boolean isRtlRun, int runStart, int runEnd, int spanStart, int spanEnd)386 public static int resolveRunFlagForSubSequence(int runFlag, boolean isRtlRun, int runStart, 387 int runEnd, int spanStart, int spanEnd) { 388 if (runFlag == 0) { 389 // Easy case. If the run is in the middle of the line, any inline span is also in the 390 // middle of the line. 391 return 0; 392 } 393 int localRunFlag = runFlag; 394 if ((runFlag & Paint.TEXT_RUN_FLAG_LEFT_EDGE) != 0) { 395 if (isRtlRun) { 396 if (spanEnd != runEnd) { 397 // In the RTL context, the last run is the most left run. 398 localRunFlag &= ~Paint.TEXT_RUN_FLAG_LEFT_EDGE; 399 } 400 } else { // LTR 401 if (spanStart != runStart) { 402 // In the LTR context, the first run is the most left run. 403 localRunFlag &= ~Paint.TEXT_RUN_FLAG_LEFT_EDGE; 404 } 405 } 406 } 407 if ((runFlag & Paint.TEXT_RUN_FLAG_RIGHT_EDGE) != 0) { 408 if (isRtlRun) { 409 if (spanStart != runStart) { 410 // In the RTL context, the start of the run is the most right run. 411 localRunFlag &= ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE; 412 } 413 } else { // LTR 414 if (spanEnd != runEnd) { 415 // In the LTR context, the last run is the most right position. 416 localRunFlag &= ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE; 417 } 418 } 419 } 420 return localRunFlag; 421 } 422 423 /** 424 * Renders the TextLine. 425 * 426 * @param c the canvas to render on 427 * @param x the leading margin position 428 * @param top the top of the line 429 * @param y the baseline 430 * @param bottom the bottom of the line 431 */ draw(Canvas c, float x, int top, int y, int bottom)432 void draw(Canvas c, float x, int top, int y, int bottom) { 433 float h = 0; 434 final int runCount = mDirections.getRunCount(); 435 for (int runIndex = 0; runIndex < runCount; runIndex++) { 436 final int runStart = mDirections.getRunStart(runIndex); 437 if (runStart > mLen) break; 438 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); 439 final boolean runIsRtl = mDirections.isRunRtl(runIndex); 440 441 final int runFlag = calculateRunFlag(runIndex, runCount, mDir); 442 443 int segStart = runStart; 444 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { 445 if (j == runLimit || charAt(j) == TAB_CHAR) { 446 h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom, 447 runIndex != (runCount - 1) || j != mLen, runFlag); 448 449 if (j != runLimit) { // charAt(j) == TAB_CHAR 450 h = mDir * nextTab(h * mDir); 451 } 452 segStart = j + 1; 453 } 454 } 455 } 456 } 457 458 /** 459 * Returns metrics information for the entire line. 460 * 461 * @param fmi receives font metrics information, can be null 462 * @param drawBounds output parameter for drawing bounding box. optional. 463 * @param returnDrawWidth true for returning width of the bounding box, false for returning 464 * total advances. 465 * @param lineInfo an optional output parameter for filling line information. 466 * @return the signed width of the line 467 */ 468 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth, @Nullable LineInfo lineInfo)469 public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth, 470 @Nullable LineInfo lineInfo) { 471 if (returnDrawWidth) { 472 if (drawBounds == null) { 473 if (mTmpRectForMeasure == null) { 474 mTmpRectForMeasure = new RectF(); 475 } 476 drawBounds = mTmpRectForMeasure; 477 } 478 drawBounds.setEmpty(); 479 float w = measure(mLen, false, fmi, drawBounds, lineInfo); 480 float boundsWidth; 481 if (w >= 0) { 482 boundsWidth = Math.max(drawBounds.right, w) - Math.min(0, drawBounds.left); 483 } else { 484 boundsWidth = Math.max(drawBounds.right, 0) - Math.min(w, drawBounds.left); 485 } 486 if (Math.abs(w) > boundsWidth) { 487 return w; 488 } else { 489 // bounds width is always positive but output of measure is signed width. 490 // To be able to use bounds width as signed width, use the sign of the width. 491 return Math.signum(w) * boundsWidth; 492 } 493 } else { 494 return measure(mLen, false, fmi, drawBounds, lineInfo); 495 } 496 } 497 498 /** 499 * Shape the TextLine. 500 */ shape(TextShaper.GlyphsConsumer consumer)501 void shape(TextShaper.GlyphsConsumer consumer) { 502 float horizontal = 0; 503 float x = 0; 504 final int runCount = mDirections.getRunCount(); 505 for (int runIndex = 0; runIndex < runCount; runIndex++) { 506 final int runStart = mDirections.getRunStart(runIndex); 507 if (runStart > mLen) break; 508 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); 509 final boolean runIsRtl = mDirections.isRunRtl(runIndex); 510 511 final int runFlag = calculateRunFlag(runIndex, runCount, mDir); 512 int segStart = runStart; 513 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { 514 if (j == runLimit || charAt(j) == TAB_CHAR) { 515 horizontal += shapeRun(consumer, segStart, j, runIsRtl, x + horizontal, 516 runIndex != (runCount - 1) || j != mLen, runFlag); 517 518 if (j != runLimit) { // charAt(j) == TAB_CHAR 519 horizontal = mDir * nextTab(horizontal * mDir); 520 } 521 segStart = j + 1; 522 } 523 } 524 } 525 } 526 527 /** 528 * Returns the signed graphical offset from the leading margin. 529 * 530 * Following examples are all for measuring offset=3. LX(e.g. L0, L1, ...) denotes a 531 * character which has LTR BiDi property. On the other hand, RX(e.g. R0, R1, ...) denotes a 532 * character which has RTL BiDi property. Assuming all character has 1em width. 533 * 534 * Example 1: All LTR chars within LTR context 535 * Input Text (logical) : L0 L1 L2 L3 L4 L5 L6 L7 L8 536 * Input Text (visual) : L0 L1 L2 L3 L4 L5 L6 L7 L8 537 * Output(trailing=true) : |--------| (Returns 3em) 538 * Output(trailing=false): |--------| (Returns 3em) 539 * 540 * Example 2: All RTL chars within RTL context. 541 * Input Text (logical) : R0 R1 R2 R3 R4 R5 R6 R7 R8 542 * Input Text (visual) : R8 R7 R6 R5 R4 R3 R2 R1 R0 543 * Output(trailing=true) : |--------| (Returns -3em) 544 * Output(trailing=false): |--------| (Returns -3em) 545 * 546 * Example 3: BiDi chars within LTR context. 547 * Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8 548 * Input Text (visual) : L0 L1 L2 R5 R4 R3 L6 L7 L8 549 * Output(trailing=true) : |-----------------| (Returns 6em) 550 * Output(trailing=false): |--------| (Returns 3em) 551 * 552 * Example 4: BiDi chars within RTL context. 553 * Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8 554 * Input Text (visual) : L6 L7 L8 R5 R4 R3 L0 L1 L2 555 * Output(trailing=true) : |-----------------| (Returns -6em) 556 * Output(trailing=false): |--------| (Returns -3em) 557 * 558 * @param offset the line-relative character offset, between 0 and the line length, inclusive 559 * @param trailing no effect if the offset is not on the BiDi transition offset. If the offset 560 * is on the BiDi transition offset and true is passed, the offset is regarded 561 * as the edge of the trailing run's edge. If false, the offset is regarded as 562 * the edge of the preceding run's edge. See example above. 563 * @param fmi receives metrics information about the requested character, can be null 564 * @param drawBounds output parameter for drawing bounding box. optional. 565 * @param lineInfo an optional output parameter for filling line information. 566 * @return the signed graphical offset from the leading margin to the requested character edge. 567 * The positive value means the offset is right from the leading edge. The negative 568 * value means the offset is left from the leading edge. 569 */ measure(@ntRangefrom = 0) int offset, boolean trailing, @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable LineInfo lineInfo)570 public float measure(@IntRange(from = 0) int offset, boolean trailing, 571 @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable LineInfo lineInfo) { 572 if (offset > mLen) { 573 throw new IndexOutOfBoundsException( 574 "offset(" + offset + ") should be less than line limit(" + mLen + ")"); 575 } 576 if (lineInfo != null) { 577 lineInfo.setClusterCount(0); 578 } 579 final int target = trailing ? offset - 1 : offset; 580 if (target < 0) { 581 return 0; 582 } 583 584 float h = 0; 585 final int runCount = mDirections.getRunCount(); 586 for (int runIndex = 0; runIndex < runCount; runIndex++) { 587 final int runStart = mDirections.getRunStart(runIndex); 588 if (runStart > mLen) break; 589 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); 590 final boolean runIsRtl = mDirections.isRunRtl(runIndex); 591 final int runFlag = calculateRunFlag(runIndex, runCount, mDir); 592 593 int segStart = runStart; 594 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { 595 if (j == runLimit || charAt(j) == TAB_CHAR) { 596 final boolean targetIsInThisSegment = target >= segStart && target < j; 597 final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; 598 599 if (targetIsInThisSegment && sameDirection) { 600 return h + measureRun(segStart, offset, j, runIsRtl, fmi, drawBounds, null, 601 0, h, lineInfo, runFlag); 602 } 603 604 final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, drawBounds, 605 null, 0, h, lineInfo, runFlag); 606 h += sameDirection ? segmentWidth : -segmentWidth; 607 608 if (targetIsInThisSegment) { 609 return h + measureRun(segStart, offset, j, runIsRtl, null, null, null, 0, 610 h, lineInfo, runFlag); 611 } 612 613 if (j != runLimit) { // charAt(j) == TAB_CHAR 614 if (offset == j) { 615 return h; 616 } 617 h = mDir * nextTab(h * mDir); 618 if (target == j) { 619 return h; 620 } 621 } 622 623 segStart = j + 1; 624 } 625 } 626 } 627 628 return h; 629 } 630 631 /** 632 * Return the signed horizontal bounds of the characters in the line. 633 * 634 * The length of the returned array equals to 2 * mLen. The left bound of the i th character 635 * is stored at index 2 * i. And the right bound of the i th character is stored at index 636 * (2 * i + 1). 637 * 638 * Check the following examples. LX(e.g. L0, L1, ...) denotes a character which has LTR BiDi 639 * property. On the other hand, RX(e.g. R0, R1, ...) denotes a character which has RTL BiDi 640 * property. Assuming all character has 1em width. 641 * 642 * Example 1: All LTR chars within LTR context 643 * Input Text (logical) : L0 L1 L2 L3 644 * Input Text (visual) : L0 L1 L2 L3 645 * Output : [0em, 1em, 1em, 2em, 2em, 3em, 3em, 4em] 646 * 647 * Example 2: All RTL chars within RTL context. 648 * Input Text (logical) : R0 R1 R2 R3 649 * Input Text (visual) : R3 R2 R1 R0 650 * Output : [-1em, 0em, -2em, -1em, -3em, -2em, -4em, -3em] 651 652 * 653 * Example 3: BiDi chars within LTR context. 654 * Input Text (logical) : L0 L1 R2 R3 L4 L5 655 * Input Text (visual) : L0 L1 R3 R2 L4 L5 656 * Output : [0em, 1em, 1em, 2em, 3em, 4em, 2em, 3em, 4em, 5em, 5em, 6em] 657 658 * 659 * Example 4: BiDi chars within RTL context. 660 * Input Text (logical) : L0 L1 R2 R3 L4 L5 661 * Input Text (visual) : L4 L5 R3 R2 L0 L1 662 * Output : [-2em, -1em, -1em, 0em, -3em, -2em, -4em, -3em, -6em, -5em, -5em, -4em] 663 * 664 * @param bounds the array to receive the character bounds data. Its length should be at least 665 * 2 times of the line length. 666 * @param advances the array to receive the character advance data, nullable. If provided, its 667 * length should be equal or larger than the line length. 668 * 669 * @throws IllegalArgumentException if the given {@code bounds} is null. 670 * @throws IndexOutOfBoundsException if the given {@code bounds} or {@code advances} doesn't 671 * have enough space to hold the result. 672 */ 673 public void measureAllBounds(@NonNull float[] bounds, @Nullable float[] advances) { 674 if (bounds == null) { 675 throw new IllegalArgumentException("bounds can't be null"); 676 } 677 if (bounds.length < 2 * mLen) { 678 throw new IndexOutOfBoundsException("bounds doesn't have enough space to receive the " 679 + "result, needed: " + (2 * mLen) + " had: " + bounds.length); 680 } 681 if (advances == null) { 682 advances = new float[mLen]; 683 } 684 if (advances.length < mLen) { 685 throw new IndexOutOfBoundsException("advance doesn't have enough space to receive the " 686 + "result, needed: " + mLen + " had: " + advances.length); 687 } 688 float h = 0; 689 final int runCount = mDirections.getRunCount(); 690 for (int runIndex = 0; runIndex < runCount; runIndex++) { 691 final int runStart = mDirections.getRunStart(runIndex); 692 if (runStart > mLen) break; 693 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); 694 final boolean runIsRtl = mDirections.isRunRtl(runIndex); 695 final int runFlag = calculateRunFlag(runIndex, runCount, mDir); 696 697 int segStart = runStart; 698 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { 699 if (j == runLimit || charAt(j) == TAB_CHAR) { 700 final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; 701 final float segmentWidth = 702 measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0, 703 null, runFlag); 704 705 final float oldh = h; 706 h += sameDirection ? segmentWidth : -segmentWidth; 707 float currh = sameDirection ? oldh : h; 708 for (int offset = segStart; offset < j && offset < mLen; ++offset) { 709 if (runIsRtl) { 710 bounds[2 * offset + 1] = currh; 711 currh -= advances[offset]; 712 bounds[2 * offset] = currh; 713 } else { 714 bounds[2 * offset] = currh; 715 currh += advances[offset]; 716 bounds[2 * offset + 1] = currh; 717 } 718 } 719 720 if (j != runLimit) { // charAt(j) == TAB_CHAR 721 final float leftX; 722 final float rightX; 723 if (runIsRtl) { 724 rightX = h; 725 h = mDir * nextTab(h * mDir); 726 leftX = h; 727 } else { 728 leftX = h; 729 h = mDir * nextTab(h * mDir); 730 rightX = h; 731 } 732 bounds[2 * j] = leftX; 733 bounds[2 * j + 1] = rightX; 734 advances[j] = rightX - leftX; 735 } 736 737 segStart = j + 1; 738 } 739 } 740 } 741 } 742 743 /** 744 * @see #measure(int, boolean, FontMetricsInt, RectF, LineInfo) 745 * @return The measure results for all possible offsets 746 */ 747 @VisibleForTesting 748 public float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) { 749 float[] measurement = new float[mLen + 1]; 750 if (trailing[0]) { 751 measurement[0] = 0; 752 } 753 754 float horizontal = 0; 755 final int runCount = mDirections.getRunCount(); 756 for (int runIndex = 0; runIndex < runCount; runIndex++) { 757 final int runStart = mDirections.getRunStart(runIndex); 758 if (runStart > mLen) break; 759 final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); 760 final boolean runIsRtl = mDirections.isRunRtl(runIndex); 761 final int runFlag = calculateRunFlag(runIndex, runCount, mDir); 762 763 int segStart = runStart; 764 for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) { 765 if (j == runLimit || charAt(j) == TAB_CHAR) { 766 final float oldHorizontal = horizontal; 767 final boolean sameDirection = 768 (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; 769 770 // We are using measurement to receive character advance here. So that it 771 // doesn't need to allocate a new array. 772 // But be aware that when trailing[segStart] is true, measurement[segStart] 773 // will be computed in the previous run. And we need to store it first in case 774 // measureRun overwrites the result. 775 final float previousSegEndHorizontal = measurement[segStart]; 776 final float width = 777 measureRun(segStart, j, j, runIsRtl, fmi, null, measurement, segStart, 778 0, null, runFlag); 779 horizontal += sameDirection ? width : -width; 780 781 float currHorizontal = sameDirection ? oldHorizontal : horizontal; 782 final int segLimit = Math.min(j, mLen); 783 784 for (int offset = segStart; offset <= segLimit; ++offset) { 785 float advance = 0f; 786 // When offset == segLimit, advance is meaningless. 787 if (offset < segLimit) { 788 advance = runIsRtl ? -measurement[offset] : measurement[offset]; 789 } 790 791 if (offset == segStart && trailing[offset]) { 792 // If offset == segStart and trailing[segStart] is true, restore the 793 // value of measurement[segStart] from the previous run. 794 measurement[offset] = previousSegEndHorizontal; 795 } else if (offset != segLimit || trailing[offset]) { 796 measurement[offset] = currHorizontal; 797 } 798 799 currHorizontal += advance; 800 } 801 802 if (j != runLimit) { // charAt(j) == TAB_CHAR 803 if (!trailing[j]) { 804 measurement[j] = horizontal; 805 } 806 horizontal = mDir * nextTab(horizontal * mDir); 807 if (trailing[j + 1]) { 808 measurement[j + 1] = horizontal; 809 } 810 } 811 812 segStart = j + 1; 813 } 814 } 815 } 816 if (!trailing[mLen]) { 817 measurement[mLen] = horizontal; 818 } 819 return measurement; 820 } 821 822 /** 823 * Draws a unidirectional (but possibly multi-styled) run of text. 824 * 825 * 826 * @param c the canvas to draw on 827 * @param start the line-relative start 828 * @param limit the line-relative limit 829 * @param runIsRtl true if the run is right-to-left 830 * @param x the position of the run that is closest to the leading margin 831 * @param top the top of the line 832 * @param y the baseline 833 * @param bottom the bottom of the line 834 * @param needWidth true if the width value is required. 835 * @param runFlag the run flag to be applied for this run. 836 * @return the signed width of the run, based on the paragraph direction. 837 * Only valid if needWidth is true. 838 */ 839 private float drawRun(Canvas c, int start, 840 int limit, boolean runIsRtl, float x, int top, int y, int bottom, 841 boolean needWidth, int runFlag) { 842 843 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { 844 float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null, 845 runFlag); 846 handleRun(start, limit, limit, runIsRtl, c, null, x + w, top, 847 y, bottom, null, null, false, null, 0, null, runFlag); 848 return w; 849 } 850 851 return handleRun(start, limit, limit, runIsRtl, c, null, x, top, 852 y, bottom, null, null, needWidth, null, 0, null, runFlag); 853 } 854 855 /** 856 * Measures a unidirectional (but possibly multi-styled) run of text. 857 * 858 * 859 * @param start the line-relative start of the run 860 * @param offset the offset to measure to, between start and limit inclusive 861 * @param limit the line-relative limit of the run 862 * @param runIsRtl true if the run is right-to-left 863 * @param fmi receives metrics information about the requested 864 * run, can be null. 865 * @param advances receives the advance information about the requested run, can be null. 866 * @param advancesIndex the start index to fill in the advance information. 867 * @param x horizontal offset of the run. 868 * @param lineInfo an optional output parameter for filling line information. 869 * @param runFlag the run flag to be applied for this run. 870 * @return the signed width from the start of the run to the leading edge 871 * of the character at offset, based on the run (not paragraph) direction 872 */ 873 private float measureRun(int start, int offset, int limit, boolean runIsRtl, 874 @Nullable FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable float[] advances, 875 int advancesIndex, float x, @Nullable LineInfo lineInfo, int runFlag) { 876 if (drawBounds != null && (mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { 877 float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0, null, 878 runFlag); 879 return handleRun(start, offset, limit, runIsRtl, null, null, x + w, 0, 0, 0, fmi, 880 drawBounds, true, advances, advancesIndex, lineInfo, runFlag); 881 } 882 return handleRun(start, offset, limit, runIsRtl, null, null, x, 0, 0, 0, fmi, drawBounds, 883 true, advances, advancesIndex, lineInfo, runFlag); 884 } 885 886 /** 887 * Shape a unidirectional (but possibly multi-styled) run of text. 888 * 889 * @param consumer the consumer of the shape result 890 * @param start the line-relative start 891 * @param limit the line-relative limit 892 * @param runIsRtl true if the run is right-to-left 893 * @param x the position of the run that is closest to the leading margin 894 * @param needWidth true if the width value is required. 895 * @param runFlag the run flag to be applied for this run. 896 * @return the signed width of the run, based on the paragraph direction. 897 * Only valid if needWidth is true. 898 */ 899 private float shapeRun(TextShaper.GlyphsConsumer consumer, int start, 900 int limit, boolean runIsRtl, float x, boolean needWidth, int runFlag) { 901 902 if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { 903 float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null, 904 runFlag); 905 handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, null, 906 false, null, 0, null, runFlag); 907 return w; 908 } 909 910 return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, null, 911 needWidth, null, 0, null, runFlag); 912 } 913 914 915 /** 916 * Walk the cursor through this line, skipping conjuncts and 917 * zero-width characters. 918 * 919 * <p>This function cannot properly walk the cursor off the ends of the line 920 * since it does not know about any shaping on the previous/following line 921 * that might affect the cursor position. Callers must either avoid these 922 * situations or handle the result specially. 923 * 924 * @param cursor the starting position of the cursor, between 0 and the 925 * length of the line, inclusive 926 * @param toLeft true if the caret is moving to the left. 927 * @return the new offset. If it is less than 0 or greater than the length 928 * of the line, the previous/following line should be examined to get the 929 * actual offset. 930 */ 931 int getOffsetToLeftRightOf(int cursor, boolean toLeft) { 932 // 1) The caret marks the leading edge of a character. The character 933 // logically before it might be on a different level, and the active caret 934 // position is on the character at the lower level. If that character 935 // was the previous character, the caret is on its trailing edge. 936 // 2) Take this character/edge and move it in the indicated direction. 937 // This gives you a new character and a new edge. 938 // 3) This position is between two visually adjacent characters. One of 939 // these might be at a lower level. The active position is on the 940 // character at the lower level. 941 // 4) If the active position is on the trailing edge of the character, 942 // the new caret position is the following logical character, else it 943 // is the character. 944 945 int lineStart = 0; 946 int lineEnd = mLen; 947 boolean paraIsRtl = mDir == -1; 948 int[] runs = mDirections.mDirections; 949 950 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1; 951 boolean trailing = false; 952 953 if (cursor == lineStart) { 954 runIndex = -2; 955 } else if (cursor == lineEnd) { 956 runIndex = runs.length; 957 } else { 958 // First, get information about the run containing the character with 959 // the active caret. 960 for (runIndex = 0; runIndex < runs.length; runIndex += 2) { 961 runStart = lineStart + runs[runIndex]; 962 if (cursor >= runStart) { 963 runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK); 964 if (runLimit > lineEnd) { 965 runLimit = lineEnd; 966 } 967 if (cursor < runLimit) { 968 runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & 969 Layout.RUN_LEVEL_MASK; 970 if (cursor == runStart) { 971 // The caret is on a run boundary, see if we should 972 // use the position on the trailing edge of the previous 973 // logical character instead. 974 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit; 975 int pos = cursor - 1; 976 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) { 977 prevRunStart = lineStart + runs[prevRunIndex]; 978 if (pos >= prevRunStart) { 979 prevRunLimit = prevRunStart + 980 (runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK); 981 if (prevRunLimit > lineEnd) { 982 prevRunLimit = lineEnd; 983 } 984 if (pos < prevRunLimit) { 985 prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) 986 & Layout.RUN_LEVEL_MASK; 987 if (prevRunLevel < runLevel) { 988 // Start from logically previous character. 989 runIndex = prevRunIndex; 990 runLevel = prevRunLevel; 991 runStart = prevRunStart; 992 runLimit = prevRunLimit; 993 trailing = true; 994 break; 995 } 996 } 997 } 998 } 999 } 1000 break; 1001 } 1002 } 1003 } 1004 1005 // caret might be == lineEnd. This is generally a space or paragraph 1006 // separator and has an associated run, but might be the end of 1007 // text, in which case it doesn't. If that happens, we ran off the 1008 // end of the run list, and runIndex == runs.length. In this case, 1009 // we are at a run boundary so we skip the below test. 1010 if (runIndex != runs.length) { 1011 boolean runIsRtl = (runLevel & 0x1) != 0; 1012 boolean advance = toLeft == runIsRtl; 1013 if (cursor != (advance ? runLimit : runStart) || advance != trailing) { 1014 // Moving within or into the run, so we can move logically. 1015 newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit, 1016 runIsRtl, cursor, advance); 1017 // If the new position is internal to the run, we're at the strong 1018 // position already so we're finished. 1019 if (newCaret != (advance ? runLimit : runStart)) { 1020 return newCaret; 1021 } 1022 } 1023 } 1024 } 1025 1026 // If newCaret is -1, we're starting at a run boundary and crossing 1027 // into another run. Otherwise we've arrived at a run boundary, and 1028 // need to figure out which character to attach to. Note we might 1029 // need to run this twice, if we cross a run boundary and end up at 1030 // another run boundary. 1031 while (true) { 1032 boolean advance = toLeft == paraIsRtl; 1033 int otherRunIndex = runIndex + (advance ? 2 : -2); 1034 if (otherRunIndex >= 0 && otherRunIndex < runs.length) { 1035 int otherRunStart = lineStart + runs[otherRunIndex]; 1036 int otherRunLimit = otherRunStart + 1037 (runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK); 1038 if (otherRunLimit > lineEnd) { 1039 otherRunLimit = lineEnd; 1040 } 1041 int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) & 1042 Layout.RUN_LEVEL_MASK; 1043 boolean otherRunIsRtl = (otherRunLevel & 1) != 0; 1044 1045 advance = toLeft == otherRunIsRtl; 1046 if (newCaret == -1) { 1047 newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart, 1048 otherRunLimit, otherRunIsRtl, 1049 advance ? otherRunStart : otherRunLimit, advance); 1050 if (newCaret == (advance ? otherRunLimit : otherRunStart)) { 1051 // Crossed and ended up at a new boundary, 1052 // repeat a second and final time. 1053 runIndex = otherRunIndex; 1054 runLevel = otherRunLevel; 1055 continue; 1056 } 1057 break; 1058 } 1059 1060 // The new caret is at a boundary. 1061 if (otherRunLevel < runLevel) { 1062 // The strong character is in the other run. 1063 newCaret = advance ? otherRunStart : otherRunLimit; 1064 } 1065 break; 1066 } 1067 1068 if (newCaret == -1) { 1069 // We're walking off the end of the line. The paragraph 1070 // level is always equal to or lower than any internal level, so 1071 // the boundaries get the strong caret. 1072 newCaret = advance ? mLen + 1 : -1; 1073 break; 1074 } 1075 1076 // Else we've arrived at the end of the line. That's a strong position. 1077 // We might have arrived here by crossing over a run with no internal 1078 // breaks and dropping out of the above loop before advancing one final 1079 // time, so reset the caret. 1080 // Note, we use '<=' below to handle a situation where the only run 1081 // on the line is a counter-directional run. If we're not advancing, 1082 // we can end up at the 'lineEnd' position but the caret we want is at 1083 // the lineStart. 1084 if (newCaret <= lineEnd) { 1085 newCaret = advance ? lineEnd : lineStart; 1086 } 1087 break; 1088 } 1089 1090 return newCaret; 1091 } 1092 1093 /** 1094 * Returns the next valid offset within this directional run, skipping 1095 * conjuncts and zero-width characters. This should not be called to walk 1096 * off the end of the line, since the returned values might not be valid 1097 * on neighboring lines. If the returned offset is less than zero or 1098 * greater than the line length, the offset should be recomputed on the 1099 * preceding or following line, respectively. 1100 * 1101 * @param runIndex the run index 1102 * @param runStart the start of the run 1103 * @param runLimit the limit of the run 1104 * @param runIsRtl true if the run is right-to-left 1105 * @param offset the offset 1106 * @param after true if the new offset should logically follow the provided 1107 * offset 1108 * @return the new offset 1109 */ getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, boolean runIsRtl, int offset, boolean after)1110 private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit, 1111 boolean runIsRtl, int offset, boolean after) { 1112 1113 if (runIndex < 0 || offset == (after ? mLen : 0)) { 1114 // Walking off end of line. Since we don't know 1115 // what cursor positions are available on other lines, we can't 1116 // return accurate values. These are a guess. 1117 if (after) { 1118 return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart; 1119 } 1120 return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart; 1121 } 1122 1123 TextPaint wp = mWorkPaint; 1124 wp.set(mPaint); 1125 if (mIsJustifying) { 1126 wp.setWordSpacing(mAddedWordSpacingInPx); 1127 wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize()); // Convert to Em 1128 } 1129 1130 int spanStart = runStart; 1131 int spanLimit; 1132 if (mSpanned == null || runStart == runLimit) { 1133 spanLimit = runLimit; 1134 } else { 1135 int target = after ? offset + 1 : offset; 1136 int limit = mStart + runLimit; 1137 while (true) { 1138 spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit, 1139 MetricAffectingSpan.class) - mStart; 1140 if (spanLimit >= target) { 1141 break; 1142 } 1143 spanStart = spanLimit; 1144 } 1145 1146 MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart, 1147 mStart + spanLimit, MetricAffectingSpan.class); 1148 spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); 1149 1150 if (spans.length > 0) { 1151 ReplacementSpan replacement = null; 1152 for (int j = 0; j < spans.length; j++) { 1153 MetricAffectingSpan span = spans[j]; 1154 if (span instanceof ReplacementSpan) { 1155 replacement = (ReplacementSpan)span; 1156 } else { 1157 span.updateMeasureState(wp); 1158 } 1159 } 1160 1161 if (replacement != null) { 1162 // If we have a replacement span, we're moving either to 1163 // the start or end of this span. 1164 return after ? spanLimit : spanStart; 1165 } 1166 } 1167 } 1168 1169 int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE; 1170 if (mCharsValid) { 1171 return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart, 1172 runIsRtl, offset, cursorOpt); 1173 } else { 1174 return wp.getTextRunCursor(mText, mStart + spanStart, 1175 mStart + spanLimit, runIsRtl, mStart + offset, cursorOpt) - mStart; 1176 } 1177 } 1178 1179 /** 1180 * @param wp 1181 */ expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp)1182 private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) { 1183 final int previousTop = fmi.top; 1184 final int previousAscent = fmi.ascent; 1185 final int previousDescent = fmi.descent; 1186 final int previousBottom = fmi.bottom; 1187 final int previousLeading = fmi.leading; 1188 1189 wp.getFontMetricsInt(fmi); 1190 1191 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 1192 previousLeading); 1193 } 1194 expandMetricsFromPaint(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, FontMetricsInt fmi)1195 private void expandMetricsFromPaint(TextPaint wp, int start, int end, 1196 int contextStart, int contextEnd, boolean runIsRtl, FontMetricsInt fmi) { 1197 1198 final int previousTop = fmi.top; 1199 final int previousAscent = fmi.ascent; 1200 final int previousDescent = fmi.descent; 1201 final int previousBottom = fmi.bottom; 1202 final int previousLeading = fmi.leading; 1203 1204 int count = end - start; 1205 int contextCount = contextEnd - contextStart; 1206 if (mCharsValid) { 1207 wp.getFontMetricsInt(mChars, start, count, contextStart, contextCount, runIsRtl, 1208 fmi); 1209 } else { 1210 if (mComputed == null) { 1211 wp.getFontMetricsInt(mText, mStart + start, count, mStart + contextStart, 1212 contextCount, runIsRtl, fmi); 1213 } else { 1214 mComputed.getFontMetricsInt(mStart + start, mStart + end, fmi); 1215 } 1216 } 1217 1218 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 1219 previousLeading); 1220 } 1221 1222 updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, int previousDescent, int previousBottom, int previousLeading)1223 static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent, 1224 int previousDescent, int previousBottom, int previousLeading) { 1225 fmi.top = Math.min(fmi.top, previousTop); 1226 fmi.ascent = Math.min(fmi.ascent, previousAscent); 1227 fmi.descent = Math.max(fmi.descent, previousDescent); 1228 fmi.bottom = Math.max(fmi.bottom, previousBottom); 1229 fmi.leading = Math.max(fmi.leading, previousLeading); 1230 } 1231 drawStroke(TextPaint wp, Canvas c, int color, float position, float thickness, float xleft, float xright, float baseline)1232 private static void drawStroke(TextPaint wp, Canvas c, int color, float position, 1233 float thickness, float xleft, float xright, float baseline) { 1234 final float strokeTop = baseline + wp.baselineShift + position; 1235 1236 final int previousColor = wp.getColor(); 1237 final Paint.Style previousStyle = wp.getStyle(); 1238 final boolean previousAntiAlias = wp.isAntiAlias(); 1239 1240 wp.setStyle(Paint.Style.FILL); 1241 wp.setAntiAlias(true); 1242 1243 wp.setColor(color); 1244 c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp); 1245 1246 wp.setStyle(previousStyle); 1247 wp.setColor(previousColor); 1248 wp.setAntiAlias(previousAntiAlias); 1249 } 1250 getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex, RectF drawingBounds, @Nullable LineInfo lineInfo)1251 private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd, 1252 boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex, 1253 RectF drawingBounds, @Nullable LineInfo lineInfo) { 1254 if (lineInfo != null) { 1255 if (mRunInfo == null) { 1256 mRunInfo = new Paint.RunInfo(); 1257 } 1258 mRunInfo.setClusterCount(0); 1259 } else { 1260 mRunInfo = null; 1261 } 1262 if (mCharsValid) { 1263 float r = wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd, 1264 runIsRtl, offset, advances, advancesIndex, drawingBounds, mRunInfo); 1265 if (lineInfo != null) { 1266 lineInfo.setClusterCount(lineInfo.getClusterCount() + mRunInfo.getClusterCount()); 1267 } 1268 return r; 1269 } else { 1270 final int delta = mStart; 1271 // TODO: Add cluster information to the PrecomputedText for better performance of 1272 // justification. 1273 if (mComputed == null || advances != null || lineInfo != null) { 1274 float r = wp.getRunCharacterAdvance(mText, delta + start, delta + end, 1275 delta + contextStart, delta + contextEnd, runIsRtl, 1276 delta + offset, advances, advancesIndex, drawingBounds, mRunInfo); 1277 if (lineInfo != null) { 1278 lineInfo.setClusterCount( 1279 lineInfo.getClusterCount() + mRunInfo.getClusterCount()); 1280 } 1281 return r; 1282 } else { 1283 if (drawingBounds != null) { 1284 if (mTmpRectForPrecompute == null) { 1285 mTmpRectForPrecompute = new Rect(); 1286 } 1287 mComputed.getBounds(start + delta, end + delta, mTmpRectForPrecompute); 1288 drawingBounds.set(mTmpRectForPrecompute); 1289 } 1290 return mComputed.getWidth(start + delta, end + delta); 1291 } 1292 } 1293 } 1294 1295 /** 1296 * Utility function for measuring and rendering text. The text must 1297 * not include a tab. 1298 * 1299 * @param wp the working paint 1300 * @param start the start of the text 1301 * @param end the end of the text 1302 * @param runIsRtl true if the run is right-to-left 1303 * @param c the canvas, can be null if rendering is not needed 1304 * @param consumer the output positioned glyph list, can be null if not necessary 1305 * @param x the edge of the run closest to the leading margin 1306 * @param top the top of the line 1307 * @param y the baseline 1308 * @param bottom the bottom of the line 1309 * @param fmi receives metrics information, can be null 1310 * @param needWidth true if the width of the run is needed 1311 * @param offset the offset for the purpose of measuring 1312 * @param decorations the list of locations and paremeters for drawing decorations 1313 * @param advances receives the advance information about the requested run, can be null. 1314 * @param advancesIndex the start index to fill in the advance information. 1315 * @param lineInfo an optional output parameter for filling line information. 1316 * @param runFlag the run flag to be applied for this run. 1317 * @return the signed width of the run based on the run direction; only 1318 * valid if needWidth is true 1319 */ 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, RectF drawBounds, boolean needWidth, int offset, @Nullable ArrayList<DecorationInfo> decorations, @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, int runFlag)1320 private float handleText(TextPaint wp, int start, int end, 1321 int contextStart, int contextEnd, boolean runIsRtl, 1322 Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, 1323 FontMetricsInt fmi, RectF drawBounds, boolean needWidth, int offset, 1324 @Nullable ArrayList<DecorationInfo> decorations, 1325 @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, 1326 int runFlag) { 1327 if (mIsJustifying) { 1328 wp.setWordSpacing(mAddedWordSpacingInPx); 1329 wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize()); // Convert to Em 1330 } 1331 // Get metrics first (even for empty strings or "0" width runs) 1332 if (drawBounds != null && fmi == null) { 1333 fmi = new FontMetricsInt(); 1334 } 1335 if (fmi != null) { 1336 expandMetricsFromPaint(fmi, wp); 1337 } 1338 1339 // No need to do anything if the run width is "0" 1340 if (end == start) { 1341 return 0f; 1342 } 1343 1344 float totalWidth = 0; 1345 if ((runFlag & Paint.TEXT_RUN_FLAG_LEFT_EDGE) == Paint.TEXT_RUN_FLAG_LEFT_EDGE) { 1346 wp.setFlags(wp.getFlags() | Paint.TEXT_RUN_FLAG_LEFT_EDGE); 1347 } else { 1348 wp.setFlags(wp.getFlags() & ~Paint.TEXT_RUN_FLAG_LEFT_EDGE); 1349 } 1350 if ((runFlag & Paint.TEXT_RUN_FLAG_RIGHT_EDGE) == Paint.TEXT_RUN_FLAG_RIGHT_EDGE) { 1351 wp.setFlags(wp.getFlags() | Paint.TEXT_RUN_FLAG_RIGHT_EDGE); 1352 } else { 1353 wp.setFlags(wp.getFlags() & ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE); 1354 } 1355 final int numDecorations = decorations == null ? 0 : decorations.size(); 1356 if (needWidth || ((c != null || consumer != null) && (wp.bgColor != 0 1357 || numDecorations != 0 || runIsRtl))) { 1358 if (drawBounds != null && mTmpRectForPaintAPI == null) { 1359 mTmpRectForPaintAPI = new RectF(); 1360 } 1361 totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset, 1362 advances, advancesIndex, drawBounds == null ? null : mTmpRectForPaintAPI, 1363 lineInfo); 1364 if (drawBounds != null) { 1365 if (runIsRtl) { 1366 mTmpRectForPaintAPI.offset(x - totalWidth, 0); 1367 } else { 1368 mTmpRectForPaintAPI.offset(x, 0); 1369 } 1370 drawBounds.union(mTmpRectForPaintAPI); 1371 } 1372 } 1373 1374 final float leftX, rightX; 1375 if (runIsRtl) { 1376 leftX = x - totalWidth; 1377 rightX = x; 1378 } else { 1379 leftX = x; 1380 rightX = x + totalWidth; 1381 } 1382 1383 if (consumer != null) { 1384 shapeTextRun(consumer, wp, start, end, contextStart, contextEnd, runIsRtl, leftX); 1385 } 1386 1387 if (mUseFallbackExtent && fmi != null) { 1388 expandMetricsFromPaint(wp, start, end, contextStart, contextEnd, runIsRtl, fmi); 1389 } 1390 1391 if (c != null) { 1392 if (wp.bgColor != 0) { 1393 int previousColor = wp.getColor(); 1394 Paint.Style previousStyle = wp.getStyle(); 1395 1396 wp.setColor(wp.bgColor); 1397 wp.setStyle(Paint.Style.FILL); 1398 c.drawRect(leftX, top, rightX, bottom, wp); 1399 1400 wp.setStyle(previousStyle); 1401 wp.setColor(previousColor); 1402 } 1403 1404 drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl, 1405 leftX, y + wp.baselineShift); 1406 1407 if (numDecorations != 0) { 1408 for (int i = 0; i < numDecorations; i++) { 1409 final DecorationInfo info = decorations.get(i); 1410 1411 final int decorationStart = Math.max(info.start, start); 1412 final int decorationEnd = Math.min(info.end, offset); 1413 float decorationStartAdvance = getRunAdvance(wp, start, end, contextStart, 1414 contextEnd, runIsRtl, decorationStart, null, 0, null, null); 1415 float decorationEndAdvance = getRunAdvance(wp, start, end, contextStart, 1416 contextEnd, runIsRtl, decorationEnd, null, 0, null, null); 1417 final float decorationXLeft, decorationXRight; 1418 if (runIsRtl) { 1419 decorationXLeft = rightX - decorationEndAdvance; 1420 decorationXRight = rightX - decorationStartAdvance; 1421 } else { 1422 decorationXLeft = leftX + decorationStartAdvance; 1423 decorationXRight = leftX + decorationEndAdvance; 1424 } 1425 1426 // Theoretically, there could be cases where both Paint's and TextPaint's 1427 // setUnderLineText() are called. For backward compatibility, we need to draw 1428 // both underlines, the one with custom color first. 1429 if (info.underlineColor != 0) { 1430 drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(), 1431 info.underlineThickness, decorationXLeft, decorationXRight, y); 1432 } 1433 if (info.isUnderlineText) { 1434 final float thickness = 1435 Math.max(wp.getUnderlineThickness(), 1.0f); 1436 drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness, 1437 decorationXLeft, decorationXRight, y); 1438 } 1439 1440 if (info.isStrikeThruText) { 1441 final float thickness = 1442 Math.max(wp.getStrikeThruThickness(), 1.0f); 1443 drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness, 1444 decorationXLeft, decorationXRight, y); 1445 } 1446 } 1447 } 1448 1449 } 1450 1451 return runIsRtl ? -totalWidth : totalWidth; 1452 } 1453 1454 /** 1455 * Utility function for measuring and rendering a replacement. 1456 * 1457 * 1458 * @param replacement the replacement 1459 * @param wp the work paint 1460 * @param start the start of the run 1461 * @param limit the limit of the run 1462 * @param runIsRtl true if the run is right-to-left 1463 * @param c the canvas, can be null if not rendering 1464 * @param x the edge of the replacement closest to the leading margin 1465 * @param top the top of the line 1466 * @param y the baseline 1467 * @param bottom the bottom of the line 1468 * @param fmi receives metrics information, can be null 1469 * @param needWidth true if the width of the replacement is needed 1470 * @return the signed width of the run based on the run direction; only 1471 * valid if needWidth is true 1472 */ 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)1473 private float handleReplacement(ReplacementSpan replacement, TextPaint wp, 1474 int start, int limit, boolean runIsRtl, Canvas c, 1475 float x, int top, int y, int bottom, FontMetricsInt fmi, 1476 boolean needWidth) { 1477 1478 float ret = 0; 1479 1480 int textStart = mStart + start; 1481 int textLimit = mStart + limit; 1482 1483 if (needWidth || (c != null && runIsRtl)) { 1484 int previousTop = 0; 1485 int previousAscent = 0; 1486 int previousDescent = 0; 1487 int previousBottom = 0; 1488 int previousLeading = 0; 1489 1490 boolean needUpdateMetrics = (fmi != null); 1491 1492 if (needUpdateMetrics) { 1493 previousTop = fmi.top; 1494 previousAscent = fmi.ascent; 1495 previousDescent = fmi.descent; 1496 previousBottom = fmi.bottom; 1497 previousLeading = fmi.leading; 1498 } 1499 1500 ret = replacement.getSize(wp, mText, textStart, textLimit, fmi); 1501 1502 if (needUpdateMetrics) { 1503 updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom, 1504 previousLeading); 1505 } 1506 } 1507 1508 if (c != null) { 1509 if (runIsRtl) { 1510 x -= ret; 1511 } 1512 replacement.draw(c, mText, textStart, textLimit, 1513 x, top, y, bottom, wp); 1514 } 1515 1516 return runIsRtl ? -ret : ret; 1517 } 1518 adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit)1519 private int adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit) { 1520 // Only draw hyphens on first in line. Disable them otherwise. 1521 return start > 0 ? Paint.START_HYPHEN_EDIT_NO_EDIT : startHyphenEdit; 1522 } 1523 adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit)1524 private int adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit) { 1525 // Only draw hyphens on last run in line. Disable them otherwise. 1526 return limit < mLen ? Paint.END_HYPHEN_EDIT_NO_EDIT : endHyphenEdit; 1527 } 1528 1529 private static final class DecorationInfo { 1530 public boolean isStrikeThruText; 1531 public boolean isUnderlineText; 1532 public int underlineColor; 1533 public float underlineThickness; 1534 public int start = -1; 1535 public int end = -1; 1536 hasDecoration()1537 public boolean hasDecoration() { 1538 return isStrikeThruText || isUnderlineText || underlineColor != 0; 1539 } 1540 1541 // Copies the info, but not the start and end range. copyInfo()1542 public DecorationInfo copyInfo() { 1543 final DecorationInfo copy = new DecorationInfo(); 1544 copy.isStrikeThruText = isStrikeThruText; 1545 copy.isUnderlineText = isUnderlineText; 1546 copy.underlineColor = underlineColor; 1547 copy.underlineThickness = underlineThickness; 1548 return copy; 1549 } 1550 } 1551 extractDecorationInfo(@onNull TextPaint paint, @NonNull DecorationInfo info)1552 private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) { 1553 info.isStrikeThruText = paint.isStrikeThruText(); 1554 if (info.isStrikeThruText) { 1555 paint.setStrikeThruText(false); 1556 } 1557 info.isUnderlineText = paint.isUnderlineText(); 1558 if (info.isUnderlineText) { 1559 paint.setUnderlineText(false); 1560 } 1561 info.underlineColor = paint.underlineColor; 1562 info.underlineThickness = paint.underlineThickness; 1563 paint.setUnderlineText(0, 0.0f); 1564 } 1565 1566 /** 1567 * Utility function for handling a unidirectional run. The run must not 1568 * contain tabs but can contain styles. 1569 * 1570 * 1571 * @param start the line-relative start of the run 1572 * @param measureLimit the offset to measure to, between start and limit inclusive 1573 * @param limit the limit of the run 1574 * @param runIsRtl true if the run is right-to-left 1575 * @param c the canvas, can be null 1576 * @param consumer the output positioned glyphs, can be null 1577 * @param x the end of the run closest to the leading margin 1578 * @param top the top of the line 1579 * @param y the baseline 1580 * @param bottom the bottom of the line 1581 * @param fmi receives metrics information, can be null 1582 * @param needWidth true if the width is required 1583 * @param advances receives the advance information about the requested run, can be null. 1584 * @param advancesIndex the start index to fill in the advance information. 1585 * @param lineInfo an optional output parameter for filling line information. 1586 * @param runFlag the run flag to be applied for this run. 1587 * @return the signed width of the run based on the run direction; only 1588 * valid if needWidth is true 1589 */ handleRun(int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth, @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, int runFlag)1590 private float handleRun(int start, int measureLimit, 1591 int limit, boolean runIsRtl, Canvas c, 1592 TextShaper.GlyphsConsumer consumer, float x, int top, int y, 1593 int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth, 1594 @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, 1595 int runFlag) { 1596 1597 if (measureLimit < start || measureLimit > limit) { 1598 throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of " 1599 + "start (" + start + ") and limit (" + limit + ") bounds"); 1600 } 1601 1602 if (advances != null && advances.length - advancesIndex < measureLimit - start) { 1603 throw new IndexOutOfBoundsException("advances doesn't have enough space to receive the " 1604 + "result"); 1605 } 1606 1607 // Case of an empty line, make sure we update fmi according to mPaint 1608 if (start == measureLimit) { 1609 final TextPaint wp = mWorkPaint; 1610 wp.set(mPaint); 1611 if (fmi != null) { 1612 expandMetricsFromPaint(fmi, wp); 1613 } 1614 if (drawBounds != null) { 1615 if (fmi == null) { 1616 FontMetricsInt tmpFmi = new FontMetricsInt(); 1617 expandMetricsFromPaint(tmpFmi, wp); 1618 fmi = tmpFmi; 1619 } 1620 drawBounds.union(0f, fmi.top, 0f, fmi.bottom); 1621 } 1622 return 0f; 1623 } 1624 1625 final boolean needsSpanMeasurement; 1626 if (mSpanned == null) { 1627 needsSpanMeasurement = false; 1628 } else { 1629 mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit); 1630 mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit); 1631 needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0 1632 || mCharacterStyleSpanSet.numberOfSpans != 0; 1633 } 1634 1635 if (!needsSpanMeasurement) { 1636 final TextPaint wp = mWorkPaint; 1637 wp.set(mPaint); 1638 wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit())); 1639 wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit())); 1640 return handleText(wp, start, limit, start, limit, runIsRtl, c, consumer, x, top, 1641 y, bottom, fmi, drawBounds, needWidth, measureLimit, null, advances, 1642 advancesIndex, lineInfo, runFlag); 1643 } 1644 1645 // Shaping needs to take into account context up to metric boundaries, 1646 // but rendering needs to take into account character style boundaries. 1647 // So we iterate through metric runs to get metric bounds, 1648 // then within each metric run iterate through character style runs 1649 // for the run bounds. 1650 final float originalX = x; 1651 for (int i = start, inext; i < measureLimit; i = inext) { 1652 final TextPaint wp = mWorkPaint; 1653 wp.set(mPaint); 1654 1655 inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) - 1656 mStart; 1657 int mlimit = Math.min(inext, measureLimit); 1658 1659 ReplacementSpan replacement = null; 1660 1661 for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) { 1662 // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT 1663 // empty by construction. This special case in getSpans() explains the >= & <= tests 1664 if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) 1665 || (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue; 1666 1667 boolean insideEllipsis = 1668 mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j] 1669 && mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd; 1670 final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j]; 1671 if (span instanceof ReplacementSpan) { 1672 replacement = !insideEllipsis ? (ReplacementSpan) span : null; 1673 } else { 1674 // We might have a replacement that uses the draw 1675 // state, otherwise measure state would suffice. 1676 span.updateDrawState(wp); 1677 } 1678 } 1679 1680 if (replacement != null) { 1681 final float width = handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, 1682 x, top, y, bottom, fmi, needWidth || mlimit < measureLimit); 1683 x += width; 1684 if (advances != null) { 1685 // For replacement, the entire width is assigned to the first character. 1686 advances[advancesIndex + i - start] = runIsRtl ? -width : width; 1687 for (int j = i + 1; j < mlimit; ++j) { 1688 advances[advancesIndex + j - start] = 0.0f; 1689 } 1690 } 1691 continue; 1692 } 1693 1694 final TextPaint activePaint = mActivePaint; 1695 activePaint.set(mPaint); 1696 int activeStart = i; 1697 int activeEnd = mlimit; 1698 final DecorationInfo decorationInfo = mDecorationInfo; 1699 mDecorations.clear(); 1700 for (int j = i, jnext; j < mlimit; j = jnext) { 1701 jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) - 1702 mStart; 1703 1704 final int offset = Math.min(jnext, mlimit); 1705 wp.set(mPaint); 1706 for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) { 1707 // Intentionally using >= and <= as explained above 1708 if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) || 1709 (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue; 1710 1711 final CharacterStyle span = mCharacterStyleSpanSet.spans[k]; 1712 span.updateDrawState(wp); 1713 } 1714 1715 extractDecorationInfo(wp, decorationInfo); 1716 1717 if (j == i) { 1718 // First chunk of text. We can't handle it yet, since we may need to merge it 1719 // with the next chunk. So we just save the TextPaint for future comparisons 1720 // and use. 1721 activePaint.set(wp); 1722 } else if (!equalAttributes(wp, activePaint)) { 1723 final int spanRunFlag = resolveRunFlagForSubSequence( 1724 runFlag, runIsRtl, start, measureLimit, activeStart, activeEnd); 1725 1726 // The style of the present chunk of text is substantially different from the 1727 // style of the previous chunk. We need to handle the active piece of text 1728 // and restart with the present chunk. 1729 activePaint.setStartHyphenEdit( 1730 adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit())); 1731 activePaint.setEndHyphenEdit( 1732 adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit())); 1733 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, 1734 consumer, x, top, y, bottom, fmi, drawBounds, 1735 needWidth || activeEnd < measureLimit, 1736 Math.min(activeEnd, mlimit), mDecorations, 1737 advances, advancesIndex + activeStart - start, lineInfo, spanRunFlag); 1738 1739 activeStart = j; 1740 activePaint.set(wp); 1741 mDecorations.clear(); 1742 } else { 1743 // The present TextPaint is substantially equal to the last TextPaint except 1744 // perhaps for decorations. We just need to expand the active piece of text to 1745 // include the present chunk, which we always do anyway. We don't need to save 1746 // wp to activePaint, since they are already equal. 1747 } 1748 1749 activeEnd = jnext; 1750 if (decorationInfo.hasDecoration()) { 1751 final DecorationInfo copy = decorationInfo.copyInfo(); 1752 copy.start = j; 1753 copy.end = jnext; 1754 mDecorations.add(copy); 1755 } 1756 } 1757 1758 final int spanRunFlag = resolveRunFlagForSubSequence( 1759 runFlag, runIsRtl, start, measureLimit, activeStart, activeEnd); 1760 // Handle the final piece of text. 1761 activePaint.setStartHyphenEdit( 1762 adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit())); 1763 activePaint.setEndHyphenEdit( 1764 adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit())); 1765 x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x, 1766 top, y, bottom, fmi, drawBounds, needWidth || activeEnd < measureLimit, 1767 Math.min(activeEnd, mlimit), mDecorations, 1768 advances, advancesIndex + activeStart - start, lineInfo, spanRunFlag); 1769 } 1770 1771 return x - originalX; 1772 } 1773 1774 /** 1775 * Render a text run with the set-up paint. 1776 * 1777 * @param c the canvas 1778 * @param wp the paint used to render the text 1779 * @param start the start of the run 1780 * @param end the end of the run 1781 * @param contextStart the start of context for the run 1782 * @param contextEnd the end of the context for the run 1783 * @param runIsRtl true if the run is right-to-left 1784 * @param x the x position of the left edge of the run 1785 * @param y the baseline of the run 1786 */ drawTextRun(Canvas c, TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x, int y)1787 private void drawTextRun(Canvas c, TextPaint wp, int start, int end, 1788 int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { 1789 if (mCharsValid) { 1790 int count = end - start; 1791 int contextCount = contextEnd - contextStart; 1792 c.drawTextRun(mChars, start, count, contextStart, contextCount, 1793 x, y, runIsRtl, wp); 1794 } else { 1795 int delta = mStart; 1796 c.drawTextRun(mText, delta + start, delta + end, 1797 delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp); 1798 } 1799 } 1800 1801 /** 1802 * Shape a text run with the set-up paint. 1803 * 1804 * @param consumer the output positioned glyphs list 1805 * @param paint the paint used to render the text 1806 * @param start the start of the run 1807 * @param end the end of the run 1808 * @param contextStart the start of context for the run 1809 * @param contextEnd the end of the context for the run 1810 * @param runIsRtl true if the run is right-to-left 1811 * @param x the x position of the left edge of the run 1812 */ shapeTextRun(TextShaper.GlyphsConsumer consumer, TextPaint paint, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x)1813 private void shapeTextRun(TextShaper.GlyphsConsumer consumer, TextPaint paint, 1814 int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x) { 1815 1816 int count = end - start; 1817 int contextCount = contextEnd - contextStart; 1818 PositionedGlyphs glyphs; 1819 if (mCharsValid) { 1820 glyphs = TextRunShaper.shapeTextRun( 1821 mChars, 1822 start, count, 1823 contextStart, contextCount, 1824 x, 0f, 1825 runIsRtl, 1826 paint 1827 ); 1828 } else { 1829 glyphs = TextRunShaper.shapeTextRun( 1830 mText, 1831 mStart + start, count, 1832 mStart + contextStart, contextCount, 1833 x, 0f, 1834 runIsRtl, 1835 paint 1836 ); 1837 } 1838 consumer.accept(start, count, glyphs, paint); 1839 } 1840 1841 1842 /** 1843 * Returns the next tab position. 1844 * 1845 * @param h the (unsigned) offset from the leading margin 1846 * @return the (unsigned) tab position after this offset 1847 */ nextTab(float h)1848 float nextTab(float h) { 1849 if (mTabs != null) { 1850 return mTabs.nextTab(h); 1851 } 1852 return TabStops.nextDefaultStop(h, TAB_INCREMENT); 1853 } 1854 isStretchableWhitespace(int ch)1855 private boolean isStretchableWhitespace(int ch) { 1856 // TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709). 1857 return ch == 0x0020; 1858 } 1859 1860 /* Return the number of spaces in the text line, for the purpose of justification */ countStretchableSpaces(int start, int end)1861 private int countStretchableSpaces(int start, int end) { 1862 int count = 0; 1863 for (int i = start; i < end; i++) { 1864 final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart); 1865 if (isStretchableWhitespace(c)) { 1866 count++; 1867 } 1868 } 1869 return count; 1870 } 1871 1872 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace() isLineEndSpace(char ch)1873 public static boolean isLineEndSpace(char ch) { 1874 return ch == ' ' || ch == '\t' || ch == 0x1680 1875 || (0x2000 <= ch && ch <= 0x200A && ch != 0x2007) 1876 || ch == 0x205F || ch == 0x3000; 1877 } 1878 1879 private static final int TAB_INCREMENT = 20; 1880 equalAttributes(@onNull TextPaint lp, @NonNull TextPaint rp)1881 private static boolean equalAttributes(@NonNull TextPaint lp, @NonNull TextPaint rp) { 1882 return lp.getColorFilter() == rp.getColorFilter() 1883 && lp.getMaskFilter() == rp.getMaskFilter() 1884 && lp.getShader() == rp.getShader() 1885 && lp.getTypeface() == rp.getTypeface() 1886 && lp.getXfermode() == rp.getXfermode() 1887 && lp.getTextLocales().equals(rp.getTextLocales()) 1888 && TextUtils.equals(lp.getFontFeatureSettings(), rp.getFontFeatureSettings()) 1889 && TextUtils.equals(lp.getFontVariationSettings(), rp.getFontVariationSettings()) 1890 && lp.getShadowLayerRadius() == rp.getShadowLayerRadius() 1891 && lp.getShadowLayerDx() == rp.getShadowLayerDx() 1892 && lp.getShadowLayerDy() == rp.getShadowLayerDy() 1893 && lp.getShadowLayerColor() == rp.getShadowLayerColor() 1894 && lp.getFlags() == rp.getFlags() 1895 && lp.getHinting() == rp.getHinting() 1896 && lp.getStyle() == rp.getStyle() 1897 && lp.getColor() == rp.getColor() 1898 && lp.getStrokeWidth() == rp.getStrokeWidth() 1899 && lp.getStrokeMiter() == rp.getStrokeMiter() 1900 && lp.getStrokeCap() == rp.getStrokeCap() 1901 && lp.getStrokeJoin() == rp.getStrokeJoin() 1902 && lp.getTextAlign() == rp.getTextAlign() 1903 && lp.isElegantTextHeight() == rp.isElegantTextHeight() 1904 && lp.getTextSize() == rp.getTextSize() 1905 && lp.getTextScaleX() == rp.getTextScaleX() 1906 && lp.getTextSkewX() == rp.getTextSkewX() 1907 && lp.getLetterSpacing() == rp.getLetterSpacing() 1908 && lp.getWordSpacing() == rp.getWordSpacing() 1909 && lp.getStartHyphenEdit() == rp.getStartHyphenEdit() 1910 && lp.getEndHyphenEdit() == rp.getEndHyphenEdit() 1911 && lp.bgColor == rp.bgColor 1912 && lp.baselineShift == rp.baselineShift 1913 && lp.linkColor == rp.linkColor 1914 && lp.drawableState == rp.drawableState 1915 && lp.density == rp.density 1916 && lp.underlineColor == rp.underlineColor 1917 && lp.underlineThickness == rp.underlineThickness; 1918 } 1919 } 1920