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