1 /* 2 * Copyright (C) 2006 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 static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; 20 import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; 21 import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; 22 23 import android.annotation.ColorInt; 24 import android.annotation.FlaggedApi; 25 import android.annotation.FloatRange; 26 import android.annotation.IntDef; 27 import android.annotation.IntRange; 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.annotation.SuppressLint; 31 import android.compat.annotation.UnsupportedAppUsage; 32 import android.graphics.BlendMode; 33 import android.graphics.Canvas; 34 import android.graphics.Color; 35 import android.graphics.Paint; 36 import android.graphics.Path; 37 import android.graphics.Rect; 38 import android.graphics.RectF; 39 import android.graphics.text.LineBreakConfig; 40 import android.graphics.text.LineBreaker; 41 import android.os.Build; 42 import android.text.method.TextKeyListener; 43 import android.text.style.AlignmentSpan; 44 import android.text.style.LeadingMarginSpan; 45 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; 46 import android.text.style.LineBackgroundSpan; 47 import android.text.style.ParagraphStyle; 48 import android.text.style.ReplacementSpan; 49 import android.text.style.TabStopSpan; 50 import android.widget.TextView; 51 52 import com.android.graphics.hwui.flags.Flags; 53 import com.android.internal.annotations.VisibleForTesting; 54 import com.android.internal.graphics.ColorUtils; 55 import com.android.internal.util.ArrayUtils; 56 import com.android.internal.util.GrowingArrayUtils; 57 58 import java.lang.annotation.Retention; 59 import java.lang.annotation.RetentionPolicy; 60 import java.text.BreakIterator; 61 import java.util.Arrays; 62 import java.util.List; 63 import java.util.Locale; 64 65 /** 66 * A base class that manages text layout in visual elements on 67 * the screen. 68 * <p>For text that will be edited, use a {@link DynamicLayout}, 69 * which will be updated as the text changes. 70 * For text that will not change, use a {@link StaticLayout}. 71 */ 72 @android.ravenwood.annotation.RavenwoodKeepWholeClass 73 public abstract class Layout { 74 75 // These should match the constants in framework/base/libs/hwui/hwui/DrawTextFunctor.h 76 private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX = 0f; 77 private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f; 78 // since we're not using soft light yet, this needs to be much lower than the spec'd 0.8 79 private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f; 80 @VisibleForTesting 81 static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP = 5f; 82 @VisibleForTesting 83 static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR = 0.5f; 84 85 /** @hide */ 86 @IntDef(prefix = { "BREAK_STRATEGY_" }, value = { 87 LineBreaker.BREAK_STRATEGY_SIMPLE, 88 LineBreaker.BREAK_STRATEGY_HIGH_QUALITY, 89 LineBreaker.BREAK_STRATEGY_BALANCED 90 }) 91 @Retention(RetentionPolicy.SOURCE) 92 public @interface BreakStrategy {} 93 94 /** 95 * Value for break strategy indicating simple line breaking. Automatic hyphens are not added 96 * (though soft hyphens are respected), and modifying text generally doesn't affect the layout 97 * before it (which yields a more consistent user experience when editing), but layout may not 98 * be the highest quality. 99 */ 100 public static final int BREAK_STRATEGY_SIMPLE = LineBreaker.BREAK_STRATEGY_SIMPLE; 101 102 /** 103 * Value for break strategy indicating high quality line breaking, including automatic 104 * hyphenation and doing whole-paragraph optimization of line breaks. 105 */ 106 public static final int BREAK_STRATEGY_HIGH_QUALITY = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY; 107 108 /** 109 * Value for break strategy indicating balanced line breaking. The breaks are chosen to 110 * make all lines as close to the same length as possible, including automatic hyphenation. 111 */ 112 public static final int BREAK_STRATEGY_BALANCED = LineBreaker.BREAK_STRATEGY_BALANCED; 113 114 /** @hide */ 115 @IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = { 116 HYPHENATION_FREQUENCY_NORMAL, 117 HYPHENATION_FREQUENCY_NORMAL_FAST, 118 HYPHENATION_FREQUENCY_FULL, 119 HYPHENATION_FREQUENCY_FULL_FAST, 120 HYPHENATION_FREQUENCY_NONE 121 }) 122 @Retention(RetentionPolicy.SOURCE) 123 public @interface HyphenationFrequency {} 124 125 /** 126 * Value for hyphenation frequency indicating no automatic hyphenation. Useful 127 * for backward compatibility, and for cases where the automatic hyphenation algorithm results 128 * in incorrect hyphenation. Mid-word breaks may still happen when a word is wider than the 129 * layout and there is otherwise no valid break. Soft hyphens are ignored and will not be used 130 * as suggestions for potential line breaks. 131 */ 132 public static final int HYPHENATION_FREQUENCY_NONE = 0; 133 134 /** 135 * Value for hyphenation frequency indicating a light amount of automatic hyphenation, which 136 * is a conservative default. Useful for informal cases, such as short sentences or chat 137 * messages. 138 */ 139 public static final int HYPHENATION_FREQUENCY_NORMAL = 1; 140 141 /** 142 * Value for hyphenation frequency indicating the full amount of automatic hyphenation, typical 143 * in typography. Useful for running text and where it's important to put the maximum amount of 144 * text in a screen with limited space. 145 */ 146 public static final int HYPHENATION_FREQUENCY_FULL = 2; 147 148 /** 149 * Value for hyphenation frequency indicating a light amount of automatic hyphenation with 150 * using faster algorithm. 151 * 152 * This option is useful for informal cases, such as short sentences or chat messages. To make 153 * text rendering faster with hyphenation, this algorithm ignores some hyphen character related 154 * typographic features, e.g. kerning. 155 */ 156 public static final int HYPHENATION_FREQUENCY_NORMAL_FAST = 3; 157 /** 158 * Value for hyphenation frequency indicating the full amount of automatic hyphenation with 159 * using faster algorithm. 160 * 161 * This option is useful for running text and where it's important to put the maximum amount of 162 * text in a screen with limited space. To make text rendering faster with hyphenation, this 163 * algorithm ignores some hyphen character related typographic features, e.g. kerning. 164 */ 165 public static final int HYPHENATION_FREQUENCY_FULL_FAST = 4; 166 167 private static final ParagraphStyle[] NO_PARA_SPANS = 168 ArrayUtils.emptyArray(ParagraphStyle.class); 169 170 /** @hide */ 171 @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = { 172 LineBreaker.JUSTIFICATION_MODE_NONE, 173 LineBreaker.JUSTIFICATION_MODE_INTER_WORD, 174 LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER, 175 }) 176 @Retention(RetentionPolicy.SOURCE) 177 public @interface JustificationMode {} 178 179 /** 180 * Value for justification mode indicating no justification. 181 */ 182 public static final int JUSTIFICATION_MODE_NONE = LineBreaker.JUSTIFICATION_MODE_NONE; 183 184 /** 185 * Value for justification mode indicating the text is justified by stretching word spacing. 186 */ 187 public static final int JUSTIFICATION_MODE_INTER_WORD = 188 LineBreaker.JUSTIFICATION_MODE_INTER_WORD; 189 190 /** 191 * Value for justification mode indicating the text is justified by stretching letter spacing. 192 */ 193 @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION) 194 public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 195 LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER; 196 197 /* 198 * Line spacing multiplier for default line spacing. 199 */ 200 public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f; 201 202 /* 203 * Line spacing addition for default line spacing. 204 */ 205 public static final float DEFAULT_LINESPACING_ADDITION = 0.0f; 206 207 /** 208 * Strategy which considers a text segment to be inside a rectangle area if the segment bounds 209 * intersect the rectangle. 210 */ 211 @NonNull 212 public static final TextInclusionStrategy INCLUSION_STRATEGY_ANY_OVERLAP = 213 RectF::intersects; 214 215 /** 216 * Strategy which considers a text segment to be inside a rectangle area if the center of the 217 * segment bounds is inside the rectangle. 218 */ 219 @NonNull 220 public static final TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_CENTER = 221 (segmentBounds, area) -> 222 area.contains(segmentBounds.centerX(), segmentBounds.centerY()); 223 224 /** 225 * Strategy which considers a text segment to be inside a rectangle area if the segment bounds 226 * are completely contained within the rectangle. 227 */ 228 @NonNull 229 public static final TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_ALL = 230 (segmentBounds, area) -> area.contains(segmentBounds); 231 232 /** 233 * Return how wide a layout must be in order to display the specified text with one line per 234 * paragraph. 235 * 236 * <p>As of O, Uses 237 * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In 238 * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p> 239 */ getDesiredWidth(CharSequence source, TextPaint paint)240 public static float getDesiredWidth(CharSequence source, 241 TextPaint paint) { 242 return getDesiredWidth(source, 0, source.length(), paint); 243 } 244 245 /** 246 * Return how wide a layout must be in order to display the specified text slice with one 247 * line per paragraph. 248 * 249 * <p>As of O, Uses 250 * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In 251 * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p> 252 */ getDesiredWidth(CharSequence source, int start, int end, TextPaint paint)253 public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint) { 254 return getDesiredWidth(source, start, end, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR); 255 } 256 257 /** 258 * Return how wide a layout must be in order to display the 259 * specified text slice with one line per paragraph. 260 * 261 * @hide 262 */ getDesiredWidth(CharSequence source, int start, int end, TextPaint paint, TextDirectionHeuristic textDir)263 public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint, 264 TextDirectionHeuristic textDir) { 265 return getDesiredWidthWithLimit(source, start, end, paint, textDir, Float.MAX_VALUE, false); 266 } 267 /** 268 * Return how wide a layout must be in order to display the 269 * specified text slice with one line per paragraph. 270 * 271 * If the measured width exceeds given limit, returns limit value instead. 272 * @hide 273 */ getDesiredWidthWithLimit(CharSequence source, int start, int end, TextPaint paint, TextDirectionHeuristic textDir, float upperLimit, boolean useBoundsForWidth)274 public static float getDesiredWidthWithLimit(CharSequence source, int start, int end, 275 TextPaint paint, TextDirectionHeuristic textDir, float upperLimit, 276 boolean useBoundsForWidth) { 277 float need = 0; 278 279 int next; 280 for (int i = start; i <= end; i = next) { 281 next = TextUtils.indexOf(source, '\n', i, end); 282 283 if (next < 0) 284 next = end; 285 286 // note, omits trailing paragraph char 287 float w = measurePara(paint, source, i, next, textDir, useBoundsForWidth); 288 if (w > upperLimit) { 289 return upperLimit; 290 } 291 292 if (w > need) 293 need = w; 294 295 next++; 296 } 297 298 return need; 299 } 300 301 /** 302 * Subclasses of Layout use this constructor to set the display text, 303 * width, and other standard properties. 304 * @param text the text to render 305 * @param paint the default paint for the layout. Styles can override 306 * various attributes of the paint. 307 * @param width the wrapping width for the text. 308 * @param align whether to left, right, or center the text. Styles can 309 * override the alignment. 310 * @param spacingMult factor by which to scale the font size to get the 311 * default line spacing 312 * @param spacingAdd amount to add to the default line spacing 313 */ Layout(CharSequence text, TextPaint paint, int width, Alignment align, float spacingMult, float spacingAdd)314 protected Layout(CharSequence text, TextPaint paint, 315 int width, Alignment align, 316 float spacingMult, float spacingAdd) { 317 this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, 318 spacingMult, spacingAdd, false, false, 0, null, Integer.MAX_VALUE, 319 BREAK_STRATEGY_SIMPLE, HYPHENATION_FREQUENCY_NONE, null, null, 320 JUSTIFICATION_MODE_NONE, LineBreakConfig.NONE, false, false, null); 321 } 322 323 /** 324 * Subclasses of Layout use this constructor to set the display text, 325 * width, and other standard properties. 326 * @param text the text to render 327 * @param paint the default paint for the layout. Styles can override 328 * various attributes of the paint. 329 * @param width the wrapping width for the text. 330 * @param align whether to left, right, or center the text. Styles can 331 * override the alignment. 332 * @param textDir a text direction heuristic. 333 * @param spacingMult factor by which to scale the font size to get the 334 * default line spacing 335 * @param spacingAdd amount to add to the default line spacing 336 * @param includePad true for enabling including font padding 337 * @param fallbackLineSpacing true for enabling fallback line spacing 338 * @param ellipsizedWidth width as used for ellipsizing purpose 339 * @param ellipsize an ellipsize option 340 * @param maxLines a maximum number of lines. 341 * @param breakStrategy a break strategy. 342 * @param hyphenationFrequency a hyphenation frequency 343 * @param leftIndents a visually left margins 344 * @param rightIndents a visually right margins 345 * @param justificationMode a justification mode 346 * @param lineBreakConfig a line break config 347 * 348 * @hide 349 */ Layout( CharSequence text, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingMult, float spacingAdd, boolean includePad, boolean fallbackLineSpacing, int ellipsizedWidth, TextUtils.TruncateAt ellipsize, int maxLines, int breakStrategy, int hyphenationFrequency, int[] leftIndents, int[] rightIndents, int justificationMode, LineBreakConfig lineBreakConfig, boolean useBoundsForWidth, boolean shiftDrawingOffsetForStartOverhang, Paint.FontMetrics minimumFontMetrics )350 protected Layout( 351 CharSequence text, 352 TextPaint paint, 353 int width, 354 Alignment align, 355 TextDirectionHeuristic textDir, 356 float spacingMult, 357 float spacingAdd, 358 boolean includePad, 359 boolean fallbackLineSpacing, 360 int ellipsizedWidth, 361 TextUtils.TruncateAt ellipsize, 362 int maxLines, 363 int breakStrategy, 364 int hyphenationFrequency, 365 int[] leftIndents, 366 int[] rightIndents, 367 int justificationMode, 368 LineBreakConfig lineBreakConfig, 369 boolean useBoundsForWidth, 370 boolean shiftDrawingOffsetForStartOverhang, 371 Paint.FontMetrics minimumFontMetrics 372 ) { 373 374 if (width < 0) 375 throw new IllegalArgumentException("Layout: " + width + " < 0"); 376 377 // Ensure paint doesn't have baselineShift set. 378 // While normally we don't modify the paint the user passed in, 379 // we were already doing this in Styled.drawUniformRun with both 380 // baselineShift and bgColor. We probably should reevaluate bgColor. 381 if (paint != null) { 382 paint.bgColor = 0; 383 paint.baselineShift = 0; 384 } 385 386 mText = text; 387 mPaint = paint; 388 mWidth = width; 389 mAlignment = align; 390 mSpacingMult = spacingMult; 391 mSpacingAdd = spacingAdd; 392 mSpannedText = text instanceof Spanned; 393 mTextDir = textDir; 394 mIncludePad = includePad; 395 mFallbackLineSpacing = fallbackLineSpacing; 396 mEllipsizedWidth = ellipsize == null ? width : ellipsizedWidth; 397 mEllipsize = ellipsize; 398 mMaxLines = maxLines; 399 mBreakStrategy = breakStrategy; 400 mHyphenationFrequency = hyphenationFrequency; 401 mLeftIndents = leftIndents; 402 mRightIndents = rightIndents; 403 mJustificationMode = justificationMode; 404 mLineBreakConfig = lineBreakConfig; 405 mUseBoundsForWidth = useBoundsForWidth; 406 mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang; 407 mMinimumFontMetrics = minimumFontMetrics; 408 409 initSpanColors(); 410 } 411 initSpanColors()412 private void initSpanColors() { 413 if (mSpannedText && Flags.highContrastTextSmallTextRect()) { 414 if (mSpanColors == null) { 415 mSpanColors = new SpanColors(); 416 } else { 417 mSpanColors.recycle(); 418 } 419 } else { 420 mSpanColors = null; 421 } 422 } 423 424 /** 425 * Replace constructor properties of this Layout with new ones. Be careful. 426 */ replaceWith(CharSequence text, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd)427 /* package */ void replaceWith(CharSequence text, TextPaint paint, 428 int width, Alignment align, 429 float spacingmult, float spacingadd) { 430 if (width < 0) { 431 throw new IllegalArgumentException("Layout: " + width + " < 0"); 432 } 433 434 mText = text; 435 mPaint = paint; 436 mWidth = width; 437 mAlignment = align; 438 mSpacingMult = spacingmult; 439 mSpacingAdd = spacingadd; 440 mSpannedText = text instanceof Spanned; 441 initSpanColors(); 442 } 443 444 /** 445 * Draw this Layout on the specified Canvas. 446 * 447 * This API draws background first, then draws text on top of it. 448 * 449 * @see #draw(Canvas, List, List, Path, Paint, int) 450 */ draw(Canvas c)451 public void draw(Canvas c) { 452 draw(c, (Path) null, (Paint) null, 0); 453 } 454 455 /** 456 * Draw this Layout on the specified canvas, with the highlight path drawn 457 * between the background and the text. 458 * 459 * @param canvas the canvas 460 * @param selectionHighlight the path of the selection highlight or cursor; can be null 461 * @param selectionHighlightPaint the paint for the selection highlight 462 * @param cursorOffsetVertical the amount to temporarily translate the 463 * canvas while rendering the highlight 464 * 465 * @see #draw(Canvas, List, List, Path, Paint, int) 466 */ draw( Canvas canvas, Path selectionHighlight, Paint selectionHighlightPaint, int cursorOffsetVertical)467 public void draw( 468 Canvas canvas, Path selectionHighlight, 469 Paint selectionHighlightPaint, int cursorOffsetVertical) { 470 draw(canvas, null, null, selectionHighlight, selectionHighlightPaint, cursorOffsetVertical); 471 } 472 473 /** 474 * Draw this layout on the specified canvas. 475 * 476 * This API draws background first, then draws highlight paths on top of it, then draws 477 * selection or cursor, then finally draws text on top of it. 478 * 479 * @see #drawBackground(Canvas) 480 * @see #drawText(Canvas) 481 * 482 * @param canvas the canvas 483 * @param highlightPaths the path of the highlights. The highlightPaths and highlightPaints must 484 * have the same length and aligned in the same order. For example, the 485 * paint of the n-th of the highlightPaths should be stored at the n-th of 486 * highlightPaints. 487 * @param highlightPaints the paints for the highlights. The highlightPaths and highlightPaints 488 * must have the same length and aligned in the same order. For example, 489 * the paint of the n-th of the highlightPaths should be stored at the 490 * n-th of highlightPaints. 491 * @param selectionPath the selection or cursor path 492 * @param selectionPaint the paint for the selection or cursor. 493 * @param cursorOffsetVertical the amount to temporarily translate the canvas while rendering 494 * the highlight 495 */ draw(@onNull Canvas canvas, @Nullable List<Path> highlightPaths, @Nullable List<Paint> highlightPaints, @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical)496 public void draw(@NonNull Canvas canvas, 497 @Nullable List<Path> highlightPaths, 498 @Nullable List<Paint> highlightPaints, 499 @Nullable Path selectionPath, 500 @Nullable Paint selectionPaint, 501 int cursorOffsetVertical) { 502 float leftShift = 0; 503 if (mUseBoundsForWidth && mShiftDrawingOffsetForStartOverhang) { 504 RectF drawingRect = computeDrawingBoundingBox(); 505 if (drawingRect.left < 0) { 506 leftShift = -drawingRect.left; 507 canvas.translate(leftShift, 0); 508 } 509 } 510 final long lineRange = getLineRangeForDraw(canvas); 511 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); 512 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); 513 if (lastLine < 0) return; 514 515 if (shouldDrawHighlightsOnTop(canvas)) { 516 drawBackground(canvas, firstLine, lastLine); 517 } else { 518 drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint, 519 cursorOffsetVertical, firstLine, lastLine); 520 } 521 522 drawText(canvas, firstLine, lastLine); 523 524 // Since high contrast text draws a thick border on the text, the highlight actually makes 525 // it harder to read. In this case we draw over the top of the text with a blend mode that 526 // ensures the text stays high-contrast. 527 if (shouldDrawHighlightsOnTop(canvas)) { 528 drawHighlights(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint, 529 cursorOffsetVertical, firstLine, lastLine); 530 } 531 532 if (leftShift != 0) { 533 // Manually translate back to the original position because of b/324498002, using 534 // save/restore disappears the toggle switch drawables. 535 canvas.translate(-leftShift, 0); 536 } 537 } 538 shouldDrawHighlightsOnTop(Canvas canvas)539 private static boolean shouldDrawHighlightsOnTop(Canvas canvas) { 540 return Flags.highContrastTextSmallTextRect() && canvas.isHighContrastTextEnabled(); 541 } 542 setToHighlightPaint(Paint p, BlendMode blendMode, Paint outPaint)543 private static Paint setToHighlightPaint(Paint p, BlendMode blendMode, Paint outPaint) { 544 if (p == null) return null; 545 outPaint.set(p); 546 outPaint.setBlendMode(blendMode); 547 // Yellow for maximum contrast 548 outPaint.setColor(Color.YELLOW); 549 return outPaint; 550 } 551 552 /** 553 * Draw text part of this layout. 554 * 555 * Different from {@link #draw(Canvas, List, List, Path, Paint, int)} API, this API only draws 556 * text part, not drawing highlights, selections, or backgrounds. 557 * 558 * @see #draw(Canvas, List, List, Path, Paint, int) 559 * @see #drawBackground(Canvas) 560 * 561 * @param canvas the canvas 562 */ drawText(@onNull Canvas canvas)563 public void drawText(@NonNull Canvas canvas) { 564 final long lineRange = getLineRangeForDraw(canvas); 565 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); 566 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); 567 if (lastLine < 0) return; 568 drawText(canvas, firstLine, lastLine); 569 } 570 571 /** 572 * Draw background of this layout. 573 * 574 * Different from {@link #draw(Canvas, List, List, Path, Paint, int)} API, this API only draws 575 * background, not drawing text, highlights or selections. The background here is drawn by 576 * {@link LineBackgroundSpan} attached to the text. 577 * 578 * @see #draw(Canvas, List, List, Path, Paint, int) 579 * @see #drawText(Canvas) 580 * 581 * @param canvas the canvas 582 */ drawBackground(@onNull Canvas canvas)583 public void drawBackground(@NonNull Canvas canvas) { 584 final long lineRange = getLineRangeForDraw(canvas); 585 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); 586 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); 587 if (lastLine < 0) return; 588 drawBackground(canvas, firstLine, lastLine); 589 } 590 591 /** 592 * @hide public for Editor.java 593 */ drawWithoutText( @onNull Canvas canvas, @Nullable List<Path> highlightPaths, @Nullable List<Paint> highlightPaints, @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical, int firstLine, int lastLine)594 public void drawWithoutText( 595 @NonNull Canvas canvas, 596 @Nullable List<Path> highlightPaths, 597 @Nullable List<Paint> highlightPaints, 598 @Nullable Path selectionPath, 599 @Nullable Paint selectionPaint, 600 int cursorOffsetVertical, 601 int firstLine, 602 int lastLine) { 603 drawBackground(canvas, firstLine, lastLine); 604 drawHighlights(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint, 605 cursorOffsetVertical, firstLine, lastLine); 606 } 607 608 /** 609 * @hide public for Editor.java 610 */ drawHighlights( @onNull Canvas canvas, @Nullable List<Path> highlightPaths, @Nullable List<Paint> highlightPaints, @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical, int firstLine, int lastLine)611 public void drawHighlights( 612 @NonNull Canvas canvas, 613 @Nullable List<Path> highlightPaths, 614 @Nullable List<Paint> highlightPaints, 615 @Nullable Path selectionPath, 616 @Nullable Paint selectionPaint, 617 int cursorOffsetVertical, 618 int firstLine, 619 int lastLine) { 620 if (highlightPaths == null && highlightPaints == null) { 621 return; 622 } 623 if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical); 624 try { 625 BlendMode blendMode = determineHighContrastHighlightBlendMode(canvas); 626 if (highlightPaths != null) { 627 if (highlightPaints == null) { 628 throw new IllegalArgumentException( 629 "if highlight is specified, highlightPaint must be specified."); 630 } 631 if (highlightPaints.size() != highlightPaths.size()) { 632 throw new IllegalArgumentException( 633 "The highlight path size is different from the size of highlight" 634 + " paints"); 635 } 636 for (int i = 0; i < highlightPaths.size(); ++i) { 637 final Path highlight = highlightPaths.get(i); 638 Paint highlightPaint = highlightPaints.get(i); 639 if (shouldDrawHighlightsOnTop(canvas)) { 640 highlightPaint = setToHighlightPaint(highlightPaint, blendMode, 641 mWorkPlainPaint); 642 } 643 644 if (highlight != null) { 645 canvas.drawPath(highlight, highlightPaint); 646 } 647 } 648 } 649 650 if (selectionPath != null) { 651 if (shouldDrawHighlightsOnTop(canvas)) { 652 selectionPaint = setToHighlightPaint(selectionPaint, blendMode, 653 mWorkPlainPaint); 654 } 655 canvas.drawPath(selectionPath, selectionPaint); 656 } 657 } finally { 658 if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical); 659 } 660 } 661 662 @Nullable determineHighContrastHighlightBlendMode(Canvas canvas)663 private BlendMode determineHighContrastHighlightBlendMode(Canvas canvas) { 664 if (!shouldDrawHighlightsOnTop(canvas)) { 665 return null; 666 } 667 668 return isHighContrastTextDark(mPaint.getColor()) ? BlendMode.MULTIPLY 669 : BlendMode.DIFFERENCE; 670 } 671 isHighContrastTextDark(@olorInt int color)672 private boolean isHighContrastTextDark(@ColorInt int color) { 673 // High-contrast text mode 674 // Determine if the text is black-on-white or white-on-black, so we know what blendmode will 675 // give the highest contrast and most realistic text color. 676 // LINT.IfChange(hct_darken) 677 var lab = new double[3]; 678 ColorUtils.colorToLAB(color, lab); 679 return lab[0] <= 50.0; 680 // LINT.ThenChange(/libs/hwui/hwui/DrawTextFunctor.h:hct_darken) 681 } 682 isJustificationRequired(int lineNum)683 private boolean isJustificationRequired(int lineNum) { 684 if (mJustificationMode == JUSTIFICATION_MODE_NONE) return false; 685 final int lineEnd = getLineEnd(lineNum); 686 return lineEnd < mText.length() && mText.charAt(lineEnd - 1) != '\n'; 687 } 688 getJustifyWidth(int lineNum)689 private float getJustifyWidth(int lineNum) { 690 Alignment paraAlign = mAlignment; 691 692 int left = 0; 693 int right = mWidth; 694 695 final int dir = getParagraphDirection(lineNum); 696 697 ParagraphStyle[] spans = NO_PARA_SPANS; 698 if (mSpannedText) { 699 Spanned sp = (Spanned) mText; 700 final int start = getLineStart(lineNum); 701 702 final boolean isFirstParaLine = (start == 0 || mText.charAt(start - 1) == '\n'); 703 704 if (isFirstParaLine) { 705 final int spanEnd = sp.nextSpanTransition(start, mText.length(), 706 ParagraphStyle.class); 707 spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class); 708 709 for (int n = spans.length - 1; n >= 0; n--) { 710 if (spans[n] instanceof AlignmentSpan) { 711 paraAlign = ((AlignmentSpan) spans[n]).getAlignment(); 712 break; 713 } 714 } 715 } 716 717 final int length = spans.length; 718 boolean useFirstLineMargin = isFirstParaLine; 719 for (int n = 0; n < length; n++) { 720 if (spans[n] instanceof LeadingMarginSpan2) { 721 int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount(); 722 int startLine = getLineForOffset(sp.getSpanStart(spans[n])); 723 if (lineNum < startLine + count) { 724 useFirstLineMargin = true; 725 break; 726 } 727 } 728 } 729 for (int n = 0; n < length; n++) { 730 if (spans[n] instanceof LeadingMarginSpan) { 731 LeadingMarginSpan margin = (LeadingMarginSpan) spans[n]; 732 if (dir == DIR_RIGHT_TO_LEFT) { 733 right -= margin.getLeadingMargin(useFirstLineMargin); 734 } else { 735 left += margin.getLeadingMargin(useFirstLineMargin); 736 } 737 } 738 } 739 } 740 741 final Alignment align; 742 if (paraAlign == Alignment.ALIGN_LEFT) { 743 align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE; 744 } else if (paraAlign == Alignment.ALIGN_RIGHT) { 745 align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL; 746 } else { 747 align = paraAlign; 748 } 749 750 final int indentWidth; 751 if (align == Alignment.ALIGN_NORMAL) { 752 if (dir == DIR_LEFT_TO_RIGHT) { 753 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT); 754 } else { 755 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT); 756 } 757 } else if (align == Alignment.ALIGN_OPPOSITE) { 758 if (dir == DIR_LEFT_TO_RIGHT) { 759 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT); 760 } else { 761 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT); 762 } 763 } else { // Alignment.ALIGN_CENTER 764 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER); 765 } 766 767 return right - left - indentWidth; 768 } 769 770 /** 771 * @hide 772 */ 773 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) drawText(Canvas canvas, int firstLine, int lastLine)774 public void drawText(Canvas canvas, int firstLine, int lastLine) { 775 int previousLineBottom = getLineTop(firstLine); 776 int previousLineEnd = getLineStart(firstLine); 777 ParagraphStyle[] spans = NO_PARA_SPANS; 778 int spanEnd = 0; 779 final TextPaint paint = mWorkPaint; 780 paint.set(mPaint); 781 CharSequence buf = mText; 782 783 Alignment paraAlign = mAlignment; 784 TabStops tabStops = null; 785 boolean tabStopsIsInitialized = false; 786 787 TextLine tl = TextLine.obtain(); 788 789 // Draw the lines, one at a time. 790 // The baseline is the top of the following line minus the current line's descent. 791 for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) { 792 int start = previousLineEnd; 793 previousLineEnd = getLineStart(lineNum + 1); 794 final boolean justify = isJustificationRequired(lineNum); 795 int end = getLineVisibleEnd(lineNum, start, previousLineEnd, 796 true /* trailingSpaceAtLastLineIsVisible */); 797 paint.setStartHyphenEdit(getStartHyphenEdit(lineNum)); 798 paint.setEndHyphenEdit(getEndHyphenEdit(lineNum)); 799 800 int ltop = previousLineBottom; 801 int lbottom = getLineTop(lineNum + 1); 802 previousLineBottom = lbottom; 803 int lbaseline = lbottom - getLineDescent(lineNum); 804 805 int dir = getParagraphDirection(lineNum); 806 int left = 0; 807 int right = mWidth; 808 809 if (mSpannedText) { 810 Spanned sp = (Spanned) buf; 811 int textLength = buf.length(); 812 boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n'); 813 814 // New batch of paragraph styles, collect into spans array. 815 // Compute the alignment, last alignment style wins. 816 // Reset tabStops, we'll rebuild if we encounter a line with 817 // tabs. 818 // We expect paragraph spans to be relatively infrequent, use 819 // spanEnd so that we can check less frequently. Since 820 // paragraph styles ought to apply to entire paragraphs, we can 821 // just collect the ones present at the start of the paragraph. 822 // If spanEnd is before the end of the paragraph, that's not 823 // our problem. 824 if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) { 825 spanEnd = sp.nextSpanTransition(start, textLength, 826 ParagraphStyle.class); 827 spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class); 828 829 paraAlign = mAlignment; 830 for (int n = spans.length - 1; n >= 0; n--) { 831 if (spans[n] instanceof AlignmentSpan) { 832 paraAlign = ((AlignmentSpan) spans[n]).getAlignment(); 833 break; 834 } 835 } 836 837 tabStopsIsInitialized = false; 838 } 839 840 // Draw all leading margin spans. Adjust left or right according 841 // to the paragraph direction of the line. 842 final int length = spans.length; 843 boolean useFirstLineMargin = isFirstParaLine; 844 for (int n = 0; n < length; n++) { 845 if (spans[n] instanceof LeadingMarginSpan2) { 846 int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount(); 847 int startLine = getLineForOffset(sp.getSpanStart(spans[n])); 848 // if there is more than one LeadingMarginSpan2, use 849 // the count that is greatest 850 if (lineNum < startLine + count) { 851 useFirstLineMargin = true; 852 break; 853 } 854 } 855 } 856 for (int n = 0; n < length; n++) { 857 if (spans[n] instanceof LeadingMarginSpan) { 858 LeadingMarginSpan margin = (LeadingMarginSpan) spans[n]; 859 if (dir == DIR_RIGHT_TO_LEFT) { 860 margin.drawLeadingMargin(canvas, paint, right, dir, ltop, 861 lbaseline, lbottom, buf, 862 start, end, isFirstParaLine, this); 863 right -= margin.getLeadingMargin(useFirstLineMargin); 864 } else { 865 margin.drawLeadingMargin(canvas, paint, left, dir, ltop, 866 lbaseline, lbottom, buf, 867 start, end, isFirstParaLine, this); 868 left += margin.getLeadingMargin(useFirstLineMargin); 869 } 870 } 871 } 872 } 873 874 boolean hasTab = getLineContainsTab(lineNum); 875 // Can't tell if we have tabs for sure, currently 876 if (hasTab && !tabStopsIsInitialized) { 877 if (tabStops == null) { 878 tabStops = new TabStops(TAB_INCREMENT, spans); 879 } else { 880 tabStops.reset(TAB_INCREMENT, spans); 881 } 882 tabStopsIsInitialized = true; 883 } 884 885 // Determine whether the line aligns to normal, opposite, or center. 886 Alignment align = paraAlign; 887 if (align == Alignment.ALIGN_LEFT) { 888 align = (dir == DIR_LEFT_TO_RIGHT) ? 889 Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE; 890 } else if (align == Alignment.ALIGN_RIGHT) { 891 align = (dir == DIR_LEFT_TO_RIGHT) ? 892 Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL; 893 } 894 895 int x; 896 final int indentWidth; 897 if (align == Alignment.ALIGN_NORMAL) { 898 if (dir == DIR_LEFT_TO_RIGHT) { 899 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT); 900 x = left + indentWidth; 901 } else { 902 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT); 903 x = right - indentWidth; 904 } 905 } else { 906 int max = (int)getLineExtent(lineNum, tabStops, false); 907 if (align == Alignment.ALIGN_OPPOSITE) { 908 if (dir == DIR_LEFT_TO_RIGHT) { 909 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT); 910 x = right - max - indentWidth; 911 } else { 912 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT); 913 x = left - max + indentWidth; 914 } 915 } else { // Alignment.ALIGN_CENTER 916 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER); 917 max = max & ~1; 918 x = ((right + left - max) >> 1) + indentWidth; 919 } 920 } 921 922 Directions directions = getLineDirections(lineNum); 923 if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) { 924 // XXX: assumes there's nothing additional to be done 925 canvas.drawText(buf, start, end, x, lbaseline, paint); 926 } else { 927 tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops, 928 getEllipsisStart(lineNum), 929 getEllipsisStart(lineNum) + getEllipsisCount(lineNum), 930 isFallbackLineSpacingEnabled()); 931 if (justify) { 932 tl.justify(mJustificationMode, right - left - indentWidth); 933 } 934 tl.draw(canvas, x, ltop, lbaseline, lbottom); 935 } 936 } 937 938 TextLine.recycle(tl); 939 } 940 941 /** 942 * @hide 943 */ 944 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) drawBackground( @onNull Canvas canvas, int firstLine, int lastLine)945 public void drawBackground( 946 @NonNull Canvas canvas, 947 int firstLine, int lastLine) { 948 949 drawHighContrastBackground(canvas, firstLine, lastLine); 950 951 // First, draw LineBackgroundSpans. 952 // LineBackgroundSpans know nothing about the alignment, margins, or 953 // direction of the layout or line. XXX: Should they? 954 // They are evaluated at each line. 955 if (mSpannedText) { 956 if (mLineBackgroundSpans == null) { 957 mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(LineBackgroundSpan.class); 958 } 959 960 Spanned buffer = (Spanned) mText; 961 int textLength = buffer.length(); 962 mLineBackgroundSpans.init(buffer, 0, textLength); 963 964 if (mLineBackgroundSpans.numberOfSpans > 0) { 965 int previousLineBottom = getLineTop(firstLine); 966 int previousLineEnd = getLineStart(firstLine); 967 ParagraphStyle[] spans = NO_PARA_SPANS; 968 int spansLength = 0; 969 TextPaint paint = mPaint; 970 int spanEnd = 0; 971 final int width = mWidth; 972 for (int i = firstLine; i <= lastLine; i++) { 973 int start = previousLineEnd; 974 int end = getLineStart(i + 1); 975 previousLineEnd = end; 976 977 int ltop = previousLineBottom; 978 int lbottom = getLineTop(i + 1); 979 previousLineBottom = lbottom; 980 int lbaseline = lbottom - getLineDescent(i); 981 982 if (end >= spanEnd) { 983 // These should be infrequent, so we'll use this so that 984 // we don't have to check as often. 985 spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength); 986 // All LineBackgroundSpans on a line contribute to its background. 987 spansLength = 0; 988 // Duplication of the logic of getParagraphSpans 989 if (start != end || start == 0) { 990 // Equivalent to a getSpans(start, end), but filling the 'spans' local 991 // array instead to reduce memory allocation 992 for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) { 993 // equal test is valid since both intervals are not empty by 994 // construction 995 if (mLineBackgroundSpans.spanStarts[j] >= end || 996 mLineBackgroundSpans.spanEnds[j] <= start) continue; 997 spans = GrowingArrayUtils.append( 998 spans, spansLength, mLineBackgroundSpans.spans[j]); 999 spansLength++; 1000 } 1001 } 1002 } 1003 1004 for (int n = 0; n < spansLength; n++) { 1005 LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n]; 1006 lineBackgroundSpan.drawBackground(canvas, paint, 0, width, 1007 ltop, lbaseline, lbottom, 1008 buffer, start, end, i); 1009 } 1010 } 1011 } 1012 mLineBackgroundSpans.recycle(); 1013 } 1014 } 1015 1016 /** 1017 * Draws a solid rectangle behind the text, the same color as the high contrast stroke border, 1018 * to make it even easier to read. 1019 * 1020 * <p>We draw it here instead of in DrawTextFunctor so that multiple spans don't draw 1021 * backgrounds over each other's text. 1022 */ drawHighContrastBackground(@onNull Canvas canvas, int firstLine, int lastLine)1023 private void drawHighContrastBackground(@NonNull Canvas canvas, int firstLine, int lastLine) { 1024 if (!shouldDrawHighlightsOnTop(canvas)) { 1025 return; 1026 } 1027 1028 if (!mSpannedText || mSpanColors == null) { 1029 if (mPaint.getAlpha() == 0) { 1030 return; 1031 } 1032 } 1033 1034 var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX, 1035 mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR); 1036 var cornerRadius = Math.max( 1037 mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP, 1038 mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR); 1039 1040 // We set the alpha on the color itself instead of Paint.setAlpha(), because that function 1041 // actually mutates the color in... *ehem* very strange ways. Also the color might get reset 1042 // for various reasons, which also resets the alpha. 1043 var white = Color.argb(HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE, 1f, 1f, 1f); 1044 var black = Color.argb(HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE, 0f, 0f, 0f); 1045 1046 var originalTextColor = mPaint.getColor(); 1047 var bgPaint = mWorkPlainPaint; 1048 bgPaint.reset(); 1049 bgPaint.setColor(isHighContrastTextDark(originalTextColor) ? white : black); 1050 bgPaint.setStyle(Paint.Style.FILL); 1051 1052 int start = getLineStart(firstLine); 1053 int end = getLineEnd(lastLine); 1054 // Draw a separate background rectangle for each line of text, that only surrounds the 1055 // characters on that line. But we also have to check the text color for each character, and 1056 // make sure we are drawing the correct contrasting background. This is because Spans can 1057 // change colors throughout the text and we'll need to match our backgrounds. 1058 if (mSpannedText && mSpanColors != null) { 1059 mSpanColors.init(mWorkPaint, ((Spanned) mText), start, end); 1060 } 1061 1062 forEachCharacterBounds( 1063 start, 1064 end, 1065 firstLine, 1066 lastLine, 1067 new CharacterBoundsListener() { 1068 int mLastLineNum = -1; 1069 final RectF mLineBackground = new RectF(); 1070 1071 @ColorInt int mLastColor = originalTextColor; 1072 1073 @Override 1074 public void onCharacterBounds(int index, int lineNum, float left, float top, 1075 float right, float bottom) { 1076 1077 // Skip processing if the character is a space or a tap to avoid 1078 // rendering an abrupt, empty rectangle. 1079 if (TextLine.isLineEndSpace(mText.charAt(index))) { 1080 return; 1081 } 1082 1083 var newBackground = determineContrastingBackgroundColor(index); 1084 var hasBgColorChanged = newBackground != bgPaint.getColor(); 1085 1086 // To avoid highlighting emoji sequences, we use Extended_Pictgraphs as a 1087 // heuristic. Highlighting is skipped based on code points, not glyph type 1088 // (text vs. color), so emojis with default text presentation are 1089 // intentionally not highlighted (numeric representation with emoji 1090 // presentation are manually excluded). Although we process ZWJ and 1091 // variation selectors within emoji sequences, they should not affect 1092 // highlighting due to their zero-width nature. 1093 var codePoint = Character.codePointAt(mText, index); 1094 var isEmoji = Character.isEmojiComponent(codePoint) 1095 || Character.isExtendedPictographic(codePoint); 1096 if (isEmoji && !isStandardNumber(index)) { 1097 return; 1098 } 1099 1100 if (lineNum != mLastLineNum || hasBgColorChanged) { 1101 // Draw what we have so far, then reset the rect and update its color 1102 drawRect(); 1103 mLineBackground.set(left, top, right, bottom); 1104 mLastLineNum = lineNum; 1105 1106 if (hasBgColorChanged) { 1107 bgPaint.setColor(newBackground); 1108 } 1109 } else { 1110 mLineBackground.union(left, top, right, bottom); 1111 } 1112 } 1113 1114 @Override 1115 public void onEnd() { 1116 drawRect(); 1117 } 1118 1119 private boolean isStandardNumber(int index) { 1120 var codePoint = Character.codePointAt(mText, index); 1121 var isNumberSignOrAsterisk = (codePoint >= '0' && codePoint <= '9') 1122 || codePoint == '#' || codePoint == '*'; 1123 var isColoredGlyph = index + 1 < mText.length() 1124 && Character.codePointAt(mText, index + 1) == 0xFE0F; 1125 1126 return isNumberSignOrAsterisk && !isColoredGlyph; 1127 } 1128 1129 private void drawRect() { 1130 if (!mLineBackground.isEmpty()) { 1131 mLineBackground.inset(-padding, -padding); 1132 canvas.drawRoundRect( 1133 mLineBackground, 1134 cornerRadius, 1135 cornerRadius, 1136 bgPaint 1137 ); 1138 } 1139 } 1140 1141 private int determineContrastingBackgroundColor(int index) { 1142 if (!mSpannedText || mSpanColors == null) { 1143 // The text is not Spanned. it's all one color. 1144 return bgPaint.getColor(); 1145 } 1146 1147 // Sometimes the color will change, but not enough to warrant a background 1148 // color change. e.g. from black to dark grey still gets clamped to black, 1149 // so the background stays white and we don't need to draw a fresh 1150 // background. 1151 var textColor = mSpanColors.getColorAt(index); 1152 if (textColor == SpanColors.NO_COLOR_FOUND) { 1153 textColor = originalTextColor; 1154 } 1155 var hasColorChanged = textColor != mLastColor; 1156 if (hasColorChanged) { 1157 mLastColor = textColor; 1158 1159 return isHighContrastTextDark(textColor) ? white : black; 1160 } 1161 1162 return bgPaint.getColor(); 1163 } 1164 } 1165 ); 1166 1167 if (mSpanColors != null) { 1168 mSpanColors.recycle(); 1169 } 1170 } 1171 1172 /** 1173 * @param canvas 1174 * @return The range of lines that need to be drawn, possibly empty. 1175 * @hide 1176 */ 1177 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getLineRangeForDraw(Canvas canvas)1178 public long getLineRangeForDraw(Canvas canvas) { 1179 int dtop, dbottom; 1180 1181 synchronized (sTempRect) { 1182 if (!canvas.getClipBounds(sTempRect)) { 1183 // Negative range end used as a special flag 1184 return TextUtils.packRangeInLong(0, -1); 1185 } 1186 1187 dtop = sTempRect.top; 1188 dbottom = sTempRect.bottom; 1189 } 1190 1191 final int top = Math.max(dtop, 0); 1192 final int bottom = Math.min(getLineTop(getLineCount()), dbottom); 1193 1194 if (top >= bottom) return TextUtils.packRangeInLong(0, -1); 1195 return TextUtils.packRangeInLong(getLineForVertical(top), getLineForVertical(bottom)); 1196 } 1197 1198 /** 1199 * Return the start position of the line, given the left and right bounds of the margins. 1200 * 1201 * @param line the line index 1202 * @param left the left bounds (0, or leading margin if ltr para) 1203 * @param right the right bounds (width, minus leading margin if rtl para) 1204 * @return the start position of the line (to right of line if rtl para) 1205 */ getLineStartPos(int line, int left, int right)1206 private int getLineStartPos(int line, int left, int right) { 1207 // Adjust the point at which to start rendering depending on the 1208 // alignment of the paragraph. 1209 Alignment align = getParagraphAlignment(line); 1210 int dir = getParagraphDirection(line); 1211 1212 if (align == Alignment.ALIGN_LEFT) { 1213 align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE; 1214 } else if (align == Alignment.ALIGN_RIGHT) { 1215 align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL; 1216 } 1217 1218 int x; 1219 if (align == Alignment.ALIGN_NORMAL) { 1220 if (dir == DIR_LEFT_TO_RIGHT) { 1221 x = left + getIndentAdjust(line, Alignment.ALIGN_LEFT); 1222 } else { 1223 x = right + getIndentAdjust(line, Alignment.ALIGN_RIGHT); 1224 } 1225 } else { 1226 TabStops tabStops = null; 1227 if (mSpannedText && getLineContainsTab(line)) { 1228 Spanned spanned = (Spanned) mText; 1229 int start = getLineStart(line); 1230 int spanEnd = spanned.nextSpanTransition(start, spanned.length(), 1231 TabStopSpan.class); 1232 TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd, 1233 TabStopSpan.class); 1234 if (tabSpans.length > 0) { 1235 tabStops = new TabStops(TAB_INCREMENT, tabSpans); 1236 } 1237 } 1238 int max = (int)getLineExtent(line, tabStops, false); 1239 if (align == Alignment.ALIGN_OPPOSITE) { 1240 if (dir == DIR_LEFT_TO_RIGHT) { 1241 x = right - max + getIndentAdjust(line, Alignment.ALIGN_RIGHT); 1242 } else { 1243 // max is negative here 1244 x = left - max + getIndentAdjust(line, Alignment.ALIGN_LEFT); 1245 } 1246 } else { // Alignment.ALIGN_CENTER 1247 max = max & ~1; 1248 x = (left + right - max) >> 1 + getIndentAdjust(line, Alignment.ALIGN_CENTER); 1249 } 1250 } 1251 return x; 1252 } 1253 1254 /** 1255 * Increase the width of this layout to the specified width. 1256 * Be careful to use this only when you know it is appropriate— 1257 * it does not cause the text to reflow to use the full new width. 1258 */ increaseWidthTo(int wid)1259 public final void increaseWidthTo(int wid) { 1260 if (wid < mWidth) { 1261 throw new RuntimeException("attempted to reduce Layout width"); 1262 } 1263 1264 mWidth = wid; 1265 } 1266 1267 /** 1268 * Return the total height of this layout. 1269 */ getHeight()1270 public int getHeight() { 1271 return getLineTop(getLineCount()); 1272 } 1273 1274 /** 1275 * Return the total height of this layout. 1276 * 1277 * @param cap if true and max lines is set, returns the height of the layout at the max lines. 1278 * 1279 * @hide 1280 */ getHeight(boolean cap)1281 public int getHeight(boolean cap) { 1282 return getHeight(); 1283 } 1284 1285 /** 1286 * Return the number of lines of text in this layout. 1287 */ getLineCount()1288 public abstract int getLineCount(); 1289 1290 /** 1291 * Get an actual bounding box that draws text content. 1292 * 1293 * Note that the {@link RectF#top} and {@link RectF#bottom} may be different from the 1294 * {@link Layout#getLineTop(int)} of the first line and {@link Layout#getLineBottom(int)} of 1295 * the last line. The line top and line bottom are calculated based on yMin/yMax or 1296 * ascent/descent value of font file. On the other hand, the drawing bounding boxes are 1297 * calculated based on actual glyphs used there. 1298 * 1299 * @return bounding rectangle 1300 */ 1301 @NonNull 1302 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) computeDrawingBoundingBox()1303 public RectF computeDrawingBoundingBox() { 1304 float left = 0; 1305 float right = 0; 1306 float top = 0; 1307 float bottom = 0; 1308 TextLine tl = TextLine.obtain(); 1309 RectF rectF = new RectF(); 1310 for (int line = 0; line < getLineCount(); ++line) { 1311 final int start = getLineStart(line); 1312 final int end = getLineVisibleEnd(line); 1313 1314 final boolean hasTabs = getLineContainsTab(line); 1315 TabStops tabStops = null; 1316 if (hasTabs && mText instanceof Spanned) { 1317 // Just checking this line should be good enough, tabs should be 1318 // consistent across all lines in a paragraph. 1319 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, 1320 TabStopSpan.class); 1321 if (tabs.length > 0) { 1322 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1323 } 1324 } 1325 final Directions directions = getLineDirections(line); 1326 // Returned directions can actually be null 1327 if (directions == null) { 1328 continue; 1329 } 1330 final int dir = getParagraphDirection(line); 1331 1332 final TextPaint paint = mWorkPaint; 1333 paint.set(mPaint); 1334 paint.setStartHyphenEdit(getStartHyphenEdit(line)); 1335 paint.setEndHyphenEdit(getEndHyphenEdit(line)); 1336 tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops, 1337 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 1338 isFallbackLineSpacingEnabled()); 1339 if (isJustificationRequired(line)) { 1340 tl.justify(mJustificationMode, getJustifyWidth(line)); 1341 } 1342 tl.metrics(null, rectF, false, null); 1343 1344 float lineLeft = rectF.left; 1345 float lineRight = rectF.right; 1346 float lineTop = rectF.top + getLineBaseline(line); 1347 float lineBottom = rectF.bottom + getLineBaseline(line); 1348 if (getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT) { 1349 lineLeft += getWidth(); 1350 lineRight += getWidth(); 1351 } 1352 1353 if (line == 0) { 1354 left = lineLeft; 1355 right = lineRight; 1356 top = lineTop; 1357 bottom = lineBottom; 1358 } else { 1359 left = Math.min(left, lineLeft); 1360 right = Math.max(right, lineRight); 1361 top = Math.min(top, lineTop); 1362 bottom = Math.max(bottom, lineBottom); 1363 } 1364 } 1365 TextLine.recycle(tl); 1366 return new RectF(left, top, right, bottom); 1367 } 1368 1369 /** 1370 * Return the baseline for the specified line (0…getLineCount() - 1) 1371 * If bounds is not null, return the top, left, right, bottom extents 1372 * of the specified line in it. 1373 * @param line which line to examine (0..getLineCount() - 1) 1374 * @param bounds Optional. If not null, it returns the extent of the line 1375 * @return the Y-coordinate of the baseline 1376 */ getLineBounds(int line, Rect bounds)1377 public int getLineBounds(int line, Rect bounds) { 1378 if (bounds != null) { 1379 bounds.left = 0; // ??? 1380 bounds.top = getLineTop(line); 1381 bounds.right = mWidth; // ??? 1382 bounds.bottom = getLineTop(line + 1); 1383 } 1384 return getLineBaseline(line); 1385 } 1386 1387 /** 1388 * Return the vertical position of the top of the specified line 1389 * (0…getLineCount()). 1390 * If the specified line is equal to the line count, returns the 1391 * bottom of the last line. 1392 */ getLineTop(int line)1393 public abstract int getLineTop(int line); 1394 1395 /** 1396 * Return the descent of the specified line(0…getLineCount() - 1). 1397 */ getLineDescent(int line)1398 public abstract int getLineDescent(int line); 1399 1400 /** 1401 * Return the text offset of the beginning of the specified line ( 1402 * 0…getLineCount()). If the specified line is equal to the line 1403 * count, returns the length of the text. 1404 */ getLineStart(int line)1405 public abstract int getLineStart(int line); 1406 1407 /** 1408 * Returns the primary directionality of the paragraph containing the 1409 * specified line, either 1 for left-to-right lines, or -1 for right-to-left 1410 * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}). 1411 */ getParagraphDirection(int line)1412 public abstract int getParagraphDirection(int line); 1413 1414 /** 1415 * Returns whether the specified line contains one or more 1416 * characters that need to be handled specially, like tabs. 1417 */ getLineContainsTab(int line)1418 public abstract boolean getLineContainsTab(int line); 1419 1420 /** 1421 * Returns the directional run information for the specified line. 1422 * The array alternates counts of characters in left-to-right 1423 * and right-to-left segments of the line. 1424 * 1425 * <p>NOTE: this is inadequate to support bidirectional text, and will change. 1426 */ getLineDirections(int line)1427 public abstract Directions getLineDirections(int line); 1428 1429 /** 1430 * Returns the (negative) number of extra pixels of ascent padding in the 1431 * top line of the Layout. 1432 */ getTopPadding()1433 public abstract int getTopPadding(); 1434 1435 /** 1436 * Returns the number of extra pixels of descent padding in the 1437 * bottom line of the Layout. 1438 */ getBottomPadding()1439 public abstract int getBottomPadding(); 1440 1441 /** 1442 * Returns the start hyphen edit for a line. 1443 * 1444 * @hide 1445 */ getStartHyphenEdit(int line)1446 public @Paint.StartHyphenEdit int getStartHyphenEdit(int line) { 1447 return Paint.START_HYPHEN_EDIT_NO_EDIT; 1448 } 1449 1450 /** 1451 * Returns the end hyphen edit for a line. 1452 * 1453 * @hide 1454 */ getEndHyphenEdit(int line)1455 public @Paint.EndHyphenEdit int getEndHyphenEdit(int line) { 1456 return Paint.END_HYPHEN_EDIT_NO_EDIT; 1457 } 1458 1459 /** 1460 * Returns the left indent for a line. 1461 * 1462 * @hide 1463 */ getIndentAdjust(int line, Alignment alignment)1464 public int getIndentAdjust(int line, Alignment alignment) { 1465 return 0; 1466 } 1467 1468 /** 1469 * Returns true if the character at offset and the preceding character 1470 * are at different run levels (and thus there's a split caret). 1471 * @param offset the offset 1472 * @return true if at a level boundary 1473 * @hide 1474 */ 1475 @UnsupportedAppUsage isLevelBoundary(int offset)1476 public boolean isLevelBoundary(int offset) { 1477 int line = getLineForOffset(offset); 1478 Directions dirs = getLineDirections(line); 1479 if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) { 1480 return false; 1481 } 1482 1483 int[] runs = dirs.mDirections; 1484 int lineStart = getLineStart(line); 1485 int lineEnd = getLineEnd(line); 1486 if (offset == lineStart || offset == lineEnd) { 1487 int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1; 1488 int runIndex = offset == lineStart ? 0 : runs.length - 2; 1489 return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel; 1490 } 1491 1492 offset -= lineStart; 1493 for (int i = 0; i < runs.length; i += 2) { 1494 if (offset == runs[i]) { 1495 return true; 1496 } 1497 } 1498 return false; 1499 } 1500 1501 /** 1502 * Returns true if the character at offset is right to left (RTL). 1503 * @param offset the offset 1504 * @return true if the character is RTL, false if it is LTR 1505 */ isRtlCharAt(int offset)1506 public boolean isRtlCharAt(int offset) { 1507 int line = getLineForOffset(offset); 1508 Directions dirs = getLineDirections(line); 1509 if (dirs == DIRS_ALL_LEFT_TO_RIGHT) { 1510 return false; 1511 } 1512 if (dirs == DIRS_ALL_RIGHT_TO_LEFT) { 1513 return true; 1514 } 1515 int[] runs = dirs.mDirections; 1516 int lineStart = getLineStart(line); 1517 for (int i = 0; i < runs.length; i += 2) { 1518 int start = lineStart + runs[i]; 1519 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 1520 if (offset >= start && offset < limit) { 1521 int level = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 1522 return ((level & 1) != 0); 1523 } 1524 } 1525 // Should happen only if the offset is "out of bounds" 1526 return false; 1527 } 1528 1529 /** 1530 * Returns the range of the run that the character at offset belongs to. 1531 * @param offset the offset 1532 * @return The range of the run 1533 * @hide 1534 */ getRunRange(int offset)1535 public long getRunRange(int offset) { 1536 int line = getLineForOffset(offset); 1537 Directions dirs = getLineDirections(line); 1538 if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) { 1539 return TextUtils.packRangeInLong(0, getLineEnd(line)); 1540 } 1541 int[] runs = dirs.mDirections; 1542 int lineStart = getLineStart(line); 1543 for (int i = 0; i < runs.length; i += 2) { 1544 int start = lineStart + runs[i]; 1545 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 1546 if (offset >= start && offset < limit) { 1547 return TextUtils.packRangeInLong(start, limit); 1548 } 1549 } 1550 // Should happen only if the offset is "out of bounds" 1551 return TextUtils.packRangeInLong(0, getLineEnd(line)); 1552 } 1553 1554 /** 1555 * Checks if the trailing BiDi level should be used for an offset 1556 * 1557 * This method is useful when the offset is at the BiDi level transition point and determine 1558 * which run need to be used. For example, let's think about following input: (L* denotes 1559 * Left-to-Right characters, R* denotes Right-to-Left characters.) 1560 * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6 1561 * Input (Display Order): L1 L2 L3 R3 R2 R1 L4 L5 L6 1562 * 1563 * Then, think about selecting the range (3, 6). The offset=3 and offset=6 are ambiguous here 1564 * since they are at the BiDi transition point. In Android, the offset is considered to be 1565 * associated with the trailing run if the BiDi level of the trailing run is higher than of the 1566 * previous run. In this case, the BiDi level of the input text is as follows: 1567 * 1568 * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6 1569 * BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ] 1570 * BiDi Level: 0 0 0 1 1 1 0 0 0 1571 * 1572 * Thus, offset = 3 is part of Run 1 and this method returns true for offset = 3, since the BiDi 1573 * level of Run 1 is higher than the level of Run 0. Similarly, the offset = 6 is a part of Run 1574 * 1 and this method returns false for the offset = 6 since the BiDi level of Run 1 is higher 1575 * than the level of Run 2. 1576 * 1577 * @returns true if offset is at the BiDi level transition point and trailing BiDi level is 1578 * higher than previous BiDi level. See above for the detail. 1579 * @hide 1580 */ 1581 @VisibleForTesting primaryIsTrailingPrevious(int offset)1582 public boolean primaryIsTrailingPrevious(int offset) { 1583 int line = getLineForOffset(offset); 1584 int lineStart = getLineStart(line); 1585 int lineEnd = getLineEnd(line); 1586 int[] runs = getLineDirections(line).mDirections; 1587 1588 int levelAt = -1; 1589 for (int i = 0; i < runs.length; i += 2) { 1590 int start = lineStart + runs[i]; 1591 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 1592 if (limit > lineEnd) { 1593 limit = lineEnd; 1594 } 1595 if (offset >= start && offset < limit) { 1596 if (offset > start) { 1597 // Previous character is at same level, so don't use trailing. 1598 return false; 1599 } 1600 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 1601 break; 1602 } 1603 } 1604 if (levelAt == -1) { 1605 // Offset was limit of line. 1606 levelAt = getParagraphDirection(line) == 1 ? 0 : 1; 1607 } 1608 1609 // At level boundary, check previous level. 1610 int levelBefore = -1; 1611 if (offset == lineStart) { 1612 levelBefore = getParagraphDirection(line) == 1 ? 0 : 1; 1613 } else { 1614 offset -= 1; 1615 for (int i = 0; i < runs.length; i += 2) { 1616 int start = lineStart + runs[i]; 1617 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 1618 if (limit > lineEnd) { 1619 limit = lineEnd; 1620 } 1621 if (offset >= start && offset < limit) { 1622 levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 1623 break; 1624 } 1625 } 1626 } 1627 1628 return levelBefore < levelAt; 1629 } 1630 1631 /** 1632 * Computes in linear time the results of calling 1633 * #primaryIsTrailingPrevious for all offsets on a line. 1634 * @param line The line giving the offsets we compute the information for 1635 * @return The array of results, indexed from 0, where 0 corresponds to the line start offset 1636 * @hide 1637 */ 1638 @VisibleForTesting primaryIsTrailingPreviousAllLineOffsets(int line)1639 public boolean[] primaryIsTrailingPreviousAllLineOffsets(int line) { 1640 int lineStart = getLineStart(line); 1641 int lineEnd = getLineEnd(line); 1642 int[] runs = getLineDirections(line).mDirections; 1643 1644 boolean[] trailing = new boolean[lineEnd - lineStart + 1]; 1645 1646 byte[] level = new byte[lineEnd - lineStart + 1]; 1647 for (int i = 0; i < runs.length; i += 2) { 1648 int start = lineStart + runs[i]; 1649 int limit = start + (runs[i + 1] & RUN_LENGTH_MASK); 1650 if (limit > lineEnd) { 1651 limit = lineEnd; 1652 } 1653 if (limit == start) { 1654 continue; 1655 } 1656 level[limit - lineStart - 1] = 1657 (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK); 1658 } 1659 1660 for (int i = 0; i < runs.length; i += 2) { 1661 int start = lineStart + runs[i]; 1662 byte currentLevel = (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK); 1663 trailing[start - lineStart] = currentLevel > (start == lineStart 1664 ? (getParagraphDirection(line) == 1 ? 0 : 1) 1665 : level[start - lineStart - 1]); 1666 } 1667 1668 return trailing; 1669 } 1670 1671 /** 1672 * Get the primary horizontal position for the specified text offset. 1673 * This is the location where a new character would be inserted in 1674 * the paragraph's primary direction. 1675 */ getPrimaryHorizontal(int offset)1676 public float getPrimaryHorizontal(int offset) { 1677 return getPrimaryHorizontal(offset, false /* not clamped */); 1678 } 1679 1680 /** 1681 * Get the primary horizontal position for the specified text offset, but 1682 * optionally clamp it so that it doesn't exceed the width of the layout. 1683 * @hide 1684 */ 1685 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getPrimaryHorizontal(int offset, boolean clamped)1686 public float getPrimaryHorizontal(int offset, boolean clamped) { 1687 boolean trailing = primaryIsTrailingPrevious(offset); 1688 return getHorizontal(offset, trailing, clamped); 1689 } 1690 1691 /** 1692 * Get the secondary horizontal position for the specified text offset. 1693 * This is the location where a new character would be inserted in 1694 * the direction other than the paragraph's primary direction. 1695 */ getSecondaryHorizontal(int offset)1696 public float getSecondaryHorizontal(int offset) { 1697 return getSecondaryHorizontal(offset, false /* not clamped */); 1698 } 1699 1700 /** 1701 * Get the secondary horizontal position for the specified text offset, but 1702 * optionally clamp it so that it doesn't exceed the width of the layout. 1703 * @hide 1704 */ 1705 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getSecondaryHorizontal(int offset, boolean clamped)1706 public float getSecondaryHorizontal(int offset, boolean clamped) { 1707 boolean trailing = primaryIsTrailingPrevious(offset); 1708 return getHorizontal(offset, !trailing, clamped); 1709 } 1710 getHorizontal(int offset, boolean primary)1711 private float getHorizontal(int offset, boolean primary) { 1712 return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset); 1713 } 1714 getHorizontal(int offset, boolean trailing, boolean clamped)1715 private float getHorizontal(int offset, boolean trailing, boolean clamped) { 1716 int line = getLineForOffset(offset); 1717 1718 return getHorizontal(offset, trailing, line, clamped); 1719 } 1720 getHorizontal(int offset, boolean trailing, int line, boolean clamped)1721 private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) { 1722 int start = getLineStart(line); 1723 int end = getLineEnd(line); 1724 int dir = getParagraphDirection(line); 1725 boolean hasTab = getLineContainsTab(line); 1726 Directions directions = getLineDirections(line); 1727 1728 TabStops tabStops = null; 1729 if (hasTab && mText instanceof Spanned) { 1730 // Just checking this line should be good enough, tabs should be 1731 // consistent across all lines in a paragraph. 1732 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 1733 if (tabs.length > 0) { 1734 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1735 } 1736 } 1737 1738 TextLine tl = TextLine.obtain(); 1739 tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops, 1740 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 1741 isFallbackLineSpacingEnabled()); 1742 float wid = tl.measure(offset - start, trailing, null, null, null); 1743 TextLine.recycle(tl); 1744 1745 if (clamped && wid > mWidth) { 1746 wid = mWidth; 1747 } 1748 int left = getParagraphLeft(line); 1749 int right = getParagraphRight(line); 1750 1751 return getLineStartPos(line, left, right) + wid; 1752 } 1753 1754 /** 1755 * Computes in linear time the results of calling #getHorizontal for all offsets on a line. 1756 * 1757 * @param line The line giving the offsets we compute information for 1758 * @param clamped Whether to clamp the results to the width of the layout 1759 * @param primary Whether the results should be the primary or the secondary horizontal 1760 * @return The array of results, indexed from 0, where 0 corresponds to the line start offset 1761 */ getLineHorizontals(int line, boolean clamped, boolean primary)1762 private float[] getLineHorizontals(int line, boolean clamped, boolean primary) { 1763 int start = getLineStart(line); 1764 int end = getLineEnd(line); 1765 int dir = getParagraphDirection(line); 1766 boolean hasTab = getLineContainsTab(line); 1767 Directions directions = getLineDirections(line); 1768 1769 TabStops tabStops = null; 1770 if (hasTab && mText instanceof Spanned) { 1771 // Just checking this line should be good enough, tabs should be 1772 // consistent across all lines in a paragraph. 1773 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 1774 if (tabs.length > 0) { 1775 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1776 } 1777 } 1778 1779 TextLine tl = TextLine.obtain(); 1780 tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops, 1781 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 1782 isFallbackLineSpacingEnabled()); 1783 boolean[] trailings = primaryIsTrailingPreviousAllLineOffsets(line); 1784 if (!primary) { 1785 for (int offset = 0; offset < trailings.length; ++offset) { 1786 trailings[offset] = !trailings[offset]; 1787 } 1788 } 1789 float[] wid = tl.measureAllOffsets(trailings, null); 1790 TextLine.recycle(tl); 1791 1792 if (clamped) { 1793 for (int offset = 0; offset < wid.length; ++offset) { 1794 if (wid[offset] > mWidth) { 1795 wid[offset] = mWidth; 1796 } 1797 } 1798 } 1799 int left = getParagraphLeft(line); 1800 int right = getParagraphRight(line); 1801 1802 int lineStartPos = getLineStartPos(line, left, right); 1803 float[] horizontal = new float[end - start + 1]; 1804 for (int offset = 0; offset < horizontal.length; ++offset) { 1805 horizontal[offset] = lineStartPos + wid[offset]; 1806 } 1807 return horizontal; 1808 } 1809 fillHorizontalBoundsForLine(int line, float[] horizontalBounds)1810 private void fillHorizontalBoundsForLine(int line, float[] horizontalBounds) { 1811 final int lineStart = getLineStart(line); 1812 final int lineEnd = getLineEnd(line); 1813 final int lineLength = lineEnd - lineStart; 1814 1815 final int dir = getParagraphDirection(line); 1816 final Directions directions = getLineDirections(line); 1817 1818 final boolean hasTab = getLineContainsTab(line); 1819 TabStops tabStops = null; 1820 if (hasTab && mText instanceof Spanned) { 1821 // Just checking this line should be good enough, tabs should be 1822 // consistent across all lines in a paragraph. 1823 TabStopSpan[] tabs = 1824 getParagraphSpans((Spanned) mText, lineStart, lineEnd, TabStopSpan.class); 1825 if (tabs.length > 0) { 1826 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1827 } 1828 } 1829 1830 final TextLine tl = TextLine.obtain(); 1831 tl.set(mPaint, mText, lineStart, lineEnd, dir, directions, hasTab, tabStops, 1832 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 1833 isFallbackLineSpacingEnabled()); 1834 if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) { 1835 horizontalBounds = new float[2 * lineLength]; 1836 } 1837 1838 tl.measureAllBounds(horizontalBounds, null); 1839 TextLine.recycle(tl); 1840 } 1841 1842 /** 1843 * Return the characters' bounds in the given range. The {@code bounds} array will be filled 1844 * starting from {@code boundsStart} (inclusive). The coordinates are in local text layout. 1845 * 1846 * @param start the start index to compute the character bounds, inclusive. 1847 * @param end the end index to compute the character bounds, exclusive. 1848 * @param bounds the array to fill in the character bounds. The array is divided into segments 1849 * of four where each index in that segment represents left, top, right and 1850 * bottom of the character. 1851 * @param boundsStart the inclusive start index in the array to start filling in the values 1852 * from. 1853 * 1854 * @throws IndexOutOfBoundsException if the range defined by {@code start} and {@code end} 1855 * exceeds the range of the text, or {@code bounds} doesn't have enough space to store the 1856 * result. 1857 * @throws IllegalArgumentException if {@code bounds} is null. 1858 */ fillCharacterBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull float[] bounds, @IntRange(from = 0) int boundsStart)1859 public void fillCharacterBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 1860 @NonNull float[] bounds, @IntRange(from = 0) int boundsStart) { 1861 if (start < 0 || end < start || end > mText.length()) { 1862 throw new IndexOutOfBoundsException("given range: " + start + ", " + end + " is " 1863 + "out of the text range: 0, " + mText.length()); 1864 } 1865 1866 if (bounds == null) { 1867 throw new IllegalArgumentException("bounds can't be null."); 1868 } 1869 1870 final int neededLength = 4 * (end - start); 1871 if (neededLength > bounds.length - boundsStart) { 1872 throw new IndexOutOfBoundsException("bounds doesn't have enough space to store the " 1873 + "result, needed: " + neededLength + " had: " 1874 + (bounds.length - boundsStart)); 1875 } 1876 1877 if (start == end) { 1878 return; 1879 } 1880 1881 final int startLine = getLineForOffset(start); 1882 final int endLine = getLineForOffset(end - 1); 1883 1884 forEachCharacterBounds(start, end, startLine, endLine, 1885 (index, lineNum, left, lineTop, right, lineBottom) -> { 1886 final int boundsIndex = boundsStart + 4 * (index - start); 1887 bounds[boundsIndex] = left; 1888 bounds[boundsIndex + 1] = lineTop; 1889 bounds[boundsIndex + 2] = right; 1890 bounds[boundsIndex + 3] = lineBottom; 1891 }); 1892 } 1893 1894 /** 1895 * Return the characters' bounds in the given range. The coordinates are in local text layout. 1896 * 1897 * @param start the start index to compute the character bounds, inclusive. 1898 * @param end the end index to compute the character bounds, exclusive. 1899 * @param startLine index of the line that contains {@code start} 1900 * @param endLine index of the line that contains {@code end} 1901 * @param listener called for each character with its bounds 1902 * 1903 */ forEachCharacterBounds( @ntRangefrom = 0) int start, @IntRange(from = 0) int end, @IntRange(from = 0) int startLine, @IntRange(from = 0) int endLine, CharacterBoundsListener listener )1904 private void forEachCharacterBounds( 1905 @IntRange(from = 0) int start, 1906 @IntRange(from = 0) int end, 1907 @IntRange(from = 0) int startLine, 1908 @IntRange(from = 0) int endLine, 1909 CharacterBoundsListener listener 1910 ) { 1911 float[] horizontalBounds = null; 1912 for (int line = startLine; line <= endLine; ++line) { 1913 final int lineStart = getLineStart(line); 1914 final int lineEnd = getLineEnd(line); 1915 final int lineLength = lineEnd - lineStart; 1916 if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) { 1917 horizontalBounds = new float[2 * lineLength]; 1918 } 1919 fillHorizontalBoundsForLine(line, horizontalBounds); 1920 1921 final int lineLeft = getParagraphLeft(line); 1922 final int lineRight = getParagraphRight(line); 1923 final int lineStartPos = getLineStartPos(line, lineLeft, lineRight); 1924 1925 final int lineTop = getLineTop(line); 1926 final int lineBottom = getLineBottom(line); 1927 1928 final int startIndex = Math.max(start, lineStart); 1929 final int endIndex = Math.min(end, lineEnd); 1930 for (int index = startIndex; index < endIndex; ++index) { 1931 final int offset = index - lineStart; 1932 final float left = horizontalBounds[offset * 2] + lineStartPos; 1933 final float right = horizontalBounds[offset * 2 + 1] + lineStartPos; 1934 1935 listener.onCharacterBounds(index, line, left, lineTop, right, lineBottom); 1936 } 1937 } 1938 listener.onEnd(); 1939 } 1940 1941 /** 1942 * Get the leftmost position that should be exposed for horizontal 1943 * scrolling on the specified line. 1944 */ getLineLeft(int line)1945 public float getLineLeft(int line) { 1946 final int dir = getParagraphDirection(line); 1947 Alignment align = getParagraphAlignment(line); 1948 // Before Q, StaticLayout.Builder.setAlignment didn't check whether the input alignment 1949 // is null. And when it is null, the old behavior is the same as ALIGN_CENTER. 1950 // To keep consistency, we convert a null alignment to ALIGN_CENTER. 1951 if (align == null) { 1952 align = Alignment.ALIGN_CENTER; 1953 } 1954 1955 // First convert combinations of alignment and direction settings to 1956 // three basic cases: ALIGN_LEFT, ALIGN_RIGHT and ALIGN_CENTER. 1957 // For unexpected cases, it will fallback to ALIGN_LEFT. 1958 final Alignment resultAlign; 1959 switch(align) { 1960 case ALIGN_NORMAL: 1961 resultAlign = 1962 dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_RIGHT : Alignment.ALIGN_LEFT; 1963 break; 1964 case ALIGN_OPPOSITE: 1965 resultAlign = 1966 dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_LEFT : Alignment.ALIGN_RIGHT; 1967 break; 1968 case ALIGN_CENTER: 1969 resultAlign = Alignment.ALIGN_CENTER; 1970 break; 1971 case ALIGN_RIGHT: 1972 resultAlign = Alignment.ALIGN_RIGHT; 1973 break; 1974 default: /* align == Alignment.ALIGN_LEFT */ 1975 resultAlign = Alignment.ALIGN_LEFT; 1976 } 1977 1978 // Here we must use getLineMax() to do the computation, because it maybe overridden by 1979 // derived class. And also note that line max equals the width of the text in that line 1980 // plus the leading margin. 1981 switch (resultAlign) { 1982 case ALIGN_CENTER: 1983 final int left = getParagraphLeft(line); 1984 final float max = getLineMax(line); 1985 // This computation only works when mWidth equals leadingMargin plus 1986 // the width of text in this line. If this condition doesn't meet anymore, 1987 // please change here too. 1988 return (float) Math.floor(left + (mWidth - max) / 2); 1989 case ALIGN_RIGHT: 1990 return mWidth - getLineMax(line); 1991 default: /* resultAlign == Alignment.ALIGN_LEFT */ 1992 return 0; 1993 } 1994 } 1995 1996 /** 1997 * Get the rightmost position that should be exposed for horizontal 1998 * scrolling on the specified line. 1999 */ getLineRight(int line)2000 public float getLineRight(int line) { 2001 final int dir = getParagraphDirection(line); 2002 Alignment align = getParagraphAlignment(line); 2003 // Before Q, StaticLayout.Builder.setAlignment didn't check whether the input alignment 2004 // is null. And when it is null, the old behavior is the same as ALIGN_CENTER. 2005 // To keep consistency, we convert a null alignment to ALIGN_CENTER. 2006 if (align == null) { 2007 align = Alignment.ALIGN_CENTER; 2008 } 2009 2010 final Alignment resultAlign; 2011 switch(align) { 2012 case ALIGN_NORMAL: 2013 resultAlign = 2014 dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_RIGHT : Alignment.ALIGN_LEFT; 2015 break; 2016 case ALIGN_OPPOSITE: 2017 resultAlign = 2018 dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_LEFT : Alignment.ALIGN_RIGHT; 2019 break; 2020 case ALIGN_CENTER: 2021 resultAlign = Alignment.ALIGN_CENTER; 2022 break; 2023 case ALIGN_RIGHT: 2024 resultAlign = Alignment.ALIGN_RIGHT; 2025 break; 2026 default: /* align == Alignment.ALIGN_LEFT */ 2027 resultAlign = Alignment.ALIGN_LEFT; 2028 } 2029 2030 switch (resultAlign) { 2031 case ALIGN_CENTER: 2032 final int right = getParagraphRight(line); 2033 final float max = getLineMax(line); 2034 // This computation only works when mWidth equals leadingMargin plus width of the 2035 // text in this line. If this condition doesn't meet anymore, please change here. 2036 return (float) Math.ceil(right - (mWidth - max) / 2); 2037 case ALIGN_RIGHT: 2038 return mWidth; 2039 default: /* resultAlign == Alignment.ALIGN_LEFT */ 2040 return getLineMax(line); 2041 } 2042 } 2043 2044 /** 2045 * Gets the unsigned horizontal extent of the specified line, including 2046 * leading margin indent, but excluding trailing whitespace. 2047 */ getLineMax(int line)2048 public float getLineMax(int line) { 2049 float margin = getParagraphLeadingMargin(line); 2050 float signedExtent = getLineExtent(line, false); 2051 return margin + (signedExtent >= 0 ? signedExtent : -signedExtent); 2052 } 2053 2054 /** 2055 * Gets the unsigned horizontal extent of the specified line, including 2056 * leading margin indent and trailing whitespace. 2057 */ getLineWidth(int line)2058 public float getLineWidth(int line) { 2059 float margin = getParagraphLeadingMargin(line); 2060 float signedExtent = getLineExtent(line, true); 2061 return margin + (signedExtent >= 0 ? signedExtent : -signedExtent); 2062 } 2063 2064 /** 2065 * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the 2066 * tab stops instead of using the ones passed in. 2067 * @param line the index of the line 2068 * @param full whether to include trailing whitespace 2069 * @return the extent of the line 2070 */ getLineExtent(int line, boolean full)2071 private float getLineExtent(int line, boolean full) { 2072 final int start = getLineStart(line); 2073 final int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 2074 2075 final boolean hasTabs = getLineContainsTab(line); 2076 TabStops tabStops = null; 2077 if (hasTabs && mText instanceof Spanned) { 2078 // Just checking this line should be good enough, tabs should be 2079 // consistent across all lines in a paragraph. 2080 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 2081 if (tabs.length > 0) { 2082 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 2083 } 2084 } 2085 final Directions directions = getLineDirections(line); 2086 // Returned directions can actually be null 2087 if (directions == null) { 2088 return 0f; 2089 } 2090 final int dir = getParagraphDirection(line); 2091 2092 final TextLine tl = TextLine.obtain(); 2093 final TextPaint paint = mWorkPaint; 2094 paint.set(mPaint); 2095 paint.setStartHyphenEdit(getStartHyphenEdit(line)); 2096 paint.setEndHyphenEdit(getEndHyphenEdit(line)); 2097 tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops, 2098 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 2099 isFallbackLineSpacingEnabled()); 2100 if (isJustificationRequired(line)) { 2101 tl.justify(mJustificationMode, getJustifyWidth(line)); 2102 } 2103 final float width = tl.metrics(null, null, mUseBoundsForWidth, null); 2104 TextLine.recycle(tl); 2105 return width; 2106 } 2107 2108 /** 2109 * Returns the number of letter spacing unit in the line. 2110 * 2111 * <p> 2112 * This API returns a number of letters that is a target of letter spacing. The letter spacing 2113 * won't be added to the middle of the characters that are needed to be treated as a single, 2114 * e.g., ligatured or conjunct form. Note that this value is different from the number of] 2115 * grapheme clusters that is calculated by {@link BreakIterator#getCharacterInstance(Locale)}. 2116 * For example, if the "fi" is ligatured, the ligatured form is treated as single uni and letter 2117 * spacing is not added, but it has two separate grapheme cluster. 2118 * 2119 * <p> 2120 * This value is used for calculating the letter spacing amount for the justification because 2121 * the letter spacing is applied between clusters. For example, if extra {@code W} pixels needed 2122 * to be filled by letter spacing, the amount of letter spacing to be applied is 2123 * {@code W}/(letter spacing unit count - 1) px. 2124 * 2125 * @param line the index of the line 2126 * @param includeTrailingWhitespace whether to include trailing whitespace 2127 * @return the number of cluster count in the line. 2128 */ 2129 @IntRange(from = 0) 2130 @FlaggedApi(FLAG_LETTER_SPACING_JUSTIFICATION) getLineLetterSpacingUnitCount(@ntRangefrom = 0) int line, boolean includeTrailingWhitespace)2131 public int getLineLetterSpacingUnitCount(@IntRange(from = 0) int line, 2132 boolean includeTrailingWhitespace) { 2133 final int start = getLineStart(line); 2134 final int end = includeTrailingWhitespace ? getLineEnd(line) 2135 : getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1), 2136 false // trailingSpaceAtLastLineIsVisible: Treating trailing whitespaces at 2137 // the last line as a invisible chars for single line justification. 2138 ); 2139 2140 final Directions directions = getLineDirections(line); 2141 // Returned directions can actually be null 2142 if (directions == null) { 2143 return 0; 2144 } 2145 final int dir = getParagraphDirection(line); 2146 2147 final TextLine tl = TextLine.obtain(); 2148 final TextPaint paint = mWorkPaint; 2149 paint.set(mPaint); 2150 paint.setStartHyphenEdit(getStartHyphenEdit(line)); 2151 paint.setEndHyphenEdit(getEndHyphenEdit(line)); 2152 tl.set(paint, mText, start, end, dir, directions, 2153 false, null, // tab width is not used for cluster counting. 2154 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 2155 isFallbackLineSpacingEnabled()); 2156 if (mLineInfo == null) { 2157 mLineInfo = new TextLine.LineInfo(); 2158 } 2159 mLineInfo.setClusterCount(0); 2160 tl.metrics(null, null, mUseBoundsForWidth, mLineInfo); 2161 TextLine.recycle(tl); 2162 return mLineInfo.getClusterCount(); 2163 } 2164 2165 /** 2166 * Returns the signed horizontal extent of the specified line, excluding 2167 * leading margin. If full is false, excludes trailing whitespace. 2168 * @param line the index of the line 2169 * @param tabStops the tab stops, can be null if we know they're not used. 2170 * @param full whether to include trailing whitespace 2171 * @return the extent of the text on this line 2172 */ getLineExtent(int line, TabStops tabStops, boolean full)2173 private float getLineExtent(int line, TabStops tabStops, boolean full) { 2174 final int start = getLineStart(line); 2175 final int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 2176 final boolean hasTabs = getLineContainsTab(line); 2177 final Directions directions = getLineDirections(line); 2178 final int dir = getParagraphDirection(line); 2179 2180 final TextLine tl = TextLine.obtain(); 2181 final TextPaint paint = mWorkPaint; 2182 paint.set(mPaint); 2183 paint.setStartHyphenEdit(getStartHyphenEdit(line)); 2184 paint.setEndHyphenEdit(getEndHyphenEdit(line)); 2185 tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops, 2186 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 2187 isFallbackLineSpacingEnabled()); 2188 if (isJustificationRequired(line)) { 2189 tl.justify(mJustificationMode, getJustifyWidth(line)); 2190 } 2191 final float width = tl.metrics(null, null, mUseBoundsForWidth, null); 2192 TextLine.recycle(tl); 2193 return width; 2194 } 2195 2196 /** 2197 * Get the line number corresponding to the specified vertical position. 2198 * If you ask for a position above 0, you get 0; if you ask for a position 2199 * below the bottom of the text, you get the last line. 2200 */ 2201 // FIXME: It may be faster to do a linear search for layouts without many lines. getLineForVertical(int vertical)2202 public int getLineForVertical(int vertical) { 2203 int high = getLineCount(), low = -1, guess; 2204 2205 while (high - low > 1) { 2206 guess = (high + low) / 2; 2207 2208 if (getLineTop(guess) > vertical) 2209 high = guess; 2210 else 2211 low = guess; 2212 } 2213 2214 if (low < 0) 2215 return 0; 2216 else 2217 return low; 2218 } 2219 2220 /** 2221 * Get the line number on which the specified text offset appears. 2222 * If you ask for a position before 0, you get 0; if you ask for a position 2223 * beyond the end of the text, you get the last line. 2224 */ getLineForOffset(int offset)2225 public int getLineForOffset(int offset) { 2226 int high = getLineCount(), low = -1, guess; 2227 2228 while (high - low > 1) { 2229 guess = (high + low) / 2; 2230 2231 if (getLineStart(guess) > offset) 2232 high = guess; 2233 else 2234 low = guess; 2235 } 2236 2237 if (low < 0) { 2238 return 0; 2239 } else { 2240 return low; 2241 } 2242 } 2243 2244 /** 2245 * Get the character offset on the specified line whose position is 2246 * closest to the specified horizontal position. 2247 */ getOffsetForHorizontal(int line, float horiz)2248 public int getOffsetForHorizontal(int line, float horiz) { 2249 return getOffsetForHorizontal(line, horiz, true); 2250 } 2251 2252 /** 2253 * Get the character offset on the specified line whose position is 2254 * closest to the specified horizontal position. 2255 * 2256 * @param line the line used to find the closest offset 2257 * @param horiz the horizontal position used to find the closest offset 2258 * @param primary whether to use the primary position or secondary position to find the offset 2259 * 2260 * @hide 2261 */ getOffsetForHorizontal(int line, float horiz, boolean primary)2262 public int getOffsetForHorizontal(int line, float horiz, boolean primary) { 2263 // TODO: use Paint.getOffsetForAdvance to avoid binary search 2264 final int lineEndOffset = getLineEnd(line); 2265 final int lineStartOffset = getLineStart(line); 2266 2267 Directions dirs = getLineDirections(line); 2268 2269 TextLine tl = TextLine.obtain(); 2270 // XXX: we don't care about tabs as we just use TextLine#getOffsetToLeftRightOf here. 2271 tl.set(mPaint, mText, lineStartOffset, lineEndOffset, getParagraphDirection(line), dirs, 2272 false, null, 2273 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 2274 isFallbackLineSpacingEnabled()); 2275 final HorizontalMeasurementProvider horizontal = 2276 new HorizontalMeasurementProvider(line, primary); 2277 2278 final int max; 2279 if (line == getLineCount() - 1) { 2280 max = lineEndOffset; 2281 } else { 2282 max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset, 2283 !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset; 2284 } 2285 int best = lineStartOffset; 2286 float bestdist = Math.abs(horizontal.get(lineStartOffset) - horiz); 2287 2288 for (int i = 0; i < dirs.mDirections.length; i += 2) { 2289 int here = lineStartOffset + dirs.mDirections[i]; 2290 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 2291 boolean isRtl = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0; 2292 int swap = isRtl ? -1 : 1; 2293 2294 if (there > max) 2295 there = max; 2296 int high = there - 1 + 1, low = here + 1 - 1, guess; 2297 2298 while (high - low > 1) { 2299 guess = (high + low) / 2; 2300 int adguess = getOffsetAtStartOf(guess); 2301 2302 if (horizontal.get(adguess) * swap >= horiz * swap) { 2303 high = guess; 2304 } else { 2305 low = guess; 2306 } 2307 } 2308 2309 if (low < here + 1) 2310 low = here + 1; 2311 2312 if (low < there) { 2313 int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset; 2314 low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset; 2315 if (low >= here && low < there) { 2316 float dist = Math.abs(horizontal.get(low) - horiz); 2317 if (aft < there) { 2318 float other = Math.abs(horizontal.get(aft) - horiz); 2319 2320 if (other < dist) { 2321 dist = other; 2322 low = aft; 2323 } 2324 } 2325 2326 if (dist < bestdist) { 2327 bestdist = dist; 2328 best = low; 2329 } 2330 } 2331 } 2332 2333 float dist = Math.abs(horizontal.get(here) - horiz); 2334 2335 if (dist < bestdist) { 2336 bestdist = dist; 2337 best = here; 2338 } 2339 } 2340 2341 float dist = Math.abs(horizontal.get(max) - horiz); 2342 2343 if (dist <= bestdist) { 2344 best = max; 2345 } 2346 2347 TextLine.recycle(tl); 2348 return best; 2349 } 2350 2351 /** 2352 * Responds to #getHorizontal queries, by selecting the better strategy between: 2353 * - calling #getHorizontal explicitly for each query 2354 * - precomputing all #getHorizontal measurements, and responding to any query in constant time 2355 * The first strategy is used for LTR-only text, while the second is used for all other cases. 2356 * The class is currently only used in #getOffsetForHorizontal, so reuse with care in other 2357 * contexts. 2358 */ 2359 private class HorizontalMeasurementProvider { 2360 private final int mLine; 2361 private final boolean mPrimary; 2362 2363 private float[] mHorizontals; 2364 private int mLineStartOffset; 2365 HorizontalMeasurementProvider(final int line, final boolean primary)2366 HorizontalMeasurementProvider(final int line, final boolean primary) { 2367 mLine = line; 2368 mPrimary = primary; 2369 init(); 2370 } 2371 init()2372 private void init() { 2373 final Directions dirs = getLineDirections(mLine); 2374 if (dirs == DIRS_ALL_LEFT_TO_RIGHT) { 2375 return; 2376 } 2377 2378 mHorizontals = getLineHorizontals(mLine, false, mPrimary); 2379 mLineStartOffset = getLineStart(mLine); 2380 } 2381 get(final int offset)2382 float get(final int offset) { 2383 final int index = offset - mLineStartOffset; 2384 if (mHorizontals == null || index < 0 || index >= mHorizontals.length) { 2385 return getHorizontal(offset, mPrimary); 2386 } else { 2387 return mHorizontals[index]; 2388 } 2389 } 2390 } 2391 2392 /** 2393 * Finds the range of text which is inside the specified rectangle area. The start of the range 2394 * is the start of the first text segment inside the area, and the end of the range is the end 2395 * of the last text segment inside the area. 2396 * 2397 * <p>A text segment is considered to be inside the area according to the provided {@link 2398 * TextInclusionStrategy}. If a text segment spans multiple lines or multiple directional runs 2399 * (e.g. a hyphenated word), the text segment is divided into pieces at the line and run breaks, 2400 * then the text segment is considered to be inside the area if any of its pieces are inside the 2401 * area. 2402 * 2403 * <p>The returned range may also include text segments which are not inside the specified area, 2404 * if those text segments are in between text segments which are inside the area. For example, 2405 * the returned range may be "segment1 segment2 segment3" if "segment1" and "segment3" are 2406 * inside the area and "segment2" is not. 2407 * 2408 * @param area area for which the text range will be found 2409 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a 2410 * text segment 2411 * @param inclusionStrategy strategy for determining whether a text segment is inside the 2412 * specified area 2413 * @return int array of size 2 containing the start (inclusive) and end (exclusive) character 2414 * offsets of the text range, or null if there are no text segments inside the area 2415 */ 2416 @Nullable getRangeForRect(@onNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy)2417 public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder, 2418 @NonNull TextInclusionStrategy inclusionStrategy) { 2419 // Find the first line whose bottom (without line spacing) is below the top of the area. 2420 int startLine = getLineForVertical((int) area.top); 2421 if (area.top > getLineBottom(startLine, /* includeLineSpacing= */ false)) { 2422 startLine++; 2423 if (startLine >= getLineCount()) { 2424 // The entire area is below the last line, so it does not contain any text. 2425 return null; 2426 } 2427 } 2428 2429 // Find the last line whose top is above the bottom of the area. 2430 int endLine = getLineForVertical((int) area.bottom); 2431 if (endLine == 0 && area.bottom < getLineTop(0)) { 2432 // The entire area is above the first line, so it does not contain any text. 2433 return null; 2434 } 2435 if (endLine < startLine) { 2436 // The entire area is between two lines, so it does not contain any text. 2437 return null; 2438 } 2439 2440 int start = getStartOrEndOffsetForAreaWithinLine( 2441 startLine, area, segmentFinder, inclusionStrategy, /* getStart= */ true); 2442 // If the area does not contain any text on this line, keep trying subsequent lines until 2443 // the end line is reached. 2444 while (start == -1 && startLine < endLine) { 2445 startLine++; 2446 start = getStartOrEndOffsetForAreaWithinLine( 2447 startLine, area, segmentFinder, inclusionStrategy, /* getStart= */ true); 2448 } 2449 if (start == -1) { 2450 // All lines were checked, the area does not contain any text. 2451 return null; 2452 } 2453 2454 int end = getStartOrEndOffsetForAreaWithinLine( 2455 endLine, area, segmentFinder, inclusionStrategy, /* getStart= */ false); 2456 // If the area does not contain any text on this line, keep trying previous lines until 2457 // the start line is reached. 2458 while (end == -1 && startLine < endLine) { 2459 endLine--; 2460 end = getStartOrEndOffsetForAreaWithinLine( 2461 endLine, area, segmentFinder, inclusionStrategy, /* getStart= */ false); 2462 } 2463 if (end == -1) { 2464 // All lines were checked, the area does not contain any text. 2465 return null; 2466 } 2467 2468 // If a text segment spans multiple lines or multiple directional runs (e.g. a hyphenated 2469 // word), then getStartOrEndOffsetForAreaWithinLine() can return an offset in the middle of 2470 // a text segment. Adjust the range to include the rest of any partial text segments. If 2471 // start is already the start boundary of a text segment, then this is a no-op. 2472 start = segmentFinder.previousStartBoundary(start + 1); 2473 end = segmentFinder.nextEndBoundary(end - 1); 2474 2475 return new int[] {start, end}; 2476 } 2477 2478 /** 2479 * Finds the start character offset of the first text segment within a line inside the specified 2480 * rectangle area, or the end character offset of the last text segment inside the area. 2481 * 2482 * @param line index of the line to search 2483 * @param area area inside which text segments will be found 2484 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a 2485 * text segment 2486 * @param inclusionStrategy strategy for determining whether a text segment is inside the 2487 * specified area 2488 * @param getStart true to find the start of the first text segment inside the area, false to 2489 * find the end of the last text segment 2490 * @return the start character offset of the first text segment inside the area, or the end 2491 * character offset of the last text segment inside the area. 2492 */ getStartOrEndOffsetForAreaWithinLine( @ntRangefrom = 0) int line, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy, boolean getStart)2493 private int getStartOrEndOffsetForAreaWithinLine( 2494 @IntRange(from = 0) int line, 2495 @NonNull RectF area, 2496 @NonNull SegmentFinder segmentFinder, 2497 @NonNull TextInclusionStrategy inclusionStrategy, 2498 boolean getStart) { 2499 int lineTop = getLineTop(line); 2500 int lineBottom = getLineBottom(line, /* includeLineSpacing= */ false); 2501 2502 int lineStartOffset = getLineStart(line); 2503 int lineEndOffset = getLineEnd(line); 2504 if (lineStartOffset == lineEndOffset) { 2505 return -1; 2506 } 2507 2508 float[] horizontalBounds = new float[2 * (lineEndOffset - lineStartOffset)]; 2509 fillHorizontalBoundsForLine(line, horizontalBounds); 2510 2511 int lineStartPos = getLineStartPos(line, getParagraphLeft(line), getParagraphRight(line)); 2512 2513 // Loop through the runs forwards or backwards depending on getStart value. 2514 Layout.Directions directions = getLineDirections(line); 2515 int runIndex = getStart ? 0 : directions.getRunCount() - 1; 2516 while ((getStart && runIndex < directions.getRunCount()) || (!getStart && runIndex >= 0)) { 2517 // runStartOffset and runEndOffset are offset indices within the line. 2518 int runStartOffset = directions.getRunStart(runIndex); 2519 int runEndOffset = Math.min( 2520 runStartOffset + directions.getRunLength(runIndex), 2521 lineEndOffset - lineStartOffset); 2522 boolean isRtl = directions.isRunRtl(runIndex); 2523 float runLeft = lineStartPos 2524 + (isRtl 2525 ? horizontalBounds[2 * (runEndOffset - 1)] 2526 : horizontalBounds[2 * runStartOffset]); 2527 float runRight = lineStartPos 2528 + (isRtl 2529 ? horizontalBounds[2 * runStartOffset + 1] 2530 : horizontalBounds[2 * (runEndOffset - 1) + 1]); 2531 2532 int result = 2533 getStart 2534 ? getStartOffsetForAreaWithinRun( 2535 area, lineTop, lineBottom, 2536 lineStartOffset, lineStartPos, horizontalBounds, 2537 runStartOffset, runEndOffset, runLeft, runRight, isRtl, 2538 segmentFinder, inclusionStrategy) 2539 : getEndOffsetForAreaWithinRun( 2540 area, lineTop, lineBottom, 2541 lineStartOffset, lineStartPos, horizontalBounds, 2542 runStartOffset, runEndOffset, runLeft, runRight, isRtl, 2543 segmentFinder, inclusionStrategy); 2544 if (result >= 0) { 2545 return result; 2546 } 2547 2548 runIndex += getStart ? 1 : -1; 2549 } 2550 return -1; 2551 } 2552 2553 /** 2554 * Finds the start character offset of the first text segment within a directional run inside 2555 * the specified rectangle area. 2556 * 2557 * @param area area inside which text segments will be found 2558 * @param lineTop top of the line containing this run 2559 * @param lineBottom bottom (not including line spacing) of the line containing this run 2560 * @param lineStartOffset start character offset of the line containing this run 2561 * @param lineStartPos start position of the line containing this run 2562 * @param horizontalBounds array containing the signed horizontal bounds of the characters in 2563 * the line. The left and right bounds of the character at offset i are stored at index (2 * 2564 * i) and index (2 * i + 1). Bounds are relative to {@code lineStartPos}. 2565 * @param runStartOffset start offset of the run relative to {@code lineStartOffset} 2566 * @param runEndOffset end offset of the run relative to {@code lineStartOffset} 2567 * @param runLeft left bound of the run 2568 * @param runRight right bound of the run 2569 * @param isRtl whether the run is right-to-left 2570 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a 2571 * text segment 2572 * @param inclusionStrategy strategy for determining whether a text segment is inside the 2573 * specified area 2574 * @return the start character offset of the first text segment inside the area 2575 */ getStartOffsetForAreaWithinRun( @onNull RectF area, int lineTop, int lineBottom, @IntRange(from = 0) int lineStartOffset, @IntRange(from = 0) int lineStartPos, @NonNull float[] horizontalBounds, @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset, float runLeft, float runRight, boolean isRtl, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy)2576 private static int getStartOffsetForAreaWithinRun( 2577 @NonNull RectF area, 2578 int lineTop, int lineBottom, 2579 @IntRange(from = 0) int lineStartOffset, 2580 @IntRange(from = 0) int lineStartPos, 2581 @NonNull float[] horizontalBounds, 2582 @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset, 2583 float runLeft, float runRight, 2584 boolean isRtl, 2585 @NonNull SegmentFinder segmentFinder, 2586 @NonNull TextInclusionStrategy inclusionStrategy) { 2587 if (runRight < area.left || runLeft > area.right) { 2588 // The run does not overlap the area. 2589 return -1; 2590 } 2591 2592 // Find the first character in the run whose bounds overlap with the area. 2593 // firstCharOffset is an offset index within the line. 2594 int firstCharOffset; 2595 if ((!isRtl && area.left <= runLeft) || (isRtl && area.right >= runRight)) { 2596 firstCharOffset = runStartOffset; 2597 } else { 2598 int low = runStartOffset; 2599 int high = runEndOffset; 2600 int guess; 2601 while (high - low > 1) { 2602 guess = (high + low) / 2; 2603 // Left edge of the character at guess 2604 float pos = lineStartPos + horizontalBounds[2 * guess]; 2605 if ((!isRtl && pos > area.left) || (isRtl && pos < area.right)) { 2606 high = guess; 2607 } else { 2608 low = guess; 2609 } 2610 } 2611 // The area edge is between the left edge of the character at low and the left edge of 2612 // the character at high. For LTR text, this is within the character at low. For RTL 2613 // text, this is within the character at high. 2614 firstCharOffset = isRtl ? high : low; 2615 } 2616 2617 // Find the first text segment containing this character (or, if no text segment contains 2618 // this character, the first text segment after this character). All previous text segments 2619 // in this run are to the left (for LTR) of the area. 2620 int segmentEndOffset = 2621 segmentFinder.nextEndBoundary(lineStartOffset + firstCharOffset); 2622 if (segmentEndOffset == SegmentFinder.DONE) { 2623 // There are no text segments containing or after firstCharOffset, so no text segments 2624 // in this run overlap the area. 2625 return -1; 2626 } 2627 int segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset); 2628 if (segmentStartOffset >= lineStartOffset + runEndOffset) { 2629 // The text segment is after the end of this run, so no text segments in this run 2630 // overlap the area. 2631 return -1; 2632 } 2633 // If the segment extends outside of this run, only consider the piece of the segment within 2634 // this run. 2635 segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset); 2636 segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset); 2637 2638 RectF segmentBounds = new RectF(0, lineTop, 0, lineBottom); 2639 while (true) { 2640 // Start (left for LTR, right for RTL) edge of the character at segmentStartOffset. 2641 float segmentStart = lineStartPos + horizontalBounds[ 2642 2 * (segmentStartOffset - lineStartOffset) + (isRtl ? 1 : 0)]; 2643 if ((!isRtl && segmentStart > area.right) || (isRtl && segmentStart < area.left)) { 2644 // The entire area is to the left (for LTR) of the text segment. So the area does 2645 // not contain any text segments within this run. 2646 return -1; 2647 } 2648 // End (right for LTR, left for RTL) edge of the character at (segmentStartOffset - 1). 2649 float segmentEnd = lineStartPos + horizontalBounds[ 2650 2 * (segmentEndOffset - lineStartOffset - 1) + (isRtl ? 0 : 1)]; 2651 segmentBounds.left = isRtl ? segmentEnd : segmentStart; 2652 segmentBounds.right = isRtl ? segmentStart : segmentEnd; 2653 if (inclusionStrategy.isSegmentInside(segmentBounds, area)) { 2654 return segmentStartOffset; 2655 } 2656 // Try the next text segment. 2657 segmentStartOffset = segmentFinder.nextStartBoundary(segmentStartOffset); 2658 if (segmentStartOffset == SegmentFinder.DONE 2659 || segmentStartOffset >= lineStartOffset + runEndOffset) { 2660 // No more text segments within this run. 2661 return -1; 2662 } 2663 segmentEndOffset = segmentFinder.nextEndBoundary(segmentStartOffset); 2664 // If the segment extends past the end of this run, only consider the piece of the 2665 // segment within this run. 2666 segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset); 2667 } 2668 } 2669 2670 /** 2671 * Finds the end character offset of the last text segment within a directional run inside the 2672 * specified rectangle area. 2673 * 2674 * @param area area inside which text segments will be found 2675 * @param lineTop top of the line containing this run 2676 * @param lineBottom bottom (not including line spacing) of the line containing this run 2677 * @param lineStartOffset start character offset of the line containing this run 2678 * @param lineStartPos start position of the line containing this run 2679 * @param horizontalBounds array containing the signed horizontal bounds of the characters in 2680 * the line. The left and right bounds of the character at offset i are stored at index (2 * 2681 * i) and index (2 * i + 1). Bounds are relative to {@code lineStartPos}. 2682 * @param runStartOffset start offset of the run relative to {@code lineStartOffset} 2683 * @param runEndOffset end offset of the run relative to {@code lineStartOffset} 2684 * @param runLeft left bound of the run 2685 * @param runRight right bound of the run 2686 * @param isRtl whether the run is right-to-left 2687 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a 2688 * text segment 2689 * @param inclusionStrategy strategy for determining whether a text segment is inside the 2690 * specified area 2691 * @return the end character offset of the last text segment inside the area 2692 */ getEndOffsetForAreaWithinRun( @onNull RectF area, int lineTop, int lineBottom, @IntRange(from = 0) int lineStartOffset, @IntRange(from = 0) int lineStartPos, @NonNull float[] horizontalBounds, @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset, float runLeft, float runRight, boolean isRtl, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy)2693 private static int getEndOffsetForAreaWithinRun( 2694 @NonNull RectF area, 2695 int lineTop, int lineBottom, 2696 @IntRange(from = 0) int lineStartOffset, 2697 @IntRange(from = 0) int lineStartPos, 2698 @NonNull float[] horizontalBounds, 2699 @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset, 2700 float runLeft, float runRight, 2701 boolean isRtl, 2702 @NonNull SegmentFinder segmentFinder, 2703 @NonNull TextInclusionStrategy inclusionStrategy) { 2704 if (runRight < area.left || runLeft > area.right) { 2705 // The run does not overlap the area. 2706 return -1; 2707 } 2708 2709 // Find the last character in the run whose bounds overlap with the area. 2710 // firstCharOffset is an offset index within the line. 2711 int lastCharOffset; 2712 if ((!isRtl && area.right >= runRight) || (isRtl && area.left <= runLeft)) { 2713 lastCharOffset = runEndOffset - 1; 2714 } else { 2715 int low = runStartOffset; 2716 int high = runEndOffset; 2717 int guess; 2718 while (high - low > 1) { 2719 guess = (high + low) / 2; 2720 // Left edge of the character at guess 2721 float pos = lineStartPos + horizontalBounds[2 * guess]; 2722 if ((!isRtl && pos > area.right) || (isRtl && pos < area.left)) { 2723 high = guess; 2724 } else { 2725 low = guess; 2726 } 2727 } 2728 // The area edge is between the left edge of the character at low and the left edge of 2729 // the character at high. For LTR text, this is within the character at low. For RTL 2730 // text, this is within the character at high. 2731 lastCharOffset = isRtl ? high : low; 2732 } 2733 2734 // Find the last text segment containing this character (or, if no text segment contains 2735 // this character, the first text segment before this character). All following text 2736 // segments in this run are to the right (for LTR) of the area. 2737 // + 1 to allow segmentStartOffset = lineStartOffset + lastCharOffset 2738 int segmentStartOffset = 2739 segmentFinder.previousStartBoundary(lineStartOffset + lastCharOffset + 1); 2740 if (segmentStartOffset == SegmentFinder.DONE) { 2741 // There are no text segments containing or before lastCharOffset, so no text segments 2742 // in this run overlap the area. 2743 return -1; 2744 } 2745 int segmentEndOffset = segmentFinder.nextEndBoundary(segmentStartOffset); 2746 if (segmentEndOffset <= lineStartOffset + runStartOffset) { 2747 // The text segment is before the start of this run, so no text segments in this run 2748 // overlap the area. 2749 return -1; 2750 } 2751 // If the segment extends outside of this run, only consider the piece of the segment within 2752 // this run. 2753 segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset); 2754 segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset); 2755 2756 RectF segmentBounds = new RectF(0, lineTop, 0, lineBottom); 2757 while (true) { 2758 // End (right for LTR, left for RTL) edge of the character at (segmentStartOffset - 1). 2759 float segmentEnd = lineStartPos + horizontalBounds[ 2760 2 * (segmentEndOffset - lineStartOffset - 1) + (isRtl ? 0 : 1)]; 2761 if ((!isRtl && segmentEnd < area.left) || (isRtl && segmentEnd > area.right)) { 2762 // The entire area is to the right (for LTR) of the text segment. So the 2763 // area does not contain any text segments within this run. 2764 return -1; 2765 } 2766 // Start (left for LTR, right for RTL) edge of the character at segmentStartOffset. 2767 float segmentStart = lineStartPos + horizontalBounds[ 2768 2 * (segmentStartOffset - lineStartOffset) + (isRtl ? 1 : 0)]; 2769 segmentBounds.left = isRtl ? segmentEnd : segmentStart; 2770 segmentBounds.right = isRtl ? segmentStart : segmentEnd; 2771 if (inclusionStrategy.isSegmentInside(segmentBounds, area)) { 2772 return segmentEndOffset; 2773 } 2774 // Try the previous text segment. 2775 segmentEndOffset = segmentFinder.previousEndBoundary(segmentEndOffset); 2776 if (segmentEndOffset == SegmentFinder.DONE 2777 || segmentEndOffset <= lineStartOffset + runStartOffset) { 2778 // No more text segments within this run. 2779 return -1; 2780 } 2781 segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset); 2782 // If the segment extends past the start of this run, only consider the piece of the 2783 // segment within this run. 2784 segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset); 2785 } 2786 } 2787 2788 /** 2789 * Return the text offset after the last character on the specified line. 2790 */ getLineEnd(int line)2791 public final int getLineEnd(int line) { 2792 return getLineStart(line + 1); 2793 } 2794 2795 /** 2796 * Return the text offset after the last visible character (so whitespace 2797 * is not counted) on the specified line. 2798 */ getLineVisibleEnd(int line)2799 public int getLineVisibleEnd(int line) { 2800 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1), 2801 true /* trailingSpaceAtLastLineIsVisible */); 2802 } 2803 getLineVisibleEnd(int line, int start, int end, boolean trailingSpaceAtLastLineIsVisible)2804 private int getLineVisibleEnd(int line, int start, int end, 2805 boolean trailingSpaceAtLastLineIsVisible) { 2806 CharSequence text = mText; 2807 char ch; 2808 2809 // Historically, trailing spaces at the last line is counted as visible. However, this 2810 // doesn't work well for justification. 2811 if (trailingSpaceAtLastLineIsVisible) { 2812 if (line == getLineCount() - 1) { 2813 return end; 2814 } 2815 } 2816 2817 for (; end > start; end--) { 2818 ch = text.charAt(end - 1); 2819 2820 if (ch == '\n') { 2821 return end - 1; 2822 } 2823 2824 if (!TextLine.isLineEndSpace(ch)) { 2825 break; 2826 } 2827 2828 } 2829 2830 return end; 2831 } 2832 2833 /** 2834 * Return the vertical position of the bottom of the specified line. 2835 */ getLineBottom(int line)2836 public final int getLineBottom(int line) { 2837 return getLineBottom(line, /* includeLineSpacing= */ true); 2838 } 2839 2840 /** 2841 * Return the vertical position of the bottom of the specified line. 2842 * 2843 * @param line index of the line 2844 * @param includeLineSpacing whether to include the line spacing 2845 */ getLineBottom(int line, boolean includeLineSpacing)2846 public int getLineBottom(int line, boolean includeLineSpacing) { 2847 if (includeLineSpacing) { 2848 return getLineTop(line + 1); 2849 } else { 2850 return getLineTop(line + 1) - getLineExtra(line); 2851 } 2852 } 2853 2854 /** 2855 * Return the vertical position of the baseline of the specified line. 2856 */ getLineBaseline(int line)2857 public final int getLineBaseline(int line) { 2858 // getLineTop(line+1) == getLineBottom(line) 2859 return getLineTop(line+1) - getLineDescent(line); 2860 } 2861 2862 /** 2863 * Get the ascent of the text on the specified line. 2864 * The return value is negative to match the Paint.ascent() convention. 2865 */ getLineAscent(int line)2866 public final int getLineAscent(int line) { 2867 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) 2868 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); 2869 } 2870 2871 /** 2872 * Return the extra space added as a result of line spacing attributes 2873 * {@link #getSpacingAdd()} and {@link #getSpacingMultiplier()}. Default value is {@code zero}. 2874 * 2875 * @param line the index of the line, the value should be equal or greater than {@code zero} 2876 * @hide 2877 */ getLineExtra(@ntRangefrom = 0) int line)2878 public int getLineExtra(@IntRange(from = 0) int line) { 2879 return 0; 2880 } 2881 getOffsetToLeftOf(int offset)2882 public int getOffsetToLeftOf(int offset) { 2883 return getOffsetToLeftRightOf(offset, true); 2884 } 2885 getOffsetToRightOf(int offset)2886 public int getOffsetToRightOf(int offset) { 2887 return getOffsetToLeftRightOf(offset, false); 2888 } 2889 getOffsetToLeftRightOf(int caret, boolean toLeft)2890 private int getOffsetToLeftRightOf(int caret, boolean toLeft) { 2891 int line = getLineForOffset(caret); 2892 int lineStart = getLineStart(line); 2893 int lineEnd = getLineEnd(line); 2894 int lineDir = getParagraphDirection(line); 2895 2896 boolean lineChanged = false; 2897 boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); 2898 // if walking off line, look at the line we're headed to 2899 if (advance) { 2900 if (caret == lineEnd) { 2901 if (line < getLineCount() - 1) { 2902 lineChanged = true; 2903 ++line; 2904 } else { 2905 return caret; // at very end, don't move 2906 } 2907 } 2908 } else { 2909 if (caret == lineStart) { 2910 if (line > 0) { 2911 lineChanged = true; 2912 --line; 2913 } else { 2914 return caret; // at very start, don't move 2915 } 2916 } 2917 } 2918 2919 if (lineChanged) { 2920 lineStart = getLineStart(line); 2921 lineEnd = getLineEnd(line); 2922 int newDir = getParagraphDirection(line); 2923 if (newDir != lineDir) { 2924 // unusual case. we want to walk onto the line, but it runs 2925 // in a different direction than this one, so we fake movement 2926 // in the opposite direction. 2927 toLeft = !toLeft; 2928 lineDir = newDir; 2929 } 2930 } 2931 2932 Directions directions = getLineDirections(line); 2933 2934 TextLine tl = TextLine.obtain(); 2935 // XXX: we don't care about tabs 2936 tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null, 2937 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 2938 isFallbackLineSpacingEnabled()); 2939 caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); 2940 TextLine.recycle(tl); 2941 return caret; 2942 } 2943 getOffsetAtStartOf(int offset)2944 private int getOffsetAtStartOf(int offset) { 2945 // XXX this probably should skip local reorderings and 2946 // zero-width characters, look at callers 2947 if (offset == 0) 2948 return 0; 2949 2950 CharSequence text = mText; 2951 char c = text.charAt(offset); 2952 2953 if (c >= '\uDC00' && c <= '\uDFFF') { 2954 char c1 = text.charAt(offset - 1); 2955 2956 if (c1 >= '\uD800' && c1 <= '\uDBFF') 2957 offset -= 1; 2958 } 2959 2960 if (mSpannedText) { 2961 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 2962 ReplacementSpan.class); 2963 2964 for (int i = 0; i < spans.length; i++) { 2965 int start = ((Spanned) text).getSpanStart(spans[i]); 2966 int end = ((Spanned) text).getSpanEnd(spans[i]); 2967 2968 if (start < offset && end > offset) 2969 offset = start; 2970 } 2971 } 2972 2973 return offset; 2974 } 2975 2976 /** 2977 * Determine whether we should clamp cursor position. Currently it's 2978 * only robust for left-aligned displays. 2979 * @hide 2980 */ 2981 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) shouldClampCursor(int line)2982 public boolean shouldClampCursor(int line) { 2983 // Only clamp cursor position in left-aligned displays. 2984 switch (getParagraphAlignment(line)) { 2985 case ALIGN_LEFT: 2986 return true; 2987 case ALIGN_NORMAL: 2988 return getParagraphDirection(line) > 0; 2989 default: 2990 return false; 2991 } 2992 2993 } 2994 2995 /** 2996 * Fills in the specified Path with a representation of a cursor 2997 * at the specified offset. This will often be a vertical line 2998 * but can be multiple discontinuous lines in text with multiple 2999 * directionalities. 3000 */ getCursorPath(final int point, final Path dest, final CharSequence editingBuffer)3001 public void getCursorPath(final int point, final Path dest, final CharSequence editingBuffer) { 3002 dest.reset(); 3003 3004 int line = getLineForOffset(point); 3005 int top = getLineTop(line); 3006 int bottom = getLineBottom(line, /* includeLineSpacing= */ false); 3007 3008 boolean clamped = shouldClampCursor(line); 3009 float h1 = getPrimaryHorizontal(point, clamped) - 0.5f; 3010 3011 int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | 3012 TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); 3013 int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON); 3014 int dist = 0; 3015 3016 if (caps != 0 || fn != 0) { 3017 dist = (bottom - top) >> 2; 3018 3019 if (fn != 0) 3020 top += dist; 3021 if (caps != 0) 3022 bottom -= dist; 3023 } 3024 3025 if (h1 < 0.5f) 3026 h1 = 0.5f; 3027 3028 dest.moveTo(h1, top); 3029 dest.lineTo(h1, bottom); 3030 3031 if (caps == 2) { 3032 dest.moveTo(h1, bottom); 3033 dest.lineTo(h1 - dist, bottom + dist); 3034 dest.lineTo(h1, bottom); 3035 dest.lineTo(h1 + dist, bottom + dist); 3036 } else if (caps == 1) { 3037 dest.moveTo(h1, bottom); 3038 dest.lineTo(h1 - dist, bottom + dist); 3039 3040 dest.moveTo(h1 - dist, bottom + dist - 0.5f); 3041 dest.lineTo(h1 + dist, bottom + dist - 0.5f); 3042 3043 dest.moveTo(h1 + dist, bottom + dist); 3044 dest.lineTo(h1, bottom); 3045 } 3046 3047 if (fn == 2) { 3048 dest.moveTo(h1, top); 3049 dest.lineTo(h1 - dist, top - dist); 3050 dest.lineTo(h1, top); 3051 dest.lineTo(h1 + dist, top - dist); 3052 } else if (fn == 1) { 3053 dest.moveTo(h1, top); 3054 dest.lineTo(h1 - dist, top - dist); 3055 3056 dest.moveTo(h1 - dist, top - dist + 0.5f); 3057 dest.lineTo(h1 + dist, top - dist + 0.5f); 3058 3059 dest.moveTo(h1 + dist, top - dist); 3060 dest.lineTo(h1, top); 3061 } 3062 } 3063 addSelection(int line, int start, int end, int top, int bottom, SelectionRectangleConsumer consumer)3064 private void addSelection(int line, int start, int end, 3065 int top, int bottom, SelectionRectangleConsumer consumer) { 3066 int linestart = getLineStart(line); 3067 int lineend = getLineEnd(line); 3068 Directions dirs = getLineDirections(line); 3069 3070 if (lineend > linestart && mText.charAt(lineend - 1) == '\n') { 3071 lineend--; 3072 } 3073 3074 for (int i = 0; i < dirs.mDirections.length; i += 2) { 3075 int here = linestart + dirs.mDirections[i]; 3076 int there = here + (dirs.mDirections[i + 1] & RUN_LENGTH_MASK); 3077 3078 if (there > lineend) { 3079 there = lineend; 3080 } 3081 3082 if (start <= there && end >= here) { 3083 int st = Math.max(start, here); 3084 int en = Math.min(end, there); 3085 3086 if (st != en) { 3087 float h1 = getHorizontal(st, false, line, false /* not clamped */); 3088 float h2 = getHorizontal(en, true, line, false /* not clamped */); 3089 3090 float left = Math.min(h1, h2); 3091 float right = Math.max(h1, h2); 3092 3093 final @TextSelectionLayout int layout = 3094 ((dirs.mDirections[i + 1] & RUN_RTL_FLAG) != 0) 3095 ? TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT 3096 : TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT; 3097 3098 consumer.accept(left, top, right, bottom, layout); 3099 } 3100 } 3101 } 3102 } 3103 3104 /** 3105 * Fills in the specified Path with a representation of a highlight 3106 * between the specified offsets. This will often be a rectangle 3107 * or a potentially discontinuous set of rectangles. If the start 3108 * and end are the same, the returned path is empty. 3109 */ getSelectionPath(int start, int end, Path dest)3110 public void getSelectionPath(int start, int end, Path dest) { 3111 dest.reset(); 3112 getSelection(start, end, (left, top, right, bottom, textSelectionLayout) -> 3113 dest.addRect(left, top, right, bottom, Path.Direction.CW)); 3114 } 3115 3116 /** 3117 * Calculates the rectangles which should be highlighted to indicate a selection between start 3118 * and end and feeds them into the given {@link SelectionRectangleConsumer}. 3119 * 3120 * @param start the starting index of the selection 3121 * @param end the ending index of the selection 3122 * @param consumer the {@link SelectionRectangleConsumer} which will receive the generated 3123 * rectangles. It will be called every time a rectangle is generated. 3124 * @hide 3125 * @see #getSelectionPath(int, int, Path) 3126 */ getSelection(int start, int end, final SelectionRectangleConsumer consumer)3127 public final void getSelection(int start, int end, final SelectionRectangleConsumer consumer) { 3128 if (start == end) { 3129 return; 3130 } 3131 3132 if (end < start) { 3133 int temp = end; 3134 end = start; 3135 start = temp; 3136 } 3137 3138 final int startline = getLineForOffset(start); 3139 final int endline = getLineForOffset(end); 3140 3141 int top = getLineTop(startline); 3142 int bottom = getLineBottom(endline, /* includeLineSpacing= */ false); 3143 3144 if (startline == endline) { 3145 addSelection(startline, start, end, top, bottom, consumer); 3146 } else { 3147 final float width = mWidth; 3148 3149 addSelection(startline, start, getLineEnd(startline), 3150 top, getLineBottom(startline), consumer); 3151 3152 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) { 3153 consumer.accept(getLineLeft(startline), top, 0, getLineBottom(startline), 3154 TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); 3155 } else { 3156 consumer.accept(getLineRight(startline), top, width, getLineBottom(startline), 3157 TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT); 3158 } 3159 3160 for (int i = startline + 1; i < endline; i++) { 3161 top = getLineTop(i); 3162 bottom = getLineBottom(i); 3163 if (getParagraphDirection(i) == DIR_RIGHT_TO_LEFT) { 3164 consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); 3165 } else { 3166 consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT); 3167 } 3168 } 3169 3170 top = getLineTop(endline); 3171 bottom = getLineBottom(endline, /* includeLineSpacing= */ false); 3172 3173 addSelection(endline, getLineStart(endline), end, top, bottom, consumer); 3174 3175 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) { 3176 consumer.accept(width, top, getLineRight(endline), bottom, 3177 TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); 3178 } else { 3179 consumer.accept(0, top, getLineLeft(endline), bottom, 3180 TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT); 3181 } 3182 } 3183 } 3184 3185 /** 3186 * Get the alignment of the specified paragraph, taking into account 3187 * markup attached to it. 3188 */ getParagraphAlignment(int line)3189 public final Alignment getParagraphAlignment(int line) { 3190 Alignment align = mAlignment; 3191 3192 if (mSpannedText) { 3193 Spanned sp = (Spanned) mText; 3194 AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line), 3195 getLineEnd(line), 3196 AlignmentSpan.class); 3197 3198 int spanLength = spans.length; 3199 if (spanLength > 0) { 3200 align = spans[spanLength-1].getAlignment(); 3201 } 3202 } 3203 3204 return align; 3205 } 3206 3207 /** 3208 * Get the left edge of the specified paragraph, inset by left margins. 3209 */ getParagraphLeft(int line)3210 public final int getParagraphLeft(int line) { 3211 int left = 0; 3212 int dir = getParagraphDirection(line); 3213 if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) { 3214 return left; // leading margin has no impact, or no styles 3215 } 3216 return getParagraphLeadingMargin(line); 3217 } 3218 3219 /** 3220 * Get the right edge of the specified paragraph, inset by right margins. 3221 */ getParagraphRight(int line)3222 public final int getParagraphRight(int line) { 3223 int right = mWidth; 3224 int dir = getParagraphDirection(line); 3225 if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) { 3226 return right; // leading margin has no impact, or no styles 3227 } 3228 return right - getParagraphLeadingMargin(line); 3229 } 3230 3231 /** 3232 * Returns the effective leading margin (unsigned) for this line, 3233 * taking into account LeadingMarginSpan and LeadingMarginSpan2. 3234 * @param line the line index 3235 * @return the leading margin of this line 3236 */ getParagraphLeadingMargin(int line)3237 private int getParagraphLeadingMargin(int line) { 3238 if (!mSpannedText) { 3239 return 0; 3240 } 3241 Spanned spanned = (Spanned) mText; 3242 3243 int lineStart = getLineStart(line); 3244 int lineEnd = getLineEnd(line); 3245 int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd, 3246 LeadingMarginSpan.class); 3247 LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd, 3248 LeadingMarginSpan.class); 3249 if (spans.length == 0) { 3250 return 0; // no leading margin span; 3251 } 3252 3253 int margin = 0; 3254 3255 boolean useFirstLineMargin = lineStart == 0 || spanned.charAt(lineStart - 1) == '\n'; 3256 for (int i = 0; i < spans.length; i++) { 3257 if (spans[i] instanceof LeadingMarginSpan2) { 3258 int spStart = spanned.getSpanStart(spans[i]); 3259 int spanLine = getLineForOffset(spStart); 3260 int count = ((LeadingMarginSpan2) spans[i]).getLeadingMarginLineCount(); 3261 // if there is more than one LeadingMarginSpan2, use the count that is greatest 3262 useFirstLineMargin |= line < spanLine + count; 3263 } 3264 } 3265 for (int i = 0; i < spans.length; i++) { 3266 LeadingMarginSpan span = spans[i]; 3267 margin += span.getLeadingMargin(useFirstLineMargin); 3268 } 3269 3270 return margin; 3271 } 3272 3273 private static float measurePara(TextPaint paint, CharSequence text, int start, int end, 3274 TextDirectionHeuristic textDir, boolean useBoundsForWidth) { 3275 MeasuredParagraph mt = null; 3276 TextLine tl = TextLine.obtain(); 3277 try { 3278 mt = MeasuredParagraph.buildForBidi(text, start, end, textDir, mt); 3279 final char[] chars = mt.getChars(); 3280 final int len = chars.length; 3281 final Directions directions = mt.getDirections(0, len); 3282 final int dir = mt.getParagraphDir(); 3283 boolean hasTabs = false; 3284 TabStops tabStops = null; 3285 // leading margins should be taken into account when measuring a paragraph 3286 int margin = 0; 3287 if (text instanceof Spanned) { 3288 Spanned spanned = (Spanned) text; 3289 LeadingMarginSpan[] spans = getParagraphSpans(spanned, start, end, 3290 LeadingMarginSpan.class); 3291 for (LeadingMarginSpan lms : spans) { 3292 margin += lms.getLeadingMargin(true); 3293 } 3294 } 3295 for (int i = 0; i < len; ++i) { 3296 if (chars[i] == '\t') { 3297 hasTabs = true; 3298 if (text instanceof Spanned) { 3299 Spanned spanned = (Spanned) text; 3300 int spanEnd = spanned.nextSpanTransition(start, end, 3301 TabStopSpan.class); 3302 TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd, 3303 TabStopSpan.class); 3304 if (spans.length > 0) { 3305 tabStops = new TabStops(TAB_INCREMENT, spans); 3306 } 3307 } 3308 break; 3309 } 3310 } 3311 tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops, 3312 0 /* ellipsisStart */, 0 /* ellipsisEnd */, 3313 false /* use fallback line spacing. unused */); 3314 return margin + Math.abs(tl.metrics(null, null, useBoundsForWidth, null)); 3315 } finally { 3316 TextLine.recycle(tl); 3317 if (mt != null) { 3318 mt.recycle(); 3319 } 3320 } 3321 } 3322 3323 /** 3324 * @hide 3325 */ 3326 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 3327 public static class TabStops { 3328 private float[] mStops; 3329 private int mNumStops; 3330 private float mIncrement; 3331 3332 public TabStops(float increment, Object[] spans) { 3333 reset(increment, spans); 3334 } 3335 3336 void reset(float increment, Object[] spans) { 3337 this.mIncrement = increment; 3338 3339 int ns = 0; 3340 if (spans != null) { 3341 float[] stops = this.mStops; 3342 for (Object o : spans) { 3343 if (o instanceof TabStopSpan) { 3344 if (stops == null) { 3345 stops = new float[10]; 3346 } else if (ns == stops.length) { 3347 float[] nstops = new float[ns * 2]; 3348 for (int i = 0; i < ns; ++i) { 3349 nstops[i] = stops[i]; 3350 } 3351 stops = nstops; 3352 } 3353 stops[ns++] = ((TabStopSpan) o).getTabStop(); 3354 } 3355 } 3356 if (ns > 1) { 3357 Arrays.sort(stops, 0, ns); 3358 } 3359 if (stops != this.mStops) { 3360 this.mStops = stops; 3361 } 3362 } 3363 this.mNumStops = ns; 3364 } 3365 3366 float nextTab(float h) { 3367 int ns = this.mNumStops; 3368 if (ns > 0) { 3369 float[] stops = this.mStops; 3370 for (int i = 0; i < ns; ++i) { 3371 float stop = stops[i]; 3372 if (stop > h) { 3373 return stop; 3374 } 3375 } 3376 } 3377 return nextDefaultStop(h, mIncrement); 3378 } 3379 3380 /** 3381 * Returns the position of next tab stop. 3382 */ 3383 public static float nextDefaultStop(float h, float inc) { 3384 return ((int) ((h + inc) / inc)) * inc; 3385 } 3386 } 3387 3388 /** 3389 * Returns the position of the next tab stop after h on the line. 3390 * 3391 * @param text the text 3392 * @param start start of the line 3393 * @param end limit of the line 3394 * @param h the current horizontal offset 3395 * @param tabs the tabs, can be null. If it is null, any tabs in effect 3396 * on the line will be used. If there are no tabs, a default offset 3397 * will be used to compute the tab stop. 3398 * @return the offset of the next tab stop. 3399 */ 3400 /* package */ static float nextTab(CharSequence text, int start, int end, 3401 float h, Object[] tabs) { 3402 float nh = Float.MAX_VALUE; 3403 boolean alltabs = false; 3404 3405 if (text instanceof Spanned) { 3406 if (tabs == null) { 3407 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class); 3408 alltabs = true; 3409 } 3410 3411 for (int i = 0; i < tabs.length; i++) { 3412 if (!alltabs) { 3413 if (!(tabs[i] instanceof TabStopSpan)) 3414 continue; 3415 } 3416 3417 int where = ((TabStopSpan) tabs[i]).getTabStop(); 3418 3419 if (where < nh && where > h) 3420 nh = where; 3421 } 3422 3423 if (nh != Float.MAX_VALUE) 3424 return nh; 3425 } 3426 3427 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; 3428 } 3429 3430 protected final boolean isSpanned() { 3431 return mSpannedText; 3432 } 3433 3434 /** 3435 * Returns the same as <code>text.getSpans()</code>, except where 3436 * <code>start</code> and <code>end</code> are the same and are not 3437 * at the very beginning of the text, in which case an empty array 3438 * is returned instead. 3439 * <p> 3440 * This is needed because of the special case that <code>getSpans()</code> 3441 * on an empty range returns the spans adjacent to that range, which is 3442 * primarily for the sake of <code>TextWatchers</code> so they will get 3443 * notifications when text goes from empty to non-empty. But it also 3444 * has the unfortunate side effect that if the text ends with an empty 3445 * paragraph, that paragraph accidentally picks up the styles of the 3446 * preceding paragraph (even though those styles will not be picked up 3447 * by new text that is inserted into the empty paragraph). 3448 * <p> 3449 * The reason it just checks whether <code>start</code> and <code>end</code> 3450 * is the same is that the only time a line can contain 0 characters 3451 * is if it is the final paragraph of the Layout; otherwise any line will 3452 * contain at least one printing or newline character. The reason for the 3453 * additional check if <code>start</code> is greater than 0 is that 3454 * if the empty paragraph is the entire content of the buffer, paragraph 3455 * styles that are already applied to the buffer will apply to text that 3456 * is inserted into it. 3457 */ 3458 /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) { 3459 if (start == end && start > 0) { 3460 return ArrayUtils.emptyArray(type); 3461 } 3462 3463 if(text instanceof SpannableStringBuilder) { 3464 return ((SpannableStringBuilder) text).getSpans(start, end, type, false); 3465 } else { 3466 return text.getSpans(start, end, type); 3467 } 3468 } 3469 3470 private void ellipsize(int start, int end, int line, 3471 char[] dest, int destoff, TextUtils.TruncateAt method) { 3472 final int ellipsisCount = getEllipsisCount(line); 3473 if (ellipsisCount == 0) { 3474 return; 3475 } 3476 final int ellipsisStart = getEllipsisStart(line); 3477 final int lineStart = getLineStart(line); 3478 3479 final String ellipsisString = TextUtils.getEllipsisString(method); 3480 final int ellipsisStringLen = ellipsisString.length(); 3481 // Use the ellipsis string only if there are that at least as many characters to replace. 3482 final boolean useEllipsisString = ellipsisCount >= ellipsisStringLen; 3483 final int min = Math.max(0, start - ellipsisStart - lineStart); 3484 final int max = Math.min(ellipsisCount, end - ellipsisStart - lineStart); 3485 3486 for (int i = min; i < max; i++) { 3487 final char c; 3488 if (useEllipsisString && i < ellipsisStringLen) { 3489 c = ellipsisString.charAt(i); 3490 } else { 3491 c = TextUtils.ELLIPSIS_FILLER; 3492 } 3493 3494 final int a = i + ellipsisStart + lineStart; 3495 dest[destoff + a - start] = c; 3496 } 3497 } 3498 3499 /** 3500 * Stores information about bidirectional (left-to-right or right-to-left) 3501 * text within the layout of a line. 3502 */ 3503 public static class Directions { 3504 /** 3505 * Directions represents directional runs within a line of text. Runs are pairs of ints 3506 * listed in visual order, starting from the leading margin. The first int of each pair is 3507 * the offset from the first character of the line to the start of the run. The second int 3508 * represents both the length and level of the run. The length is in the lower bits, 3509 * accessed by masking with RUN_LENGTH_MASK. The level is in the higher bits, accessed by 3510 * shifting by RUN_LEVEL_SHIFT and masking by RUN_LEVEL_MASK. To simply test for an RTL 3511 * direction, test the bit using RUN_RTL_FLAG, if set then the direction is rtl. 3512 * @hide 3513 */ 3514 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 3515 public int[] mDirections; 3516 3517 /** 3518 * @hide 3519 */ 3520 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) Directions(int[] dirs)3521 public Directions(int[] dirs) { 3522 mDirections = dirs; 3523 } 3524 3525 /** 3526 * Returns number of BiDi runs. 3527 * 3528 * @hide 3529 */ getRunCount()3530 public @IntRange(from = 0) int getRunCount() { 3531 return mDirections.length / 2; 3532 } 3533 3534 /** 3535 * Returns the start offset of the BiDi run. 3536 * 3537 * @param runIndex the index of the BiDi run 3538 * @return the start offset of the BiDi run. 3539 * @hide 3540 */ getRunStart(@ntRangefrom = 0) int runIndex)3541 public @IntRange(from = 0) int getRunStart(@IntRange(from = 0) int runIndex) { 3542 return mDirections[runIndex * 2]; 3543 } 3544 3545 /** 3546 * Returns the length of the BiDi run. 3547 * 3548 * Note that this method may return too large number due to reducing the number of object 3549 * allocations. The too large number means the remaining part is assigned to this run. The 3550 * caller must clamp the returned value. 3551 * 3552 * @param runIndex the index of the BiDi run 3553 * @return the length of the BiDi run. 3554 * @hide 3555 */ getRunLength(@ntRangefrom = 0) int runIndex)3556 public @IntRange(from = 0) int getRunLength(@IntRange(from = 0) int runIndex) { 3557 return mDirections[runIndex * 2 + 1] & RUN_LENGTH_MASK; 3558 } 3559 3560 /** 3561 * Returns the BiDi level of this run. 3562 * 3563 * @param runIndex the index of the BiDi run 3564 * @return the BiDi level of this run. 3565 * @hide 3566 */ 3567 @IntRange(from = 0) getRunLevel(int runIndex)3568 public int getRunLevel(int runIndex) { 3569 return (mDirections[runIndex * 2 + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 3570 } 3571 3572 /** 3573 * Returns true if the BiDi run is RTL. 3574 * 3575 * @param runIndex the index of the BiDi run 3576 * @return true if the BiDi run is RTL. 3577 * @hide 3578 */ isRunRtl(int runIndex)3579 public boolean isRunRtl(int runIndex) { 3580 return (mDirections[runIndex * 2 + 1] & RUN_RTL_FLAG) != 0; 3581 } 3582 } 3583 3584 /** 3585 * Return the offset of the first character to be ellipsized away, 3586 * relative to the start of the line. (So 0 if the beginning of the 3587 * line is ellipsized, not getLineStart().) 3588 */ 3589 public abstract int getEllipsisStart(int line); 3590 3591 /** 3592 * Returns the number of characters to be ellipsized away, or 0 if 3593 * no ellipsis is to take place. 3594 */ 3595 public abstract int getEllipsisCount(int line); 3596 3597 /* package */ static class Ellipsizer implements CharSequence, GetChars { 3598 /* package */ CharSequence mText; 3599 /* package */ Layout mLayout; 3600 /* package */ int mWidth; 3601 /* package */ TextUtils.TruncateAt mMethod; 3602 Ellipsizer(CharSequence s)3603 public Ellipsizer(CharSequence s) { 3604 mText = s; 3605 } 3606 charAt(int off)3607 public char charAt(int off) { 3608 char[] buf = TextUtils.obtain(1); 3609 getChars(off, off + 1, buf, 0); 3610 char ret = buf[0]; 3611 3612 TextUtils.recycle(buf); 3613 return ret; 3614 } 3615 getChars(int start, int end, char[] dest, int destoff)3616 public void getChars(int start, int end, char[] dest, int destoff) { 3617 int line1 = mLayout.getLineForOffset(start); 3618 int line2 = mLayout.getLineForOffset(end); 3619 3620 TextUtils.getChars(mText, start, end, dest, destoff); 3621 3622 for (int i = line1; i <= line2; i++) { 3623 mLayout.ellipsize(start, end, i, dest, destoff, mMethod); 3624 } 3625 } 3626 length()3627 public int length() { 3628 return mText.length(); 3629 } 3630 subSequence(int start, int end)3631 public CharSequence subSequence(int start, int end) { 3632 char[] s = new char[end - start]; 3633 getChars(start, end, s, 0); 3634 return new String(s); 3635 } 3636 3637 @Override toString()3638 public String toString() { 3639 char[] s = new char[length()]; 3640 getChars(0, length(), s, 0); 3641 return new String(s); 3642 } 3643 3644 } 3645 3646 /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned { 3647 private Spanned mSpanned; 3648 SpannedEllipsizer(CharSequence display)3649 public SpannedEllipsizer(CharSequence display) { 3650 super(display); 3651 mSpanned = (Spanned) display; 3652 } 3653 getSpans(int start, int end, Class<T> type)3654 public <T> T[] getSpans(int start, int end, Class<T> type) { 3655 return mSpanned.getSpans(start, end, type); 3656 } 3657 getSpanStart(Object tag)3658 public int getSpanStart(Object tag) { 3659 return mSpanned.getSpanStart(tag); 3660 } 3661 getSpanEnd(Object tag)3662 public int getSpanEnd(Object tag) { 3663 return mSpanned.getSpanEnd(tag); 3664 } 3665 getSpanFlags(Object tag)3666 public int getSpanFlags(Object tag) { 3667 return mSpanned.getSpanFlags(tag); 3668 } 3669 3670 @SuppressWarnings("rawtypes") nextSpanTransition(int start, int limit, Class type)3671 public int nextSpanTransition(int start, int limit, Class type) { 3672 return mSpanned.nextSpanTransition(start, limit, type); 3673 } 3674 3675 @Override subSequence(int start, int end)3676 public CharSequence subSequence(int start, int end) { 3677 char[] s = new char[end - start]; 3678 getChars(start, end, s, 0); 3679 3680 SpannableString ss = new SpannableString(new String(s)); 3681 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); 3682 return ss; 3683 } 3684 } 3685 3686 private CharSequence mText; 3687 @UnsupportedAppUsage 3688 private TextPaint mPaint; 3689 private final TextPaint mWorkPaint = new TextPaint(); 3690 private final Paint mWorkPlainPaint = new Paint(); 3691 private int mWidth; 3692 private Alignment mAlignment = Alignment.ALIGN_NORMAL; 3693 private float mSpacingMult; 3694 private float mSpacingAdd; 3695 private static final Rect sTempRect = new Rect(); 3696 private boolean mSpannedText; 3697 @Nullable private SpanColors mSpanColors; 3698 private TextDirectionHeuristic mTextDir; 3699 private SpanSet<LineBackgroundSpan> mLineBackgroundSpans; 3700 private boolean mIncludePad; 3701 private boolean mFallbackLineSpacing; 3702 private int mEllipsizedWidth; 3703 private TextUtils.TruncateAt mEllipsize; 3704 private int mMaxLines; 3705 private int mBreakStrategy; 3706 private int mHyphenationFrequency; 3707 private int[] mLeftIndents; 3708 private int[] mRightIndents; 3709 private int mJustificationMode; 3710 private LineBreakConfig mLineBreakConfig; 3711 private boolean mUseBoundsForWidth; 3712 private boolean mShiftDrawingOffsetForStartOverhang; 3713 private @Nullable Paint.FontMetrics mMinimumFontMetrics; 3714 3715 private TextLine.LineInfo mLineInfo = null; 3716 3717 /** @hide */ 3718 @IntDef(prefix = { "DIR_" }, value = { 3719 DIR_LEFT_TO_RIGHT, 3720 DIR_RIGHT_TO_LEFT 3721 }) 3722 @Retention(RetentionPolicy.SOURCE) 3723 public @interface Direction {} 3724 3725 public static final int DIR_LEFT_TO_RIGHT = 1; 3726 public static final int DIR_RIGHT_TO_LEFT = -1; 3727 3728 /* package */ static final int DIR_REQUEST_LTR = 1; 3729 /* package */ static final int DIR_REQUEST_RTL = -1; 3730 @UnsupportedAppUsage 3731 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; 3732 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; 3733 3734 /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; 3735 /* package */ static final int RUN_LEVEL_SHIFT = 26; 3736 /* package */ static final int RUN_LEVEL_MASK = 0x3f; 3737 /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; 3738 3739 public enum Alignment { 3740 ALIGN_NORMAL, 3741 ALIGN_OPPOSITE, 3742 ALIGN_CENTER, 3743 /** @hide */ 3744 @UnsupportedAppUsage 3745 ALIGN_LEFT, 3746 /** @hide */ 3747 @UnsupportedAppUsage 3748 ALIGN_RIGHT, 3749 } 3750 3751 private static final float TAB_INCREMENT = 20; 3752 3753 /** @hide */ 3754 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 3755 @UnsupportedAppUsage 3756 public static final Directions DIRS_ALL_LEFT_TO_RIGHT = 3757 new Directions(new int[] { 0, RUN_LENGTH_MASK }); 3758 3759 /** @hide */ 3760 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 3761 @UnsupportedAppUsage 3762 public static final Directions DIRS_ALL_RIGHT_TO_LEFT = 3763 new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); 3764 3765 /** @hide */ 3766 @Retention(RetentionPolicy.SOURCE) 3767 @IntDef(prefix = { "TEXT_SELECTION_LAYOUT_" }, value = { 3768 TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, 3769 TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT 3770 }) 3771 public @interface TextSelectionLayout {} 3772 3773 /** @hide */ 3774 public static final int TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT = 0; 3775 /** @hide */ 3776 public static final int TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT = 1; 3777 3778 /** @hide */ 3779 @FunctionalInterface 3780 public interface SelectionRectangleConsumer { 3781 /** 3782 * Performs this operation on the given rectangle. 3783 * 3784 * @param left the left edge of the rectangle 3785 * @param top the top edge of the rectangle 3786 * @param right the right edge of the rectangle 3787 * @param bottom the bottom edge of the rectangle 3788 * @param textSelectionLayout the layout (RTL or LTR) of the text covered by this 3789 * selection rectangle 3790 */ 3791 void accept(float left, float top, float right, float bottom, 3792 @TextSelectionLayout int textSelectionLayout); 3793 } 3794 3795 /** 3796 * Strategy for determining whether a text segment is inside a rectangle area. 3797 * 3798 * @see #getRangeForRect(RectF, SegmentFinder, TextInclusionStrategy) 3799 */ 3800 @FunctionalInterface 3801 public interface TextInclusionStrategy { 3802 /** 3803 * Returns true if this {@link TextInclusionStrategy} considers the segment with bounds 3804 * {@code segmentBounds} to be inside {@code area}. 3805 * 3806 * <p>The segment is a range of text which does not cross line boundaries or directional run 3807 * boundaries. The horizontal bounds of the segment are the start bound of the first 3808 * character to the end bound of the last character. The vertical bounds match the line 3809 * bounds ({@code getLineTop(line)} and {@code getLineBottom(line, false)}). 3810 */ 3811 boolean isSegmentInside(@NonNull RectF segmentBounds, @NonNull RectF area); 3812 } 3813 3814 /** 3815 * A builder class for Layout object. 3816 * 3817 * Different from {@link StaticLayout.Builder}, this builder generates the optimal layout based 3818 * on input. If the given text and parameters can be rendered with {@link BoringLayout}, this 3819 * builder generates {@link BoringLayout} instance. Otherwise, {@link StaticLayout} instance is 3820 * generated. 3821 * 3822 * @see StaticLayout.Builder 3823 */ 3824 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) 3825 public static final class Builder { 3826 /** 3827 * Construct a builder class. 3828 * 3829 * @param text a text to be displayed. 3830 * @param start an inclusive start index of the text to be displayed. 3831 * @param end an exclusive end index of the text to be displayed. 3832 * @param paint a paint object to be used for drawing text. 3833 * @param width a width constraint in pixels. 3834 */ Builder( @onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @IntRange(from = 0) int width)3835 public Builder( 3836 @NonNull CharSequence text, 3837 @IntRange(from = 0) int start, 3838 @IntRange(from = 0) int end, 3839 @NonNull TextPaint paint, 3840 @IntRange(from = 0) int width) { 3841 mText = text; 3842 mStart = start; 3843 mEnd = end; 3844 mPaint = paint; 3845 mWidth = width; 3846 mEllipsizedWidth = width; 3847 } 3848 3849 /** 3850 * Set the text alignment. 3851 * 3852 * The default value is {@link Layout.Alignment#ALIGN_NORMAL}. 3853 * 3854 * @param alignment an alignment. 3855 * @return this builder instance. 3856 * @see Layout.Alignment 3857 * @see Layout#getAlignment() 3858 * @see StaticLayout.Builder#setAlignment(Alignment) 3859 */ 3860 @NonNull setAlignment(@onNull Alignment alignment)3861 public Builder setAlignment(@NonNull Alignment alignment) { 3862 mAlignment = alignment; 3863 return this; 3864 } 3865 3866 /** 3867 * Set the text direction heuristics. 3868 * 3869 * The text direction heuristics is used to resolve text direction on the text. 3870 * 3871 * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} 3872 * 3873 * @param textDirection a text direction heuristic. 3874 * @return this builder instance. 3875 * @see TextDirectionHeuristics 3876 * @see Layout#getTextDirectionHeuristic() 3877 * @see StaticLayout.Builder#setTextDirection(TextDirectionHeuristic) 3878 */ 3879 @NonNull setTextDirectionHeuristic(@onNull TextDirectionHeuristic textDirection)3880 public Builder setTextDirectionHeuristic(@NonNull TextDirectionHeuristic textDirection) { 3881 mTextDir = textDirection; 3882 return this; 3883 } 3884 3885 /** 3886 * Set the line spacing amount. 3887 * 3888 * The specified amount of pixels will be added to each line. 3889 * 3890 * The default value is {@code 0}. The negative value is allowed for squeezing lines. 3891 * 3892 * @param amount an amount of pixels to be added to line height. 3893 * @return this builder instance. 3894 * @see Layout#getLineSpacingAmount() 3895 * @see Layout#getSpacingAdd() 3896 * @see StaticLayout.Builder#setLineSpacing(float, float) 3897 */ 3898 @NonNull setLineSpacingAmount(float amount)3899 public Builder setLineSpacingAmount(float amount) { 3900 mSpacingAdd = amount; 3901 return this; 3902 } 3903 3904 /** 3905 * Set the line spacing multiplier. 3906 * 3907 * The specified value will be multiplied to each line. 3908 * 3909 * The default value is {@code 1}. 3910 * 3911 * @param multiplier a multiplier to be applied to the line height 3912 * @return this builder instance. 3913 * @see Layout#getLineSpacingMultiplier() 3914 * @see Layout#getSpacingMultiplier() 3915 * @see StaticLayout.Builder#setLineSpacing(float, float) 3916 */ 3917 @NonNull setLineSpacingMultiplier(@loatRangefrom = 0) float multiplier)3918 public Builder setLineSpacingMultiplier(@FloatRange(from = 0) float multiplier) { 3919 mSpacingMult = multiplier; 3920 return this; 3921 } 3922 3923 /** 3924 * Set whether including extra padding into the first and the last line height. 3925 * 3926 * By setting true, the first line of the text and the last line of the text will have extra 3927 * vertical space for avoiding clipping. 3928 * 3929 * The default value is {@code true}. 3930 * 3931 * @param includeFontPadding true for including extra space into first and last line. 3932 * @return this builder instance. 3933 * @see Layout#isFontPaddingIncluded() 3934 * @see StaticLayout.Builder#setIncludePad(boolean) 3935 */ 3936 @NonNull setFontPaddingIncluded(boolean includeFontPadding)3937 public Builder setFontPaddingIncluded(boolean includeFontPadding) { 3938 mIncludePad = includeFontPadding; 3939 return this; 3940 } 3941 3942 /** 3943 * Set whether to respect the ascent and descent of the fallback fonts. 3944 * 3945 * Set whether to respect the ascent and descent of the fallback fonts that are used in 3946 * displaying the text (which is needed to avoid text from consecutive lines running into 3947 * each other). If set, fallback fonts that end up getting used can increase the ascent 3948 * and descent of the lines that they are used on. 3949 * 3950 * The default value is {@code false} 3951 * 3952 * @param fallbackLineSpacing whether to expand line height based on fallback fonts. 3953 * @return this builder instance. 3954 * @see Layout#isFallbackLineSpacingEnabled() 3955 * @see StaticLayout.Builder#setUseLineSpacingFromFallbacks(boolean) 3956 */ 3957 @NonNull setFallbackLineSpacingEnabled(boolean fallbackLineSpacing)3958 public Builder setFallbackLineSpacingEnabled(boolean fallbackLineSpacing) { 3959 mFallbackLineSpacing = fallbackLineSpacing; 3960 return this; 3961 } 3962 3963 /** 3964 * Set the width as used for ellipsizing purpose in pixels. 3965 * 3966 * The passed value is ignored and forced to set to the value of width constraint passed in 3967 * constructor if no ellipsize option is set. 3968 * 3969 * The default value is the width constraint. 3970 * 3971 * @param ellipsizeWidth a ellipsizing width in pixels. 3972 * @return this builder instance. 3973 * @see Layout#getEllipsizedWidth() 3974 * @see StaticLayout.Builder#setEllipsizedWidth(int) 3975 */ 3976 @NonNull setEllipsizedWidth(@ntRangefrom = 0) int ellipsizeWidth)3977 public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizeWidth) { 3978 mEllipsizedWidth = ellipsizeWidth; 3979 return this; 3980 } 3981 3982 /** 3983 * Set the ellipsizing type. 3984 * 3985 * By setting null, the ellipsize is disabled. 3986 * 3987 * The default value is {@code null}. 3988 * 3989 * @param ellipsize type of the ellipsize. null for disabling ellipsize. 3990 * @return this builder instance. 3991 * @see Layout#getEllipsize() 3992 * @see StaticLayout.Builder#getEllipsize() 3993 * @see android.text.TextUtils.TruncateAt 3994 */ 3995 @NonNull setEllipsize(@ullable TextUtils.TruncateAt ellipsize)3996 public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) { 3997 mEllipsize = ellipsize; 3998 return this; 3999 } 4000 4001 /** 4002 * Set the maximum number of lines. 4003 * 4004 * The default value is unlimited. 4005 * 4006 * @param maxLines maximum number of lines in the layout. 4007 * @return this builder instance. 4008 * @see Layout#getMaxLines() 4009 * @see StaticLayout.Builder#setMaxLines(int) 4010 */ 4011 @NonNull setMaxLines(@ntRangefrom = 1) int maxLines)4012 public Builder setMaxLines(@IntRange(from = 1) int maxLines) { 4013 mMaxLines = maxLines; 4014 return this; 4015 } 4016 4017 /** 4018 * Set the line break strategy. 4019 * 4020 * The default value is {@link Layout#BREAK_STRATEGY_SIMPLE}. 4021 * 4022 * @param breakStrategy a break strategy for line breaking. 4023 * @return this builder instance. 4024 * @see Layout#getBreakStrategy() 4025 * @see StaticLayout.Builder#setBreakStrategy(int) 4026 * @see Layout#BREAK_STRATEGY_SIMPLE 4027 * @see Layout#BREAK_STRATEGY_HIGH_QUALITY 4028 * @see Layout#BREAK_STRATEGY_BALANCED 4029 */ 4030 @NonNull setBreakStrategy(@reakStrategy int breakStrategy)4031 public Builder setBreakStrategy(@BreakStrategy int breakStrategy) { 4032 mBreakStrategy = breakStrategy; 4033 return this; 4034 } 4035 4036 /** 4037 * Set the hyphenation frequency. 4038 * 4039 * The default value is {@link Layout#HYPHENATION_FREQUENCY_NONE}. 4040 * 4041 * @param hyphenationFrequency a hyphenation frequency. 4042 * @return this builder instance. 4043 * @see Layout#getHyphenationFrequency() 4044 * @see StaticLayout.Builder#setHyphenationFrequency(int) 4045 * @see Layout#HYPHENATION_FREQUENCY_NONE 4046 * @see Layout#HYPHENATION_FREQUENCY_NORMAL 4047 * @see Layout#HYPHENATION_FREQUENCY_FULL 4048 * @see Layout#HYPHENATION_FREQUENCY_NORMAL_FAST 4049 * @see Layout#HYPHENATION_FREQUENCY_FULL_FAST 4050 */ 4051 @NonNull setHyphenationFrequency(@yphenationFrequency int hyphenationFrequency)4052 public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) { 4053 mHyphenationFrequency = hyphenationFrequency; 4054 return this; 4055 } 4056 4057 /** 4058 * Set visually left indents in pixels per lines. 4059 * 4060 * For the lines past the last element in the array, the last element repeats. Passing null 4061 * for disabling indents. 4062 * 4063 * Note that even with the RTL layout, this method reserve spacing at the visually left of 4064 * the line. 4065 * 4066 * The default value is {@code null}. 4067 * 4068 * @param leftIndents array of indents values for the left margins in pixels. 4069 * @return this builder instance. 4070 * @see Layout#getLeftIndents() 4071 * @see Layout#getRightIndents() 4072 * @see Layout.Builder#setRightIndents(int[]) 4073 * @see StaticLayout.Builder#setIndents(int[], int[]) 4074 */ 4075 @NonNull setLeftIndents(@ullable int[] leftIndents)4076 public Builder setLeftIndents(@Nullable int[] leftIndents) { 4077 mLeftIndents = leftIndents; 4078 return this; 4079 } 4080 4081 /** 4082 * Set visually right indents in pixels per lines. 4083 * 4084 * For the lines past the last element in the array, the last element repeats. Passing null 4085 * for disabling indents. 4086 * 4087 * Note that even with the RTL layout, this method reserve spacing at the visually right of 4088 * the line. 4089 * 4090 * The default value is {@code null}. 4091 * 4092 * @param rightIndents array of indents values for the right margins in pixels. 4093 * @return this builder instance. 4094 * @see Layout#getLeftIndents() 4095 * @see Layout#getRightIndents() 4096 * @see Layout.Builder#setLeftIndents(int[]) 4097 * @see StaticLayout.Builder#setIndents(int[], int[]) 4098 */ 4099 @NonNull setRightIndents(@ullable int[] rightIndents)4100 public Builder setRightIndents(@Nullable int[] rightIndents) { 4101 mRightIndents = rightIndents; 4102 return this; 4103 } 4104 4105 /** 4106 * Set justification mode. 4107 * 4108 * When justification mode is {@link Layout#JUSTIFICATION_MODE_INTER_WORD}, the word spacing 4109 * on the given Paint passed to the constructor will be ignored. This behavior also affects 4110 * spans which change the word spacing. 4111 * 4112 * The default value is {@link Layout#JUSTIFICATION_MODE_NONE}. 4113 * 4114 * @param justificationMode justification mode. 4115 * @return this builder instance. 4116 * @see Layout#getJustificationMode() 4117 * @see StaticLayout.Builder#setJustificationMode(int) 4118 * @see Layout#JUSTIFICATION_MODE_NONE 4119 * @see Layout#JUSTIFICATION_MODE_INTER_WORD 4120 */ 4121 @NonNull setJustificationMode(@ustificationMode int justificationMode)4122 public Builder setJustificationMode(@JustificationMode int justificationMode) { 4123 mJustificationMode = justificationMode; 4124 return this; 4125 } 4126 4127 /** 4128 * Set the line break configuration. 4129 * 4130 * The default value is a LinebreakConfig instance that has 4131 * {@link LineBreakConfig#LINE_BREAK_STYLE_NONE} and 4132 * {@link LineBreakConfig#LINE_BREAK_WORD_STYLE_NONE}. 4133 * 4134 * @param lineBreakConfig the line break configuration 4135 * @return this builder instance. 4136 * @see Layout#getLineBreakConfig() 4137 * @see StaticLayout.Builder#setLineBreakConfig(LineBreakConfig) 4138 */ 4139 @NonNull setLineBreakConfig(@onNull LineBreakConfig lineBreakConfig)4140 public Builder setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) { 4141 mLineBreakConfig = lineBreakConfig; 4142 return this; 4143 } 4144 4145 /** 4146 * Set true for using width of bounding box as a source of automatic line breaking and 4147 * drawing. 4148 * 4149 * If this value is false, the Layout determines the drawing offset and automatic line 4150 * breaking based on total advances. By setting true, use all joined glyph's bounding boxes 4151 * as a source of text width. 4152 * 4153 * If the font has glyphs that have negative bearing X or its xMax is greater than advance, 4154 * the glyph clipping can happen because the drawing area may be bigger. By setting this to 4155 * true, the Layout will reserve more spaces for drawing. 4156 * 4157 * @param useBoundsForWidth True for using bounding box, false for advances. 4158 * @return this builder instance 4159 * @see Layout#getUseBoundsForWidth() 4160 * @see StaticLayout.Builder#setUseBoundsForWidth(boolean) 4161 */ 4162 // The corresponding getter is getUseBoundsForWidth 4163 @NonNull 4164 @SuppressLint("MissingGetterMatchingBuilder") 4165 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) setUseBoundsForWidth(boolean useBoundsForWidth)4166 public Builder setUseBoundsForWidth(boolean useBoundsForWidth) { 4167 mUseBoundsForWidth = useBoundsForWidth; 4168 return this; 4169 } 4170 4171 /** 4172 * Set true for shifting the drawing x offset for showing overhang at the start position. 4173 * 4174 * This flag is ignored if the {@link #getUseBoundsForWidth()} is false. 4175 * 4176 * If this value is false, the Layout draws text from the zero even if there is a glyph 4177 * stroke in a region where the x coordinate is negative. 4178 * 4179 * If this value is true, the Layout draws text with shifting the x coordinate of the 4180 * drawing bounding box. 4181 * 4182 * This value is false by default. 4183 * 4184 * @param shiftDrawingOffsetForStartOverhang true for shifting the drawing offset for 4185 * showing the stroke that is in the region where 4186 * the x coordinate is negative. 4187 * @see #setUseBoundsForWidth(boolean) 4188 * @see #getUseBoundsForWidth() 4189 */ 4190 @NonNull 4191 // The corresponding getter is getShiftDrawingOffsetForStartOverhang() 4192 @SuppressLint("MissingGetterMatchingBuilder") 4193 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) setShiftDrawingOffsetForStartOverhang( boolean shiftDrawingOffsetForStartOverhang)4194 public Builder setShiftDrawingOffsetForStartOverhang( 4195 boolean shiftDrawingOffsetForStartOverhang) { 4196 mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang; 4197 return this; 4198 } 4199 4200 /** 4201 * Set the minimum font metrics used for line spacing. 4202 * 4203 * <p> 4204 * {@code null} is the default value. If {@code null} is set or left it as default, the font 4205 * metrics obtained by {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} is used. 4206 * 4207 * <p> 4208 * The minimum meaning here is the minimum value of line spacing: maximum value of 4209 * {@link Paint#ascent()}, minimum value of {@link Paint#descent()}. 4210 * 4211 * <p> 4212 * By setting this value, each line will have minimum line spacing regardless of the text 4213 * rendered. For example, usually Japanese script has larger vertical metrics than Latin 4214 * script. By setting the metrics obtained by 4215 * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} for Japanese or leave it 4216 * {@code null} if the Paint's locale is Japanese, the line spacing for Japanese is reserved 4217 * if the text is an English text. If the vertical metrics of the text is larger than 4218 * Japanese, for example Burmese, the bigger font metrics is used. 4219 * 4220 * @param minimumFontMetrics A minimum font metrics. Passing {@code null} for using the 4221 * value obtained by 4222 * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} 4223 * @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics) 4224 * @see android.widget.TextView#getMinimumFontMetrics() 4225 * @see Layout#getMinimumFontMetrics() 4226 * @see StaticLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) 4227 * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) 4228 */ 4229 @NonNull 4230 @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) setMinimumFontMetrics(@ullable Paint.FontMetrics minimumFontMetrics)4231 public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) { 4232 mMinimumFontMetrics = minimumFontMetrics; 4233 return this; 4234 } 4235 isBoring()4236 private BoringLayout.Metrics isBoring() { 4237 if (mStart != 0 || mEnd != mText.length()) { // BoringLayout only support entire text. 4238 return null; 4239 } 4240 BoringLayout.Metrics metrics = BoringLayout.isBoring(mText, mPaint, mTextDir, 4241 mFallbackLineSpacing, mMinimumFontMetrics, null); 4242 if (metrics == null) { 4243 return null; 4244 } 4245 if (metrics.width <= mWidth) { 4246 return metrics; 4247 } 4248 if (mEllipsize != null) { 4249 return metrics; 4250 } 4251 return null; 4252 } 4253 4254 /** 4255 * Build a Layout object. 4256 */ 4257 @NonNull build()4258 public Layout build() { 4259 BoringLayout.Metrics metrics = isBoring(); 4260 if (metrics == null) { // we cannot use BoringLayout, create StaticLayout. 4261 return StaticLayout.Builder.obtain(mText, mStart, mEnd, mPaint, mWidth) 4262 .setAlignment(mAlignment) 4263 .setLineSpacing(mSpacingAdd, mSpacingMult) 4264 .setTextDirection(mTextDir) 4265 .setIncludePad(mIncludePad) 4266 .setUseLineSpacingFromFallbacks(mFallbackLineSpacing) 4267 .setEllipsizedWidth(mEllipsizedWidth) 4268 .setEllipsize(mEllipsize) 4269 .setMaxLines(mMaxLines) 4270 .setBreakStrategy(mBreakStrategy) 4271 .setHyphenationFrequency(mHyphenationFrequency) 4272 .setIndents(mLeftIndents, mRightIndents) 4273 .setJustificationMode(mJustificationMode) 4274 .setLineBreakConfig(mLineBreakConfig) 4275 .setUseBoundsForWidth(mUseBoundsForWidth) 4276 .setShiftDrawingOffsetForStartOverhang(mShiftDrawingOffsetForStartOverhang) 4277 .build(); 4278 } else { 4279 return new BoringLayout( 4280 mText, mPaint, mWidth, mAlignment, mTextDir, mSpacingMult, mSpacingAdd, 4281 mIncludePad, mFallbackLineSpacing, mEllipsizedWidth, mEllipsize, mMaxLines, 4282 mBreakStrategy, mHyphenationFrequency, mLeftIndents, mRightIndents, 4283 mJustificationMode, mLineBreakConfig, metrics, mUseBoundsForWidth, 4284 mShiftDrawingOffsetForStartOverhang, mMinimumFontMetrics); 4285 } 4286 } 4287 4288 private final CharSequence mText; 4289 private final int mStart; 4290 private final int mEnd; 4291 private final TextPaint mPaint; 4292 private final int mWidth; 4293 private Alignment mAlignment = Alignment.ALIGN_NORMAL; 4294 private float mSpacingMult = 1.0f; 4295 private float mSpacingAdd = 0.0f; 4296 private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; 4297 private boolean mIncludePad = true; 4298 private boolean mFallbackLineSpacing = false; 4299 private int mEllipsizedWidth; 4300 private TextUtils.TruncateAt mEllipsize = null; 4301 private int mMaxLines = Integer.MAX_VALUE; 4302 private int mBreakStrategy = BREAK_STRATEGY_SIMPLE; 4303 private int mHyphenationFrequency = HYPHENATION_FREQUENCY_NONE; 4304 private int[] mLeftIndents = null; 4305 private int[] mRightIndents = null; 4306 private int mJustificationMode = JUSTIFICATION_MODE_NONE; 4307 private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE; 4308 private boolean mUseBoundsForWidth; 4309 private boolean mShiftDrawingOffsetForStartOverhang; 4310 private Paint.FontMetrics mMinimumFontMetrics; 4311 } 4312 4313 /////////////////////////////////////////////////////////////////////////////////////////////// 4314 // Getters of parameters that is used for building Layout instance 4315 /////////////////////////////////////////////////////////////////////////////////////////////// 4316 4317 // TODO(316208691): Revive following removed API docs. 4318 // @see Layout.Builder 4319 /** 4320 * Return the text used for creating this layout. 4321 * 4322 * @return the text used for creating this layout. 4323 */ 4324 @NonNull getText()4325 public final CharSequence getText() { 4326 return mText; 4327 } 4328 4329 // TODO(316208691): Revive following removed API docs. 4330 // @see Layout.Builder 4331 /** 4332 * Return the paint used for creating this layout. 4333 * 4334 * Do not modify the returned paint object. This paint object will still be used for 4335 * drawing/measuring text. 4336 * 4337 * @return the paint used for creating this layout. 4338 */ 4339 @NonNull getPaint()4340 public final TextPaint getPaint() { 4341 return mPaint; 4342 } 4343 4344 // TODO(316208691): Revive following removed API docs. 4345 // @see Layout.Builder 4346 /** 4347 * Return the width used for creating this layout in pixels. 4348 * 4349 * @return the width used for creating this layout in pixels. 4350 */ 4351 @IntRange(from = 0) getWidth()4352 public final int getWidth() { 4353 return mWidth; 4354 } 4355 4356 // TODO(316208691): Revive following removed API docs. 4357 // @see Layout.Builder#setAlignment(Alignment) 4358 /** 4359 * Returns the alignment used for creating this layout in pixels. 4360 * 4361 * @return the alignment used for creating this layout. 4362 * @see StaticLayout.Builder#setAlignment(Alignment) 4363 */ 4364 @NonNull getAlignment()4365 public final Alignment getAlignment() { 4366 return mAlignment; 4367 } 4368 4369 /** 4370 * Returns the text direction heuristic used for creating this layout. 4371 * 4372 * @return the text direction heuristic used for creating this layout 4373 * @see Layout.Builder#setTextDirectionHeuristic(TextDirectionHeuristic) 4374 * @see StaticLayout.Builder#setTextDirection(TextDirectionHeuristic) 4375 */ 4376 @NonNull 4377 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getTextDirectionHeuristic()4378 public final TextDirectionHeuristic getTextDirectionHeuristic() { 4379 return mTextDir; 4380 } 4381 4382 // TODO(316208691): Revive following removed API docs. 4383 // This is an alias of {@link #getLineSpacingMultiplier}. 4384 // @see Layout.Builder#setLineSpacingMultiplier(float) 4385 // @see Layout#getLineSpacingMultiplier() 4386 /** 4387 * Returns the multiplier applied to the line height. 4388 * 4389 * @return the line height multiplier. 4390 * @see StaticLayout.Builder#setLineSpacing(float, float) 4391 */ getSpacingMultiplier()4392 public final float getSpacingMultiplier() { 4393 return getLineSpacingMultiplier(); 4394 } 4395 4396 /** 4397 * Returns the multiplier applied to the line height. 4398 * 4399 * @return the line height multiplier. 4400 * @see Layout.Builder#setLineSpacingMultiplier(float) 4401 * @see StaticLayout.Builder#setLineSpacing(float, float) 4402 * @see Layout#getSpacingMultiplier() 4403 */ 4404 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getLineSpacingMultiplier()4405 public final float getLineSpacingMultiplier() { 4406 return mSpacingMult; 4407 } 4408 4409 // TODO(316208691): Revive following removed API docs. 4410 // This is an alias of {@link #getLineSpacingAmount()}. 4411 // @see Layout.Builder#setLineSpacingAmount(float) 4412 // @see Layout#getLineSpacingAmount() 4413 /** 4414 * Returns the amount added to the line height. 4415 * 4416 * @return the line height additional amount. 4417 * @see StaticLayout.Builder#setLineSpacing(float, float) 4418 */ getSpacingAdd()4419 public final float getSpacingAdd() { 4420 return getLineSpacingAmount(); 4421 } 4422 4423 /** 4424 * Returns the amount added to the line height. 4425 * 4426 * @return the line height additional amount. 4427 * @see Layout.Builder#setLineSpacingAmount(float) 4428 * @see StaticLayout.Builder#setLineSpacing(float, float) 4429 * @see Layout#getSpacingAdd() 4430 */ 4431 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getLineSpacingAmount()4432 public final float getLineSpacingAmount() { 4433 return mSpacingAdd; 4434 } 4435 4436 /** 4437 * Returns true if this layout is created with increased line height. 4438 * 4439 * @return true if the layout is created with increased line height. 4440 * @see Layout.Builder#setFontPaddingIncluded(boolean) 4441 * @see StaticLayout.Builder#setIncludePad(boolean) 4442 */ 4443 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) isFontPaddingIncluded()4444 public final boolean isFontPaddingIncluded() { 4445 return mIncludePad; 4446 } 4447 4448 // TODO(316208691): Revive following removed API docs. 4449 // @see Layout.Builder#setFallbackLineSpacingEnabled(boolean) 4450 /** 4451 * Return true if the fallback line space is enabled in this Layout. 4452 * 4453 * @return true if the fallback line space is enabled. Otherwise, returns false. 4454 * @see StaticLayout.Builder#setUseLineSpacingFromFallbacks(boolean) 4455 */ 4456 // not being final because of already published API. isFallbackLineSpacingEnabled()4457 public boolean isFallbackLineSpacingEnabled() { 4458 return mFallbackLineSpacing; 4459 } 4460 4461 // TODO(316208691): Revive following removed API docs. 4462 // @see Layout.Builder#setEllipsizedWidth(int) 4463 // @see Layout.Builder#setEllipsize(TextUtils.TruncateAt) 4464 // @see Layout#getEllipsize() 4465 /** 4466 * Return the width to which this layout is ellipsized. 4467 * 4468 * If no ellipsize is applied, the same amount of {@link #getWidth} is returned. 4469 * 4470 * @return the amount of ellipsized width in pixels. 4471 * @see StaticLayout.Builder#setEllipsizedWidth(int) 4472 * @see StaticLayout.Builder#setEllipsize(TextUtils.TruncateAt) 4473 */ 4474 @IntRange(from = 0) getEllipsizedWidth()4475 public int getEllipsizedWidth() { // not being final because of already published API. 4476 return mEllipsizedWidth; 4477 } 4478 4479 /** 4480 * Return the ellipsize option used for creating this layout. 4481 * 4482 * May return null if no ellipsize option was selected. 4483 * 4484 * @return The ellipsize option used for creating this layout, or null if no ellipsize option 4485 * was selected. 4486 * @see Layout.Builder#setEllipsize(TextUtils.TruncateAt) 4487 * @see StaticLayout.Builder#setEllipsize(TextUtils.TruncateAt) 4488 * @see Layout.Builder#setEllipsizedWidth(int) 4489 * @see StaticLayout.Builder#setEllipsizedWidth(int) 4490 * @see Layout#getEllipsizedWidth() 4491 */ 4492 @Nullable 4493 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getEllipsize()4494 public final TextUtils.TruncateAt getEllipsize() { 4495 return mEllipsize; 4496 } 4497 4498 /** 4499 * Return the maximum lines allowed used for creating this layout. 4500 * 4501 * Note that this is not an actual line count of this layout. Use {@link #getLineCount()} for 4502 * getting the actual line count of this layout. 4503 * 4504 * @return the maximum lines allowed used for creating this layout. 4505 * @see Layout.Builder#setMaxLines(int) 4506 * @see StaticLayout.Builder#setMaxLines(int) 4507 */ 4508 @IntRange(from = 1) 4509 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getMaxLines()4510 public final int getMaxLines() { 4511 return mMaxLines; 4512 } 4513 4514 /** 4515 * Return the break strategy used for creating this layout. 4516 * 4517 * @return the break strategy used for creating this layout. 4518 * @see Layout.Builder#setBreakStrategy(int) 4519 * @see StaticLayout.Builder#setBreakStrategy(int) 4520 */ 4521 @BreakStrategy 4522 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getBreakStrategy()4523 public final int getBreakStrategy() { 4524 return mBreakStrategy; 4525 } 4526 4527 /** 4528 * Return the hyphenation frequency used for creating this layout. 4529 * 4530 * @return the hyphenation frequency used for creating this layout. 4531 * @see Layout.Builder#setHyphenationFrequency(int) 4532 * @see StaticLayout.Builder#setHyphenationFrequency(int) 4533 */ 4534 @HyphenationFrequency 4535 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getHyphenationFrequency()4536 public final int getHyphenationFrequency() { 4537 return mHyphenationFrequency; 4538 } 4539 4540 /** 4541 * Return a copy of the left indents used for this layout. 4542 * 4543 * May return null if no left indentation is applied. 4544 * 4545 * @return the array of left indents in pixels. 4546 * @see Layout.Builder#setLeftIndents(int[]) 4547 * @see Layout.Builder#setRightIndents(int[]) 4548 * @see StaticLayout.Builder#setIndents(int[], int[]) 4549 */ 4550 @Nullable 4551 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getLeftIndents()4552 public final int[] getLeftIndents() { 4553 if (mLeftIndents == null) { 4554 return null; 4555 } 4556 int[] newArray = new int[mLeftIndents.length]; 4557 System.arraycopy(mLeftIndents, 0, newArray, 0, newArray.length); 4558 return newArray; 4559 } 4560 4561 /** 4562 * Return a copy of the right indents used for this layout. 4563 * 4564 * May return null if no right indentation is applied. 4565 * 4566 * @return the array of right indents in pixels. 4567 * @see Layout.Builder#setLeftIndents(int[]) 4568 * @see Layout.Builder#setRightIndents(int[]) 4569 * @see StaticLayout.Builder#setIndents(int[], int[]) 4570 */ 4571 @Nullable 4572 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getRightIndents()4573 public final int[] getRightIndents() { 4574 if (mRightIndents == null) { 4575 return null; 4576 } 4577 int[] newArray = new int[mRightIndents.length]; 4578 System.arraycopy(mRightIndents, 0, newArray, 0, newArray.length); 4579 return newArray; 4580 } 4581 4582 /** 4583 * Return the justification mode used for creating this layout. 4584 * 4585 * @return the justification mode used for creating this layout. 4586 * @see Layout.Builder#setJustificationMode(int) 4587 * @see StaticLayout.Builder#setJustificationMode(int) 4588 */ 4589 @JustificationMode 4590 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getJustificationMode()4591 public final int getJustificationMode() { 4592 return mJustificationMode; 4593 } 4594 4595 /** 4596 * Gets the {@link LineBreakConfig} used for creating this layout. 4597 * 4598 * Do not modify the returned object. 4599 * 4600 * @return The line break config used for creating this layout. 4601 */ 4602 // not being final because of subclass has already published API. 4603 @NonNull 4604 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getLineBreakConfig()4605 public LineBreakConfig getLineBreakConfig() { 4606 return mLineBreakConfig; 4607 } 4608 4609 /** 4610 * Returns true if using bounding box as a width, false for using advance as a width. 4611 * 4612 * @return True if using bounding box for width, false if using advance for width. 4613 * @see android.widget.TextView#setUseBoundsForWidth(boolean) 4614 * @see android.widget.TextView#getUseBoundsForWidth() 4615 * @see StaticLayout.Builder#setUseBoundsForWidth(boolean) 4616 * @see DynamicLayout.Builder#setUseBoundsForWidth(boolean) 4617 */ 4618 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getUseBoundsForWidth()4619 public boolean getUseBoundsForWidth() { 4620 return mUseBoundsForWidth; 4621 } 4622 4623 /** 4624 * Returns true if shifting drawing offset for start overhang. 4625 * 4626 * @return True if shifting drawing offset for start overhang. 4627 * @see android.widget.TextView#setShiftDrawingOffsetForStartOverhang(boolean) 4628 * @see TextView#getShiftDrawingOffsetForStartOverhang() 4629 * @see StaticLayout.Builder#setShiftDrawingOffsetForStartOverhang(boolean) 4630 * @see DynamicLayout.Builder#setShiftDrawingOffsetForStartOverhang(boolean) 4631 */ 4632 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) getShiftDrawingOffsetForStartOverhang()4633 public boolean getShiftDrawingOffsetForStartOverhang() { 4634 return mShiftDrawingOffsetForStartOverhang; 4635 } 4636 4637 /** 4638 * Get the minimum font metrics used for line spacing. 4639 * 4640 * @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics) 4641 * @see android.widget.TextView#getMinimumFontMetrics() 4642 * @see Layout.Builder#setMinimumFontMetrics(Paint.FontMetrics) 4643 * @see StaticLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) 4644 * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) 4645 * 4646 * @return a minimum font metrics. {@code null} for using the value obtained by 4647 * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} 4648 */ 4649 @Nullable 4650 @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) getMinimumFontMetrics()4651 public Paint.FontMetrics getMinimumFontMetrics() { 4652 return mMinimumFontMetrics; 4653 } 4654 4655 /** 4656 * Callback for {@link #forEachCharacterBounds(int, int, int, int, CharacterBoundsListener)} 4657 */ 4658 private interface CharacterBoundsListener { 4659 /** 4660 * Called for each character with its bounds. 4661 * 4662 * @param index the index of the character 4663 * @param lineNum the line number of the character 4664 * @param left the left edge of the character 4665 * @param top the top edge of the character 4666 * @param right the right edge of the character 4667 * @param bottom the bottom edge of the character 4668 */ 4669 void onCharacterBounds(int index, int lineNum, float left, float top, float right, 4670 float bottom); 4671 4672 /** Called after the last character has been sent to {@link #onCharacterBounds}. */ onEnd()4673 default void onEnd() {} 4674 } 4675 } 4676