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