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 android.annotation.IntDef; 20 import android.annotation.IntRange; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.compat.annotation.UnsupportedAppUsage; 24 import android.graphics.Canvas; 25 import android.graphics.Paint; 26 import android.graphics.Path; 27 import android.graphics.Rect; 28 import android.graphics.RectF; 29 import android.graphics.text.LineBreaker; 30 import android.os.Build; 31 import android.text.method.TextKeyListener; 32 import android.text.style.AlignmentSpan; 33 import android.text.style.LeadingMarginSpan; 34 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; 35 import android.text.style.LineBackgroundSpan; 36 import android.text.style.ParagraphStyle; 37 import android.text.style.ReplacementSpan; 38 import android.text.style.TabStopSpan; 39 40 import com.android.internal.annotations.VisibleForTesting; 41 import com.android.internal.util.ArrayUtils; 42 import com.android.internal.util.GrowingArrayUtils; 43 44 import java.lang.annotation.Retention; 45 import java.lang.annotation.RetentionPolicy; 46 import java.util.Arrays; 47 import java.util.List; 48 49 /** 50 * A base class that manages text layout in visual elements on 51 * the screen. 52 * <p>For text that will be edited, use a {@link DynamicLayout}, 53 * which will be updated as the text changes. 54 * For text that will not change, use a {@link StaticLayout}. 55 */ 56 public abstract class Layout { 57 /** @hide */ 58 @IntDef(prefix = { "BREAK_STRATEGY_" }, value = { 59 LineBreaker.BREAK_STRATEGY_SIMPLE, 60 LineBreaker.BREAK_STRATEGY_HIGH_QUALITY, 61 LineBreaker.BREAK_STRATEGY_BALANCED 62 }) 63 @Retention(RetentionPolicy.SOURCE) 64 public @interface BreakStrategy {} 65 66 /** 67 * Value for break strategy indicating simple line breaking. Automatic hyphens are not added 68 * (though soft hyphens are respected), and modifying text generally doesn't affect the layout 69 * before it (which yields a more consistent user experience when editing), but layout may not 70 * be the highest quality. 71 */ 72 public static final int BREAK_STRATEGY_SIMPLE = LineBreaker.BREAK_STRATEGY_SIMPLE; 73 74 /** 75 * Value for break strategy indicating high quality line breaking, including automatic 76 * hyphenation and doing whole-paragraph optimization of line breaks. 77 */ 78 public static final int BREAK_STRATEGY_HIGH_QUALITY = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY; 79 80 /** 81 * Value for break strategy indicating balanced line breaking. The breaks are chosen to 82 * make all lines as close to the same length as possible, including automatic hyphenation. 83 */ 84 public static final int BREAK_STRATEGY_BALANCED = LineBreaker.BREAK_STRATEGY_BALANCED; 85 86 /** @hide */ 87 @IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = { 88 HYPHENATION_FREQUENCY_NORMAL, 89 HYPHENATION_FREQUENCY_NORMAL_FAST, 90 HYPHENATION_FREQUENCY_FULL, 91 HYPHENATION_FREQUENCY_FULL_FAST, 92 HYPHENATION_FREQUENCY_NONE 93 }) 94 @Retention(RetentionPolicy.SOURCE) 95 public @interface HyphenationFrequency {} 96 97 /** 98 * Value for hyphenation frequency indicating no automatic hyphenation. Useful 99 * for backward compatibility, and for cases where the automatic hyphenation algorithm results 100 * in incorrect hyphenation. Mid-word breaks may still happen when a word is wider than the 101 * layout and there is otherwise no valid break. Soft hyphens are ignored and will not be used 102 * as suggestions for potential line breaks. 103 */ 104 public static final int HYPHENATION_FREQUENCY_NONE = 0; 105 106 /** 107 * Value for hyphenation frequency indicating a light amount of automatic hyphenation, which 108 * is a conservative default. Useful for informal cases, such as short sentences or chat 109 * messages. 110 */ 111 public static final int HYPHENATION_FREQUENCY_NORMAL = 1; 112 113 /** 114 * Value for hyphenation frequency indicating the full amount of automatic hyphenation, typical 115 * in typography. Useful for running text and where it's important to put the maximum amount of 116 * text in a screen with limited space. 117 */ 118 public static final int HYPHENATION_FREQUENCY_FULL = 2; 119 120 /** 121 * Value for hyphenation frequency indicating a light amount of automatic hyphenation with 122 * using faster algorithm. 123 * 124 * This option is useful for informal cases, such as short sentences or chat messages. To make 125 * text rendering faster with hyphenation, this algorithm ignores some hyphen character related 126 * typographic features, e.g. kerning. 127 */ 128 public static final int HYPHENATION_FREQUENCY_NORMAL_FAST = 3; 129 /** 130 * Value for hyphenation frequency indicating the full amount of automatic hyphenation with 131 * using faster algorithm. 132 * 133 * This option is useful for running text and where it's important to put the maximum amount of 134 * text in a screen with limited space. To make text rendering faster with hyphenation, this 135 * algorithm ignores some hyphen character related typographic features, e.g. kerning. 136 */ 137 public static final int HYPHENATION_FREQUENCY_FULL_FAST = 4; 138 139 private static final ParagraphStyle[] NO_PARA_SPANS = 140 ArrayUtils.emptyArray(ParagraphStyle.class); 141 142 /** @hide */ 143 @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = { 144 LineBreaker.JUSTIFICATION_MODE_NONE, 145 LineBreaker.JUSTIFICATION_MODE_INTER_WORD 146 }) 147 @Retention(RetentionPolicy.SOURCE) 148 public @interface JustificationMode {} 149 150 /** 151 * Value for justification mode indicating no justification. 152 */ 153 public static final int JUSTIFICATION_MODE_NONE = LineBreaker.JUSTIFICATION_MODE_NONE; 154 155 /** 156 * Value for justification mode indicating the text is justified by stretching word spacing. 157 */ 158 public static final int JUSTIFICATION_MODE_INTER_WORD = 159 LineBreaker.JUSTIFICATION_MODE_INTER_WORD; 160 161 /* 162 * Line spacing multiplier for default line spacing. 163 */ 164 public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f; 165 166 /* 167 * Line spacing addition for default line spacing. 168 */ 169 public static final float DEFAULT_LINESPACING_ADDITION = 0.0f; 170 171 /** 172 * Strategy which considers a text segment to be inside a rectangle area if the segment bounds 173 * intersect the rectangle. 174 */ 175 @NonNull 176 public static final TextInclusionStrategy INCLUSION_STRATEGY_ANY_OVERLAP = 177 RectF::intersects; 178 179 /** 180 * Strategy which considers a text segment to be inside a rectangle area if the center of the 181 * segment bounds is inside the rectangle. 182 */ 183 @NonNull 184 public static final TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_CENTER = 185 (segmentBounds, area) -> 186 area.contains(segmentBounds.centerX(), segmentBounds.centerY()); 187 188 /** 189 * Strategy which considers a text segment to be inside a rectangle area if the segment bounds 190 * are completely contained within the rectangle. 191 */ 192 @NonNull 193 public static final TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_ALL = 194 (segmentBounds, area) -> area.contains(segmentBounds); 195 196 /** 197 * Return how wide a layout must be in order to display the specified text with one line per 198 * paragraph. 199 * 200 * <p>As of O, Uses 201 * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In 202 * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p> 203 */ getDesiredWidth(CharSequence source, TextPaint paint)204 public static float getDesiredWidth(CharSequence source, 205 TextPaint paint) { 206 return getDesiredWidth(source, 0, source.length(), paint); 207 } 208 209 /** 210 * Return how wide a layout must be in order to display the specified text slice with one 211 * line per paragraph. 212 * 213 * <p>As of O, Uses 214 * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR} as the default text direction heuristics. In 215 * the earlier versions uses {@link TextDirectionHeuristics#LTR} as the default.</p> 216 */ getDesiredWidth(CharSequence source, int start, int end, TextPaint paint)217 public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint) { 218 return getDesiredWidth(source, start, end, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR); 219 } 220 221 /** 222 * Return how wide a layout must be in order to display the 223 * specified text slice with one line per paragraph. 224 * 225 * @hide 226 */ getDesiredWidth(CharSequence source, int start, int end, TextPaint paint, TextDirectionHeuristic textDir)227 public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint, 228 TextDirectionHeuristic textDir) { 229 return getDesiredWidthWithLimit(source, start, end, paint, textDir, Float.MAX_VALUE); 230 } 231 /** 232 * Return how wide a layout must be in order to display the 233 * specified text slice with one line per paragraph. 234 * 235 * If the measured width exceeds given limit, returns limit value instead. 236 * @hide 237 */ getDesiredWidthWithLimit(CharSequence source, int start, int end, TextPaint paint, TextDirectionHeuristic textDir, float upperLimit)238 public static float getDesiredWidthWithLimit(CharSequence source, int start, int end, 239 TextPaint paint, TextDirectionHeuristic textDir, float upperLimit) { 240 float need = 0; 241 242 int next; 243 for (int i = start; i <= end; i = next) { 244 next = TextUtils.indexOf(source, '\n', i, end); 245 246 if (next < 0) 247 next = end; 248 249 // note, omits trailing paragraph char 250 float w = measurePara(paint, source, i, next, textDir); 251 if (w > upperLimit) { 252 return upperLimit; 253 } 254 255 if (w > need) 256 need = w; 257 258 next++; 259 } 260 261 return need; 262 } 263 264 /** 265 * Subclasses of Layout use this constructor to set the display text, 266 * width, and other standard properties. 267 * @param text the text to render 268 * @param paint the default paint for the layout. Styles can override 269 * various attributes of the paint. 270 * @param width the wrapping width for the text. 271 * @param align whether to left, right, or center the text. Styles can 272 * override the alignment. 273 * @param spacingMult factor by which to scale the font size to get the 274 * default line spacing 275 * @param spacingAdd amount to add to the default line spacing 276 */ Layout(CharSequence text, TextPaint paint, int width, Alignment align, float spacingMult, float spacingAdd)277 protected Layout(CharSequence text, TextPaint paint, 278 int width, Alignment align, 279 float spacingMult, float spacingAdd) { 280 this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, 281 spacingMult, spacingAdd); 282 } 283 284 /** 285 * Subclasses of Layout use this constructor to set the display text, 286 * width, and other standard properties. 287 * @param text the text to render 288 * @param paint the default paint for the layout. Styles can override 289 * various attributes of the paint. 290 * @param width the wrapping width for the text. 291 * @param align whether to left, right, or center the text. Styles can 292 * override the alignment. 293 * @param spacingMult factor by which to scale the font size to get the 294 * default line spacing 295 * @param spacingAdd amount to add to the default line spacing 296 * 297 * @hide 298 */ Layout(CharSequence text, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingMult, float spacingAdd)299 protected Layout(CharSequence text, TextPaint paint, 300 int width, Alignment align, TextDirectionHeuristic textDir, 301 float spacingMult, float spacingAdd) { 302 303 if (width < 0) 304 throw new IllegalArgumentException("Layout: " + width + " < 0"); 305 306 // Ensure paint doesn't have baselineShift set. 307 // While normally we don't modify the paint the user passed in, 308 // we were already doing this in Styled.drawUniformRun with both 309 // baselineShift and bgColor. We probably should reevaluate bgColor. 310 if (paint != null) { 311 paint.bgColor = 0; 312 paint.baselineShift = 0; 313 } 314 315 mText = text; 316 mPaint = paint; 317 mWidth = width; 318 mAlignment = align; 319 mSpacingMult = spacingMult; 320 mSpacingAdd = spacingAdd; 321 mSpannedText = text instanceof Spanned; 322 mTextDir = textDir; 323 } 324 325 /** @hide */ setJustificationMode(@ustificationMode int justificationMode)326 protected void setJustificationMode(@JustificationMode int justificationMode) { 327 mJustificationMode = justificationMode; 328 } 329 330 /** 331 * Replace constructor properties of this Layout with new ones. Be careful. 332 */ replaceWith(CharSequence text, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd)333 /* package */ void replaceWith(CharSequence text, TextPaint paint, 334 int width, Alignment align, 335 float spacingmult, float spacingadd) { 336 if (width < 0) { 337 throw new IllegalArgumentException("Layout: " + width + " < 0"); 338 } 339 340 mText = text; 341 mPaint = paint; 342 mWidth = width; 343 mAlignment = align; 344 mSpacingMult = spacingmult; 345 mSpacingAdd = spacingadd; 346 mSpannedText = text instanceof Spanned; 347 } 348 349 /** 350 * Draw this Layout on the specified Canvas. 351 * 352 * This API draws background first, then draws text on top of it. 353 * 354 * @see #draw(Canvas, List, List, Path, Paint, int) 355 */ draw(Canvas c)356 public void draw(Canvas c) { 357 draw(c, (Path) null, (Paint) null, 0); 358 } 359 360 /** 361 * Draw this Layout on the specified canvas, with the highlight path drawn 362 * between the background and the text. 363 * 364 * @param canvas the canvas 365 * @param selectionHighlight the path of the selection highlight or cursor; can be null 366 * @param selectionHighlightPaint the paint for the selection highlight 367 * @param cursorOffsetVertical the amount to temporarily translate the 368 * canvas while rendering the highlight 369 * 370 * @see #draw(Canvas, List, List, Path, Paint, int) 371 */ draw( Canvas canvas, Path selectionHighlight, Paint selectionHighlightPaint, int cursorOffsetVertical)372 public void draw( 373 Canvas canvas, Path selectionHighlight, 374 Paint selectionHighlightPaint, int cursorOffsetVertical) { 375 draw(canvas, null, null, selectionHighlight, selectionHighlightPaint, cursorOffsetVertical); 376 } 377 378 /** 379 * Draw this layout on the specified canvas. 380 * 381 * This API draws background first, then draws highlight paths on top of it, then draws 382 * selection or cursor, then finally draws text on top of it. 383 * 384 * @see #drawBackground(Canvas) 385 * @see #drawText(Canvas) 386 * 387 * @param canvas the canvas 388 * @param highlightPaths the path of the highlights. The highlightPaths and highlightPaints must 389 * have the same length and aligned in the same order. For example, the 390 * paint of the n-th of the highlightPaths should be stored at the n-th of 391 * highlightPaints. 392 * @param highlightPaints the paints for the highlights. The highlightPaths and highlightPaints 393 * must have the same length and aligned in the same order. For example, 394 * the paint of the n-th of the highlightPaths should be stored at the 395 * n-th of highlightPaints. 396 * @param selectionPath the selection or cursor path 397 * @param selectionPaint the paint for the selection or cursor. 398 * @param cursorOffsetVertical the amount to temporarily translate the canvas while rendering 399 * the highlight 400 */ draw(@onNull Canvas canvas, @Nullable List<Path> highlightPaths, @Nullable List<Paint> highlightPaints, @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical)401 public void draw(@NonNull Canvas canvas, 402 @Nullable List<Path> highlightPaths, 403 @Nullable List<Paint> highlightPaints, 404 @Nullable Path selectionPath, 405 @Nullable Paint selectionPaint, 406 int cursorOffsetVertical) { 407 final long lineRange = getLineRangeForDraw(canvas); 408 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); 409 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); 410 if (lastLine < 0) return; 411 412 drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint, 413 cursorOffsetVertical, firstLine, lastLine); 414 drawText(canvas, firstLine, lastLine); 415 } 416 417 /** 418 * Draw text part of this layout. 419 * 420 * Different from {@link #draw(Canvas, List, List, Path, Paint, int)} API, this API only draws 421 * text part, not drawing highlights, selections, or backgrounds. 422 * 423 * @see #draw(Canvas, List, List, Path, Paint, int) 424 * @see #drawBackground(Canvas) 425 * 426 * @param canvas the canvas 427 */ drawText(@onNull Canvas canvas)428 public void drawText(@NonNull Canvas canvas) { 429 final long lineRange = getLineRangeForDraw(canvas); 430 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); 431 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); 432 if (lastLine < 0) return; 433 drawText(canvas, firstLine, lastLine); 434 } 435 436 /** 437 * Draw background of this layout. 438 * 439 * Different from {@link #draw(Canvas, List, List, Path, Paint, int)} API, this API only draws 440 * background, not drawing text, highlights or selections. The background here is drawn by 441 * {@link LineBackgroundSpan} attached to the text. 442 * 443 * @see #draw(Canvas, List, List, Path, Paint, int) 444 * @see #drawText(Canvas) 445 * 446 * @param canvas the canvas 447 */ drawBackground(@onNull Canvas canvas)448 public void drawBackground(@NonNull Canvas canvas) { 449 final long lineRange = getLineRangeForDraw(canvas); 450 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); 451 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); 452 if (lastLine < 0) return; 453 drawBackground(canvas, firstLine, lastLine); 454 } 455 456 /** 457 * @hide public for Editor.java 458 */ drawWithoutText( @onNull Canvas canvas, @Nullable List<Path> highlightPaths, @Nullable List<Paint> highlightPaints, @Nullable Path selectionPath, @Nullable Paint selectionPaint, int cursorOffsetVertical, int firstLine, int lastLine)459 public void drawWithoutText( 460 @NonNull Canvas canvas, 461 @Nullable List<Path> highlightPaths, 462 @Nullable List<Paint> highlightPaints, 463 @Nullable Path selectionPath, 464 @Nullable Paint selectionPaint, 465 int cursorOffsetVertical, 466 int firstLine, 467 int lastLine) { 468 drawBackground(canvas, firstLine, lastLine); 469 if (highlightPaths == null && highlightPaints == null) { 470 return; 471 } 472 if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical); 473 try { 474 if (highlightPaths != null) { 475 if (highlightPaints == null) { 476 throw new IllegalArgumentException( 477 "if highlight is specified, highlightPaint must be specified."); 478 } 479 if (highlightPaints.size() != highlightPaths.size()) { 480 throw new IllegalArgumentException( 481 "The highlight path size is different from the size of highlight" 482 + " paints"); 483 } 484 for (int i = 0; i < highlightPaths.size(); ++i) { 485 final Path highlight = highlightPaths.get(i); 486 final Paint highlightPaint = highlightPaints.get(i); 487 if (highlight != null) { 488 canvas.drawPath(highlight, highlightPaint); 489 } 490 } 491 } 492 493 if (selectionPath != null) { 494 canvas.drawPath(selectionPath, selectionPaint); 495 } 496 } finally { 497 if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical); 498 } 499 } 500 isJustificationRequired(int lineNum)501 private boolean isJustificationRequired(int lineNum) { 502 if (mJustificationMode == JUSTIFICATION_MODE_NONE) return false; 503 final int lineEnd = getLineEnd(lineNum); 504 return lineEnd < mText.length() && mText.charAt(lineEnd - 1) != '\n'; 505 } 506 getJustifyWidth(int lineNum)507 private float getJustifyWidth(int lineNum) { 508 Alignment paraAlign = mAlignment; 509 510 int left = 0; 511 int right = mWidth; 512 513 final int dir = getParagraphDirection(lineNum); 514 515 ParagraphStyle[] spans = NO_PARA_SPANS; 516 if (mSpannedText) { 517 Spanned sp = (Spanned) mText; 518 final int start = getLineStart(lineNum); 519 520 final boolean isFirstParaLine = (start == 0 || mText.charAt(start - 1) == '\n'); 521 522 if (isFirstParaLine) { 523 final int spanEnd = sp.nextSpanTransition(start, mText.length(), 524 ParagraphStyle.class); 525 spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class); 526 527 for (int n = spans.length - 1; n >= 0; n--) { 528 if (spans[n] instanceof AlignmentSpan) { 529 paraAlign = ((AlignmentSpan) spans[n]).getAlignment(); 530 break; 531 } 532 } 533 } 534 535 final int length = spans.length; 536 boolean useFirstLineMargin = isFirstParaLine; 537 for (int n = 0; n < length; n++) { 538 if (spans[n] instanceof LeadingMarginSpan2) { 539 int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount(); 540 int startLine = getLineForOffset(sp.getSpanStart(spans[n])); 541 if (lineNum < startLine + count) { 542 useFirstLineMargin = true; 543 break; 544 } 545 } 546 } 547 for (int n = 0; n < length; n++) { 548 if (spans[n] instanceof LeadingMarginSpan) { 549 LeadingMarginSpan margin = (LeadingMarginSpan) spans[n]; 550 if (dir == DIR_RIGHT_TO_LEFT) { 551 right -= margin.getLeadingMargin(useFirstLineMargin); 552 } else { 553 left += margin.getLeadingMargin(useFirstLineMargin); 554 } 555 } 556 } 557 } 558 559 final Alignment align; 560 if (paraAlign == Alignment.ALIGN_LEFT) { 561 align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE; 562 } else if (paraAlign == Alignment.ALIGN_RIGHT) { 563 align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL; 564 } else { 565 align = paraAlign; 566 } 567 568 final int indentWidth; 569 if (align == Alignment.ALIGN_NORMAL) { 570 if (dir == DIR_LEFT_TO_RIGHT) { 571 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT); 572 } else { 573 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT); 574 } 575 } else if (align == Alignment.ALIGN_OPPOSITE) { 576 if (dir == DIR_LEFT_TO_RIGHT) { 577 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT); 578 } else { 579 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT); 580 } 581 } else { // Alignment.ALIGN_CENTER 582 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER); 583 } 584 585 return right - left - indentWidth; 586 } 587 588 /** 589 * @hide 590 */ 591 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) drawText(Canvas canvas, int firstLine, int lastLine)592 public void drawText(Canvas canvas, int firstLine, int lastLine) { 593 int previousLineBottom = getLineTop(firstLine); 594 int previousLineEnd = getLineStart(firstLine); 595 ParagraphStyle[] spans = NO_PARA_SPANS; 596 int spanEnd = 0; 597 final TextPaint paint = mWorkPaint; 598 paint.set(mPaint); 599 CharSequence buf = mText; 600 601 Alignment paraAlign = mAlignment; 602 TabStops tabStops = null; 603 boolean tabStopsIsInitialized = false; 604 605 TextLine tl = TextLine.obtain(); 606 607 // Draw the lines, one at a time. 608 // The baseline is the top of the following line minus the current line's descent. 609 for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) { 610 int start = previousLineEnd; 611 previousLineEnd = getLineStart(lineNum + 1); 612 final boolean justify = isJustificationRequired(lineNum); 613 int end = getLineVisibleEnd(lineNum, start, previousLineEnd); 614 paint.setStartHyphenEdit(getStartHyphenEdit(lineNum)); 615 paint.setEndHyphenEdit(getEndHyphenEdit(lineNum)); 616 617 int ltop = previousLineBottom; 618 int lbottom = getLineTop(lineNum + 1); 619 previousLineBottom = lbottom; 620 int lbaseline = lbottom - getLineDescent(lineNum); 621 622 int dir = getParagraphDirection(lineNum); 623 int left = 0; 624 int right = mWidth; 625 626 if (mSpannedText) { 627 Spanned sp = (Spanned) buf; 628 int textLength = buf.length(); 629 boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n'); 630 631 // New batch of paragraph styles, collect into spans array. 632 // Compute the alignment, last alignment style wins. 633 // Reset tabStops, we'll rebuild if we encounter a line with 634 // tabs. 635 // We expect paragraph spans to be relatively infrequent, use 636 // spanEnd so that we can check less frequently. Since 637 // paragraph styles ought to apply to entire paragraphs, we can 638 // just collect the ones present at the start of the paragraph. 639 // If spanEnd is before the end of the paragraph, that's not 640 // our problem. 641 if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) { 642 spanEnd = sp.nextSpanTransition(start, textLength, 643 ParagraphStyle.class); 644 spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class); 645 646 paraAlign = mAlignment; 647 for (int n = spans.length - 1; n >= 0; n--) { 648 if (spans[n] instanceof AlignmentSpan) { 649 paraAlign = ((AlignmentSpan) spans[n]).getAlignment(); 650 break; 651 } 652 } 653 654 tabStopsIsInitialized = false; 655 } 656 657 // Draw all leading margin spans. Adjust left or right according 658 // to the paragraph direction of the line. 659 final int length = spans.length; 660 boolean useFirstLineMargin = isFirstParaLine; 661 for (int n = 0; n < length; n++) { 662 if (spans[n] instanceof LeadingMarginSpan2) { 663 int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount(); 664 int startLine = getLineForOffset(sp.getSpanStart(spans[n])); 665 // if there is more than one LeadingMarginSpan2, use 666 // the count that is greatest 667 if (lineNum < startLine + count) { 668 useFirstLineMargin = true; 669 break; 670 } 671 } 672 } 673 for (int n = 0; n < length; n++) { 674 if (spans[n] instanceof LeadingMarginSpan) { 675 LeadingMarginSpan margin = (LeadingMarginSpan) spans[n]; 676 if (dir == DIR_RIGHT_TO_LEFT) { 677 margin.drawLeadingMargin(canvas, paint, right, dir, ltop, 678 lbaseline, lbottom, buf, 679 start, end, isFirstParaLine, this); 680 right -= margin.getLeadingMargin(useFirstLineMargin); 681 } else { 682 margin.drawLeadingMargin(canvas, paint, left, dir, ltop, 683 lbaseline, lbottom, buf, 684 start, end, isFirstParaLine, this); 685 left += margin.getLeadingMargin(useFirstLineMargin); 686 } 687 } 688 } 689 } 690 691 boolean hasTab = getLineContainsTab(lineNum); 692 // Can't tell if we have tabs for sure, currently 693 if (hasTab && !tabStopsIsInitialized) { 694 if (tabStops == null) { 695 tabStops = new TabStops(TAB_INCREMENT, spans); 696 } else { 697 tabStops.reset(TAB_INCREMENT, spans); 698 } 699 tabStopsIsInitialized = true; 700 } 701 702 // Determine whether the line aligns to normal, opposite, or center. 703 Alignment align = paraAlign; 704 if (align == Alignment.ALIGN_LEFT) { 705 align = (dir == DIR_LEFT_TO_RIGHT) ? 706 Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE; 707 } else if (align == Alignment.ALIGN_RIGHT) { 708 align = (dir == DIR_LEFT_TO_RIGHT) ? 709 Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL; 710 } 711 712 int x; 713 final int indentWidth; 714 if (align == Alignment.ALIGN_NORMAL) { 715 if (dir == DIR_LEFT_TO_RIGHT) { 716 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT); 717 x = left + indentWidth; 718 } else { 719 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT); 720 x = right - indentWidth; 721 } 722 } else { 723 int max = (int)getLineExtent(lineNum, tabStops, false); 724 if (align == Alignment.ALIGN_OPPOSITE) { 725 if (dir == DIR_LEFT_TO_RIGHT) { 726 indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT); 727 x = right - max - indentWidth; 728 } else { 729 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT); 730 x = left - max + indentWidth; 731 } 732 } else { // Alignment.ALIGN_CENTER 733 indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER); 734 max = max & ~1; 735 x = ((right + left - max) >> 1) + indentWidth; 736 } 737 } 738 739 Directions directions = getLineDirections(lineNum); 740 if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) { 741 // XXX: assumes there's nothing additional to be done 742 canvas.drawText(buf, start, end, x, lbaseline, paint); 743 } else { 744 tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops, 745 getEllipsisStart(lineNum), 746 getEllipsisStart(lineNum) + getEllipsisCount(lineNum), 747 isFallbackLineSpacingEnabled()); 748 if (justify) { 749 tl.justify(right - left - indentWidth); 750 } 751 tl.draw(canvas, x, ltop, lbaseline, lbottom); 752 } 753 } 754 755 TextLine.recycle(tl); 756 } 757 758 /** 759 * @hide 760 */ 761 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) drawBackground( @onNull Canvas canvas, int firstLine, int lastLine)762 public void drawBackground( 763 @NonNull Canvas canvas, 764 int firstLine, int lastLine) { 765 // First, draw LineBackgroundSpans. 766 // LineBackgroundSpans know nothing about the alignment, margins, or 767 // direction of the layout or line. XXX: Should they? 768 // They are evaluated at each line. 769 if (mSpannedText) { 770 if (mLineBackgroundSpans == null) { 771 mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(LineBackgroundSpan.class); 772 } 773 774 Spanned buffer = (Spanned) mText; 775 int textLength = buffer.length(); 776 mLineBackgroundSpans.init(buffer, 0, textLength); 777 778 if (mLineBackgroundSpans.numberOfSpans > 0) { 779 int previousLineBottom = getLineTop(firstLine); 780 int previousLineEnd = getLineStart(firstLine); 781 ParagraphStyle[] spans = NO_PARA_SPANS; 782 int spansLength = 0; 783 TextPaint paint = mPaint; 784 int spanEnd = 0; 785 final int width = mWidth; 786 for (int i = firstLine; i <= lastLine; i++) { 787 int start = previousLineEnd; 788 int end = getLineStart(i + 1); 789 previousLineEnd = end; 790 791 int ltop = previousLineBottom; 792 int lbottom = getLineTop(i + 1); 793 previousLineBottom = lbottom; 794 int lbaseline = lbottom - getLineDescent(i); 795 796 if (end >= spanEnd) { 797 // These should be infrequent, so we'll use this so that 798 // we don't have to check as often. 799 spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength); 800 // All LineBackgroundSpans on a line contribute to its background. 801 spansLength = 0; 802 // Duplication of the logic of getParagraphSpans 803 if (start != end || start == 0) { 804 // Equivalent to a getSpans(start, end), but filling the 'spans' local 805 // array instead to reduce memory allocation 806 for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) { 807 // equal test is valid since both intervals are not empty by 808 // construction 809 if (mLineBackgroundSpans.spanStarts[j] >= end || 810 mLineBackgroundSpans.spanEnds[j] <= start) continue; 811 spans = GrowingArrayUtils.append( 812 spans, spansLength, mLineBackgroundSpans.spans[j]); 813 spansLength++; 814 } 815 } 816 } 817 818 for (int n = 0; n < spansLength; n++) { 819 LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n]; 820 lineBackgroundSpan.drawBackground(canvas, paint, 0, width, 821 ltop, lbaseline, lbottom, 822 buffer, start, end, i); 823 } 824 } 825 } 826 mLineBackgroundSpans.recycle(); 827 } 828 } 829 830 /** 831 * @param canvas 832 * @return The range of lines that need to be drawn, possibly empty. 833 * @hide 834 */ 835 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getLineRangeForDraw(Canvas canvas)836 public long getLineRangeForDraw(Canvas canvas) { 837 int dtop, dbottom; 838 839 synchronized (sTempRect) { 840 if (!canvas.getClipBounds(sTempRect)) { 841 // Negative range end used as a special flag 842 return TextUtils.packRangeInLong(0, -1); 843 } 844 845 dtop = sTempRect.top; 846 dbottom = sTempRect.bottom; 847 } 848 849 final int top = Math.max(dtop, 0); 850 final int bottom = Math.min(getLineTop(getLineCount()), dbottom); 851 852 if (top >= bottom) return TextUtils.packRangeInLong(0, -1); 853 return TextUtils.packRangeInLong(getLineForVertical(top), getLineForVertical(bottom)); 854 } 855 856 /** 857 * Return the start position of the line, given the left and right bounds of the margins. 858 * 859 * @param line the line index 860 * @param left the left bounds (0, or leading margin if ltr para) 861 * @param right the right bounds (width, minus leading margin if rtl para) 862 * @return the start position of the line (to right of line if rtl para) 863 */ getLineStartPos(int line, int left, int right)864 private int getLineStartPos(int line, int left, int right) { 865 // Adjust the point at which to start rendering depending on the 866 // alignment of the paragraph. 867 Alignment align = getParagraphAlignment(line); 868 int dir = getParagraphDirection(line); 869 870 if (align == Alignment.ALIGN_LEFT) { 871 align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE; 872 } else if (align == Alignment.ALIGN_RIGHT) { 873 align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL; 874 } 875 876 int x; 877 if (align == Alignment.ALIGN_NORMAL) { 878 if (dir == DIR_LEFT_TO_RIGHT) { 879 x = left + getIndentAdjust(line, Alignment.ALIGN_LEFT); 880 } else { 881 x = right + getIndentAdjust(line, Alignment.ALIGN_RIGHT); 882 } 883 } else { 884 TabStops tabStops = null; 885 if (mSpannedText && getLineContainsTab(line)) { 886 Spanned spanned = (Spanned) mText; 887 int start = getLineStart(line); 888 int spanEnd = spanned.nextSpanTransition(start, spanned.length(), 889 TabStopSpan.class); 890 TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd, 891 TabStopSpan.class); 892 if (tabSpans.length > 0) { 893 tabStops = new TabStops(TAB_INCREMENT, tabSpans); 894 } 895 } 896 int max = (int)getLineExtent(line, tabStops, false); 897 if (align == Alignment.ALIGN_OPPOSITE) { 898 if (dir == DIR_LEFT_TO_RIGHT) { 899 x = right - max + getIndentAdjust(line, Alignment.ALIGN_RIGHT); 900 } else { 901 // max is negative here 902 x = left - max + getIndentAdjust(line, Alignment.ALIGN_LEFT); 903 } 904 } else { // Alignment.ALIGN_CENTER 905 max = max & ~1; 906 x = (left + right - max) >> 1 + getIndentAdjust(line, Alignment.ALIGN_CENTER); 907 } 908 } 909 return x; 910 } 911 912 /** 913 * Return the text that is displayed by this Layout. 914 */ getText()915 public final CharSequence getText() { 916 return mText; 917 } 918 919 /** 920 * Return the base Paint properties for this layout. 921 * Do NOT change the paint, which may result in funny 922 * drawing for this layout. 923 */ getPaint()924 public final TextPaint getPaint() { 925 return mPaint; 926 } 927 928 /** 929 * Return the width of this layout. 930 */ getWidth()931 public final int getWidth() { 932 return mWidth; 933 } 934 935 /** 936 * Return the width to which this Layout is ellipsizing, or 937 * {@link #getWidth} if it is not doing anything special. 938 */ getEllipsizedWidth()939 public int getEllipsizedWidth() { 940 return mWidth; 941 } 942 943 /** 944 * Increase the width of this layout to the specified width. 945 * Be careful to use this only when you know it is appropriate— 946 * it does not cause the text to reflow to use the full new width. 947 */ increaseWidthTo(int wid)948 public final void increaseWidthTo(int wid) { 949 if (wid < mWidth) { 950 throw new RuntimeException("attempted to reduce Layout width"); 951 } 952 953 mWidth = wid; 954 } 955 956 /** 957 * Return the total height of this layout. 958 */ getHeight()959 public int getHeight() { 960 return getLineTop(getLineCount()); 961 } 962 963 /** 964 * Return the total height of this layout. 965 * 966 * @param cap if true and max lines is set, returns the height of the layout at the max lines. 967 * 968 * @hide 969 */ getHeight(boolean cap)970 public int getHeight(boolean cap) { 971 return getHeight(); 972 } 973 974 /** 975 * Return the base alignment of this layout. 976 */ getAlignment()977 public final Alignment getAlignment() { 978 return mAlignment; 979 } 980 981 /** 982 * Return what the text height is multiplied by to get the line height. 983 */ getSpacingMultiplier()984 public final float getSpacingMultiplier() { 985 return mSpacingMult; 986 } 987 988 /** 989 * Return the number of units of leading that are added to each line. 990 */ getSpacingAdd()991 public final float getSpacingAdd() { 992 return mSpacingAdd; 993 } 994 995 /** 996 * Return the heuristic used to determine paragraph text direction. 997 * @hide 998 */ getTextDirectionHeuristic()999 public final TextDirectionHeuristic getTextDirectionHeuristic() { 1000 return mTextDir; 1001 } 1002 1003 /** 1004 * Return the number of lines of text in this layout. 1005 */ getLineCount()1006 public abstract int getLineCount(); 1007 1008 /** 1009 * Return the baseline for the specified line (0…getLineCount() - 1) 1010 * If bounds is not null, return the top, left, right, bottom extents 1011 * of the specified line in it. 1012 * @param line which line to examine (0..getLineCount() - 1) 1013 * @param bounds Optional. If not null, it returns the extent of the line 1014 * @return the Y-coordinate of the baseline 1015 */ getLineBounds(int line, Rect bounds)1016 public int getLineBounds(int line, Rect bounds) { 1017 if (bounds != null) { 1018 bounds.left = 0; // ??? 1019 bounds.top = getLineTop(line); 1020 bounds.right = mWidth; // ??? 1021 bounds.bottom = getLineTop(line + 1); 1022 } 1023 return getLineBaseline(line); 1024 } 1025 1026 /** 1027 * Return the vertical position of the top of the specified line 1028 * (0…getLineCount()). 1029 * If the specified line is equal to the line count, returns the 1030 * bottom of the last line. 1031 */ getLineTop(int line)1032 public abstract int getLineTop(int line); 1033 1034 /** 1035 * Return the descent of the specified line(0…getLineCount() - 1). 1036 */ getLineDescent(int line)1037 public abstract int getLineDescent(int line); 1038 1039 /** 1040 * Return the text offset of the beginning of the specified line ( 1041 * 0…getLineCount()). If the specified line is equal to the line 1042 * count, returns the length of the text. 1043 */ getLineStart(int line)1044 public abstract int getLineStart(int line); 1045 1046 /** 1047 * Returns the primary directionality of the paragraph containing the 1048 * specified line, either 1 for left-to-right lines, or -1 for right-to-left 1049 * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}). 1050 */ getParagraphDirection(int line)1051 public abstract int getParagraphDirection(int line); 1052 1053 /** 1054 * Returns whether the specified line contains one or more 1055 * characters that need to be handled specially, like tabs. 1056 */ getLineContainsTab(int line)1057 public abstract boolean getLineContainsTab(int line); 1058 1059 /** 1060 * Returns the directional run information for the specified line. 1061 * The array alternates counts of characters in left-to-right 1062 * and right-to-left segments of the line. 1063 * 1064 * <p>NOTE: this is inadequate to support bidirectional text, and will change. 1065 */ getLineDirections(int line)1066 public abstract Directions getLineDirections(int line); 1067 1068 /** 1069 * Returns the (negative) number of extra pixels of ascent padding in the 1070 * top line of the Layout. 1071 */ getTopPadding()1072 public abstract int getTopPadding(); 1073 1074 /** 1075 * Returns the number of extra pixels of descent padding in the 1076 * bottom line of the Layout. 1077 */ getBottomPadding()1078 public abstract int getBottomPadding(); 1079 1080 /** 1081 * Returns the start hyphen edit for a line. 1082 * 1083 * @hide 1084 */ getStartHyphenEdit(int line)1085 public @Paint.StartHyphenEdit int getStartHyphenEdit(int line) { 1086 return Paint.START_HYPHEN_EDIT_NO_EDIT; 1087 } 1088 1089 /** 1090 * Returns the end hyphen edit for a line. 1091 * 1092 * @hide 1093 */ getEndHyphenEdit(int line)1094 public @Paint.EndHyphenEdit int getEndHyphenEdit(int line) { 1095 return Paint.END_HYPHEN_EDIT_NO_EDIT; 1096 } 1097 1098 /** 1099 * Returns the left indent for a line. 1100 * 1101 * @hide 1102 */ getIndentAdjust(int line, Alignment alignment)1103 public int getIndentAdjust(int line, Alignment alignment) { 1104 return 0; 1105 } 1106 1107 /** 1108 * Return true if the fallback line space is enabled in this Layout. 1109 * 1110 * @return true if the fallback line space is enabled. Otherwise returns false. 1111 */ isFallbackLineSpacingEnabled()1112 public boolean isFallbackLineSpacingEnabled() { 1113 return false; 1114 } 1115 1116 /** 1117 * Returns true if the character at offset and the preceding character 1118 * are at different run levels (and thus there's a split caret). 1119 * @param offset the offset 1120 * @return true if at a level boundary 1121 * @hide 1122 */ 1123 @UnsupportedAppUsage isLevelBoundary(int offset)1124 public boolean isLevelBoundary(int offset) { 1125 int line = getLineForOffset(offset); 1126 Directions dirs = getLineDirections(line); 1127 if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) { 1128 return false; 1129 } 1130 1131 int[] runs = dirs.mDirections; 1132 int lineStart = getLineStart(line); 1133 int lineEnd = getLineEnd(line); 1134 if (offset == lineStart || offset == lineEnd) { 1135 int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1; 1136 int runIndex = offset == lineStart ? 0 : runs.length - 2; 1137 return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel; 1138 } 1139 1140 offset -= lineStart; 1141 for (int i = 0; i < runs.length; i += 2) { 1142 if (offset == runs[i]) { 1143 return true; 1144 } 1145 } 1146 return false; 1147 } 1148 1149 /** 1150 * Returns true if the character at offset is right to left (RTL). 1151 * @param offset the offset 1152 * @return true if the character is RTL, false if it is LTR 1153 */ isRtlCharAt(int offset)1154 public boolean isRtlCharAt(int offset) { 1155 int line = getLineForOffset(offset); 1156 Directions dirs = getLineDirections(line); 1157 if (dirs == DIRS_ALL_LEFT_TO_RIGHT) { 1158 return false; 1159 } 1160 if (dirs == DIRS_ALL_RIGHT_TO_LEFT) { 1161 return true; 1162 } 1163 int[] runs = dirs.mDirections; 1164 int lineStart = getLineStart(line); 1165 for (int i = 0; i < runs.length; i += 2) { 1166 int start = lineStart + runs[i]; 1167 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 1168 if (offset >= start && offset < limit) { 1169 int level = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 1170 return ((level & 1) != 0); 1171 } 1172 } 1173 // Should happen only if the offset is "out of bounds" 1174 return false; 1175 } 1176 1177 /** 1178 * Returns the range of the run that the character at offset belongs to. 1179 * @param offset the offset 1180 * @return The range of the run 1181 * @hide 1182 */ getRunRange(int offset)1183 public long getRunRange(int offset) { 1184 int line = getLineForOffset(offset); 1185 Directions dirs = getLineDirections(line); 1186 if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) { 1187 return TextUtils.packRangeInLong(0, getLineEnd(line)); 1188 } 1189 int[] runs = dirs.mDirections; 1190 int lineStart = getLineStart(line); 1191 for (int i = 0; i < runs.length; i += 2) { 1192 int start = lineStart + runs[i]; 1193 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 1194 if (offset >= start && offset < limit) { 1195 return TextUtils.packRangeInLong(start, limit); 1196 } 1197 } 1198 // Should happen only if the offset is "out of bounds" 1199 return TextUtils.packRangeInLong(0, getLineEnd(line)); 1200 } 1201 1202 /** 1203 * Checks if the trailing BiDi level should be used for an offset 1204 * 1205 * This method is useful when the offset is at the BiDi level transition point and determine 1206 * which run need to be used. For example, let's think about following input: (L* denotes 1207 * Left-to-Right characters, R* denotes Right-to-Left characters.) 1208 * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6 1209 * Input (Display Order): L1 L2 L3 R3 R2 R1 L4 L5 L6 1210 * 1211 * Then, think about selecting the range (3, 6). The offset=3 and offset=6 are ambiguous here 1212 * since they are at the BiDi transition point. In Android, the offset is considered to be 1213 * associated with the trailing run if the BiDi level of the trailing run is higher than of the 1214 * previous run. In this case, the BiDi level of the input text is as follows: 1215 * 1216 * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6 1217 * BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ] 1218 * BiDi Level: 0 0 0 1 1 1 0 0 0 1219 * 1220 * Thus, offset = 3 is part of Run 1 and this method returns true for offset = 3, since the BiDi 1221 * level of Run 1 is higher than the level of Run 0. Similarly, the offset = 6 is a part of Run 1222 * 1 and this method returns false for the offset = 6 since the BiDi level of Run 1 is higher 1223 * than the level of Run 2. 1224 * 1225 * @returns true if offset is at the BiDi level transition point and trailing BiDi level is 1226 * higher than previous BiDi level. See above for the detail. 1227 * @hide 1228 */ 1229 @VisibleForTesting primaryIsTrailingPrevious(int offset)1230 public boolean primaryIsTrailingPrevious(int offset) { 1231 int line = getLineForOffset(offset); 1232 int lineStart = getLineStart(line); 1233 int lineEnd = getLineEnd(line); 1234 int[] runs = getLineDirections(line).mDirections; 1235 1236 int levelAt = -1; 1237 for (int i = 0; i < runs.length; i += 2) { 1238 int start = lineStart + runs[i]; 1239 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 1240 if (limit > lineEnd) { 1241 limit = lineEnd; 1242 } 1243 if (offset >= start && offset < limit) { 1244 if (offset > start) { 1245 // Previous character is at same level, so don't use trailing. 1246 return false; 1247 } 1248 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 1249 break; 1250 } 1251 } 1252 if (levelAt == -1) { 1253 // Offset was limit of line. 1254 levelAt = getParagraphDirection(line) == 1 ? 0 : 1; 1255 } 1256 1257 // At level boundary, check previous level. 1258 int levelBefore = -1; 1259 if (offset == lineStart) { 1260 levelBefore = getParagraphDirection(line) == 1 ? 0 : 1; 1261 } else { 1262 offset -= 1; 1263 for (int i = 0; i < runs.length; i += 2) { 1264 int start = lineStart + runs[i]; 1265 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 1266 if (limit > lineEnd) { 1267 limit = lineEnd; 1268 } 1269 if (offset >= start && offset < limit) { 1270 levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 1271 break; 1272 } 1273 } 1274 } 1275 1276 return levelBefore < levelAt; 1277 } 1278 1279 /** 1280 * Computes in linear time the results of calling 1281 * #primaryIsTrailingPrevious for all offsets on a line. 1282 * @param line The line giving the offsets we compute the information for 1283 * @return The array of results, indexed from 0, where 0 corresponds to the line start offset 1284 * @hide 1285 */ 1286 @VisibleForTesting primaryIsTrailingPreviousAllLineOffsets(int line)1287 public boolean[] primaryIsTrailingPreviousAllLineOffsets(int line) { 1288 int lineStart = getLineStart(line); 1289 int lineEnd = getLineEnd(line); 1290 int[] runs = getLineDirections(line).mDirections; 1291 1292 boolean[] trailing = new boolean[lineEnd - lineStart + 1]; 1293 1294 byte[] level = new byte[lineEnd - lineStart + 1]; 1295 for (int i = 0; i < runs.length; i += 2) { 1296 int start = lineStart + runs[i]; 1297 int limit = start + (runs[i + 1] & RUN_LENGTH_MASK); 1298 if (limit > lineEnd) { 1299 limit = lineEnd; 1300 } 1301 if (limit == start) { 1302 continue; 1303 } 1304 level[limit - lineStart - 1] = 1305 (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK); 1306 } 1307 1308 for (int i = 0; i < runs.length; i += 2) { 1309 int start = lineStart + runs[i]; 1310 byte currentLevel = (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK); 1311 trailing[start - lineStart] = currentLevel > (start == lineStart 1312 ? (getParagraphDirection(line) == 1 ? 0 : 1) 1313 : level[start - lineStart - 1]); 1314 } 1315 1316 return trailing; 1317 } 1318 1319 /** 1320 * Get the primary horizontal position for the specified text offset. 1321 * This is the location where a new character would be inserted in 1322 * the paragraph's primary direction. 1323 */ getPrimaryHorizontal(int offset)1324 public float getPrimaryHorizontal(int offset) { 1325 return getPrimaryHorizontal(offset, false /* not clamped */); 1326 } 1327 1328 /** 1329 * Get the primary horizontal position for the specified text offset, but 1330 * optionally clamp it so that it doesn't exceed the width of the layout. 1331 * @hide 1332 */ 1333 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getPrimaryHorizontal(int offset, boolean clamped)1334 public float getPrimaryHorizontal(int offset, boolean clamped) { 1335 boolean trailing = primaryIsTrailingPrevious(offset); 1336 return getHorizontal(offset, trailing, clamped); 1337 } 1338 1339 /** 1340 * Get the secondary horizontal position for the specified text offset. 1341 * This is the location where a new character would be inserted in 1342 * the direction other than the paragraph's primary direction. 1343 */ getSecondaryHorizontal(int offset)1344 public float getSecondaryHorizontal(int offset) { 1345 return getSecondaryHorizontal(offset, false /* not clamped */); 1346 } 1347 1348 /** 1349 * Get the secondary horizontal position for the specified text offset, but 1350 * optionally clamp it so that it doesn't exceed the width of the layout. 1351 * @hide 1352 */ 1353 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getSecondaryHorizontal(int offset, boolean clamped)1354 public float getSecondaryHorizontal(int offset, boolean clamped) { 1355 boolean trailing = primaryIsTrailingPrevious(offset); 1356 return getHorizontal(offset, !trailing, clamped); 1357 } 1358 getHorizontal(int offset, boolean primary)1359 private float getHorizontal(int offset, boolean primary) { 1360 return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset); 1361 } 1362 getHorizontal(int offset, boolean trailing, boolean clamped)1363 private float getHorizontal(int offset, boolean trailing, boolean clamped) { 1364 int line = getLineForOffset(offset); 1365 1366 return getHorizontal(offset, trailing, line, clamped); 1367 } 1368 getHorizontal(int offset, boolean trailing, int line, boolean clamped)1369 private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) { 1370 int start = getLineStart(line); 1371 int end = getLineEnd(line); 1372 int dir = getParagraphDirection(line); 1373 boolean hasTab = getLineContainsTab(line); 1374 Directions directions = getLineDirections(line); 1375 1376 TabStops tabStops = null; 1377 if (hasTab && mText instanceof Spanned) { 1378 // Just checking this line should be good enough, tabs should be 1379 // consistent across all lines in a paragraph. 1380 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 1381 if (tabs.length > 0) { 1382 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1383 } 1384 } 1385 1386 TextLine tl = TextLine.obtain(); 1387 tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops, 1388 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 1389 isFallbackLineSpacingEnabled()); 1390 float wid = tl.measure(offset - start, trailing, null); 1391 TextLine.recycle(tl); 1392 1393 if (clamped && wid > mWidth) { 1394 wid = mWidth; 1395 } 1396 int left = getParagraphLeft(line); 1397 int right = getParagraphRight(line); 1398 1399 return getLineStartPos(line, left, right) + wid; 1400 } 1401 1402 /** 1403 * Computes in linear time the results of calling #getHorizontal for all offsets on a line. 1404 * 1405 * @param line The line giving the offsets we compute information for 1406 * @param clamped Whether to clamp the results to the width of the layout 1407 * @param primary Whether the results should be the primary or the secondary horizontal 1408 * @return The array of results, indexed from 0, where 0 corresponds to the line start offset 1409 */ getLineHorizontals(int line, boolean clamped, boolean primary)1410 private float[] getLineHorizontals(int line, boolean clamped, boolean primary) { 1411 int start = getLineStart(line); 1412 int end = getLineEnd(line); 1413 int dir = getParagraphDirection(line); 1414 boolean hasTab = getLineContainsTab(line); 1415 Directions directions = getLineDirections(line); 1416 1417 TabStops tabStops = null; 1418 if (hasTab && mText instanceof Spanned) { 1419 // Just checking this line should be good enough, tabs should be 1420 // consistent across all lines in a paragraph. 1421 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 1422 if (tabs.length > 0) { 1423 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1424 } 1425 } 1426 1427 TextLine tl = TextLine.obtain(); 1428 tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops, 1429 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 1430 isFallbackLineSpacingEnabled()); 1431 boolean[] trailings = primaryIsTrailingPreviousAllLineOffsets(line); 1432 if (!primary) { 1433 for (int offset = 0; offset < trailings.length; ++offset) { 1434 trailings[offset] = !trailings[offset]; 1435 } 1436 } 1437 float[] wid = tl.measureAllOffsets(trailings, null); 1438 TextLine.recycle(tl); 1439 1440 if (clamped) { 1441 for (int offset = 0; offset < wid.length; ++offset) { 1442 if (wid[offset] > mWidth) { 1443 wid[offset] = mWidth; 1444 } 1445 } 1446 } 1447 int left = getParagraphLeft(line); 1448 int right = getParagraphRight(line); 1449 1450 int lineStartPos = getLineStartPos(line, left, right); 1451 float[] horizontal = new float[end - start + 1]; 1452 for (int offset = 0; offset < horizontal.length; ++offset) { 1453 horizontal[offset] = lineStartPos + wid[offset]; 1454 } 1455 return horizontal; 1456 } 1457 fillHorizontalBoundsForLine(int line, float[] horizontalBounds)1458 private void fillHorizontalBoundsForLine(int line, float[] horizontalBounds) { 1459 final int lineStart = getLineStart(line); 1460 final int lineEnd = getLineEnd(line); 1461 final int lineLength = lineEnd - lineStart; 1462 1463 final int dir = getParagraphDirection(line); 1464 final Directions directions = getLineDirections(line); 1465 1466 final boolean hasTab = getLineContainsTab(line); 1467 TabStops tabStops = null; 1468 if (hasTab && mText instanceof Spanned) { 1469 // Just checking this line should be good enough, tabs should be 1470 // consistent across all lines in a paragraph. 1471 TabStopSpan[] tabs = 1472 getParagraphSpans((Spanned) mText, lineStart, lineEnd, TabStopSpan.class); 1473 if (tabs.length > 0) { 1474 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1475 } 1476 } 1477 1478 final TextLine tl = TextLine.obtain(); 1479 tl.set(mPaint, mText, lineStart, lineEnd, dir, directions, hasTab, tabStops, 1480 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 1481 isFallbackLineSpacingEnabled()); 1482 if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) { 1483 horizontalBounds = new float[2 * lineLength]; 1484 } 1485 1486 tl.measureAllBounds(horizontalBounds, null); 1487 TextLine.recycle(tl); 1488 } 1489 1490 /** 1491 * Return the characters' bounds in the given range. The {@code bounds} array will be filled 1492 * starting from {@code boundsStart} (inclusive). The coordinates are in local text layout. 1493 * 1494 * @param start the start index to compute the character bounds, inclusive. 1495 * @param end the end index to compute the character bounds, exclusive. 1496 * @param bounds the array to fill in the character bounds. The array is divided into segments 1497 * of four where each index in that segment represents left, top, right and 1498 * bottom of the character. 1499 * @param boundsStart the inclusive start index in the array to start filling in the values 1500 * from. 1501 * 1502 * @throws IndexOutOfBoundsException if the range defined by {@code start} and {@code end} 1503 * exceeds the range of the text, or {@code bounds} doesn't have enough space to store the 1504 * result. 1505 * @throws IllegalArgumentException if {@code bounds} is null. 1506 */ fillCharacterBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull float[] bounds, @IntRange(from = 0) int boundsStart)1507 public void fillCharacterBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end, 1508 @NonNull float[] bounds, @IntRange(from = 0) int boundsStart) { 1509 if (start < 0 || end < start || end > mText.length()) { 1510 throw new IndexOutOfBoundsException("given range: " + start + ", " + end + " is " 1511 + "out of the text range: 0, " + mText.length()); 1512 } 1513 1514 if (bounds == null) { 1515 throw new IllegalArgumentException("bounds can't be null."); 1516 } 1517 1518 final int neededLength = 4 * (end - start); 1519 if (neededLength > bounds.length - boundsStart) { 1520 throw new IndexOutOfBoundsException("bounds doesn't have enough space to store the " 1521 + "result, needed: " + neededLength + " had: " 1522 + (bounds.length - boundsStart)); 1523 } 1524 1525 if (start == end) { 1526 return; 1527 } 1528 1529 final int startLine = getLineForOffset(start); 1530 final int endLine = getLineForOffset(end - 1); 1531 float[] horizontalBounds = null; 1532 for (int line = startLine; line <= endLine; ++line) { 1533 final int lineStart = getLineStart(line); 1534 final int lineEnd = getLineEnd(line); 1535 final int lineLength = lineEnd - lineStart; 1536 if (horizontalBounds == null || horizontalBounds.length < 2 * lineLength) { 1537 horizontalBounds = new float[2 * lineLength]; 1538 } 1539 fillHorizontalBoundsForLine(line, horizontalBounds); 1540 1541 final int lineLeft = getParagraphLeft(line); 1542 final int lineRight = getParagraphRight(line); 1543 final int lineStartPos = getLineStartPos(line, lineLeft, lineRight); 1544 1545 final int lineTop = getLineTop(line); 1546 final int lineBottom = getLineBottom(line); 1547 1548 final int startIndex = Math.max(start, lineStart); 1549 final int endIndex = Math.min(end, lineEnd); 1550 for (int index = startIndex; index < endIndex; ++index) { 1551 final int offset = index - lineStart; 1552 final float left = horizontalBounds[offset * 2] + lineStartPos; 1553 final float right = horizontalBounds[offset * 2 + 1] + lineStartPos; 1554 1555 final int boundsIndex = boundsStart + 4 * (index - start); 1556 bounds[boundsIndex] = left; 1557 bounds[boundsIndex + 1] = lineTop; 1558 bounds[boundsIndex + 2] = right; 1559 bounds[boundsIndex + 3] = lineBottom; 1560 } 1561 } 1562 } 1563 1564 /** 1565 * Get the leftmost position that should be exposed for horizontal 1566 * scrolling on the specified line. 1567 */ getLineLeft(int line)1568 public float getLineLeft(int line) { 1569 final int dir = getParagraphDirection(line); 1570 Alignment align = getParagraphAlignment(line); 1571 // Before Q, StaticLayout.Builder.setAlignment didn't check whether the input alignment 1572 // is null. And when it is null, the old behavior is the same as ALIGN_CENTER. 1573 // To keep consistency, we convert a null alignment to ALIGN_CENTER. 1574 if (align == null) { 1575 align = Alignment.ALIGN_CENTER; 1576 } 1577 1578 // First convert combinations of alignment and direction settings to 1579 // three basic cases: ALIGN_LEFT, ALIGN_RIGHT and ALIGN_CENTER. 1580 // For unexpected cases, it will fallback to ALIGN_LEFT. 1581 final Alignment resultAlign; 1582 switch(align) { 1583 case ALIGN_NORMAL: 1584 resultAlign = 1585 dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_RIGHT : Alignment.ALIGN_LEFT; 1586 break; 1587 case ALIGN_OPPOSITE: 1588 resultAlign = 1589 dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_LEFT : Alignment.ALIGN_RIGHT; 1590 break; 1591 case ALIGN_CENTER: 1592 resultAlign = Alignment.ALIGN_CENTER; 1593 break; 1594 case ALIGN_RIGHT: 1595 resultAlign = Alignment.ALIGN_RIGHT; 1596 break; 1597 default: /* align == Alignment.ALIGN_LEFT */ 1598 resultAlign = Alignment.ALIGN_LEFT; 1599 } 1600 1601 // Here we must use getLineMax() to do the computation, because it maybe overridden by 1602 // derived class. And also note that line max equals the width of the text in that line 1603 // plus the leading margin. 1604 switch (resultAlign) { 1605 case ALIGN_CENTER: 1606 final int left = getParagraphLeft(line); 1607 final float max = getLineMax(line); 1608 // This computation only works when mWidth equals leadingMargin plus 1609 // the width of text in this line. If this condition doesn't meet anymore, 1610 // please change here too. 1611 return (float) Math.floor(left + (mWidth - max) / 2); 1612 case ALIGN_RIGHT: 1613 return mWidth - getLineMax(line); 1614 default: /* resultAlign == Alignment.ALIGN_LEFT */ 1615 return 0; 1616 } 1617 } 1618 1619 /** 1620 * Get the rightmost position that should be exposed for horizontal 1621 * scrolling on the specified line. 1622 */ getLineRight(int line)1623 public float getLineRight(int line) { 1624 final int dir = getParagraphDirection(line); 1625 Alignment align = getParagraphAlignment(line); 1626 // Before Q, StaticLayout.Builder.setAlignment didn't check whether the input alignment 1627 // is null. And when it is null, the old behavior is the same as ALIGN_CENTER. 1628 // To keep consistency, we convert a null alignment to ALIGN_CENTER. 1629 if (align == null) { 1630 align = Alignment.ALIGN_CENTER; 1631 } 1632 1633 final Alignment resultAlign; 1634 switch(align) { 1635 case ALIGN_NORMAL: 1636 resultAlign = 1637 dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_RIGHT : Alignment.ALIGN_LEFT; 1638 break; 1639 case ALIGN_OPPOSITE: 1640 resultAlign = 1641 dir == DIR_RIGHT_TO_LEFT ? Alignment.ALIGN_LEFT : Alignment.ALIGN_RIGHT; 1642 break; 1643 case ALIGN_CENTER: 1644 resultAlign = Alignment.ALIGN_CENTER; 1645 break; 1646 case ALIGN_RIGHT: 1647 resultAlign = Alignment.ALIGN_RIGHT; 1648 break; 1649 default: /* align == Alignment.ALIGN_LEFT */ 1650 resultAlign = Alignment.ALIGN_LEFT; 1651 } 1652 1653 switch (resultAlign) { 1654 case ALIGN_CENTER: 1655 final int right = getParagraphRight(line); 1656 final float max = getLineMax(line); 1657 // This computation only works when mWidth equals leadingMargin plus width of the 1658 // text in this line. If this condition doesn't meet anymore, please change here. 1659 return (float) Math.ceil(right - (mWidth - max) / 2); 1660 case ALIGN_RIGHT: 1661 return mWidth; 1662 default: /* resultAlign == Alignment.ALIGN_LEFT */ 1663 return getLineMax(line); 1664 } 1665 } 1666 1667 /** 1668 * Gets the unsigned horizontal extent of the specified line, including 1669 * leading margin indent, but excluding trailing whitespace. 1670 */ getLineMax(int line)1671 public float getLineMax(int line) { 1672 float margin = getParagraphLeadingMargin(line); 1673 float signedExtent = getLineExtent(line, false); 1674 return margin + (signedExtent >= 0 ? signedExtent : -signedExtent); 1675 } 1676 1677 /** 1678 * Gets the unsigned horizontal extent of the specified line, including 1679 * leading margin indent and trailing whitespace. 1680 */ getLineWidth(int line)1681 public float getLineWidth(int line) { 1682 float margin = getParagraphLeadingMargin(line); 1683 float signedExtent = getLineExtent(line, true); 1684 return margin + (signedExtent >= 0 ? signedExtent : -signedExtent); 1685 } 1686 1687 /** 1688 * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the 1689 * tab stops instead of using the ones passed in. 1690 * @param line the index of the line 1691 * @param full whether to include trailing whitespace 1692 * @return the extent of the line 1693 */ getLineExtent(int line, boolean full)1694 private float getLineExtent(int line, boolean full) { 1695 final int start = getLineStart(line); 1696 final int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 1697 1698 final boolean hasTabs = getLineContainsTab(line); 1699 TabStops tabStops = null; 1700 if (hasTabs && mText instanceof Spanned) { 1701 // Just checking this line should be good enough, tabs should be 1702 // consistent across all lines in a paragraph. 1703 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 1704 if (tabs.length > 0) { 1705 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1706 } 1707 } 1708 final Directions directions = getLineDirections(line); 1709 // Returned directions can actually be null 1710 if (directions == null) { 1711 return 0f; 1712 } 1713 final int dir = getParagraphDirection(line); 1714 1715 final TextLine tl = TextLine.obtain(); 1716 final TextPaint paint = mWorkPaint; 1717 paint.set(mPaint); 1718 paint.setStartHyphenEdit(getStartHyphenEdit(line)); 1719 paint.setEndHyphenEdit(getEndHyphenEdit(line)); 1720 tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops, 1721 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 1722 isFallbackLineSpacingEnabled()); 1723 if (isJustificationRequired(line)) { 1724 tl.justify(getJustifyWidth(line)); 1725 } 1726 final float width = tl.metrics(null); 1727 TextLine.recycle(tl); 1728 return width; 1729 } 1730 1731 /** 1732 * Returns the signed horizontal extent of the specified line, excluding 1733 * leading margin. If full is false, excludes trailing whitespace. 1734 * @param line the index of the line 1735 * @param tabStops the tab stops, can be null if we know they're not used. 1736 * @param full whether to include trailing whitespace 1737 * @return the extent of the text on this line 1738 */ getLineExtent(int line, TabStops tabStops, boolean full)1739 private float getLineExtent(int line, TabStops tabStops, boolean full) { 1740 final int start = getLineStart(line); 1741 final int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 1742 final boolean hasTabs = getLineContainsTab(line); 1743 final Directions directions = getLineDirections(line); 1744 final int dir = getParagraphDirection(line); 1745 1746 final TextLine tl = TextLine.obtain(); 1747 final TextPaint paint = mWorkPaint; 1748 paint.set(mPaint); 1749 paint.setStartHyphenEdit(getStartHyphenEdit(line)); 1750 paint.setEndHyphenEdit(getEndHyphenEdit(line)); 1751 tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops, 1752 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 1753 isFallbackLineSpacingEnabled()); 1754 if (isJustificationRequired(line)) { 1755 tl.justify(getJustifyWidth(line)); 1756 } 1757 final float width = tl.metrics(null); 1758 TextLine.recycle(tl); 1759 return width; 1760 } 1761 1762 /** 1763 * Get the line number corresponding to the specified vertical position. 1764 * If you ask for a position above 0, you get 0; if you ask for a position 1765 * below the bottom of the text, you get the last line. 1766 */ 1767 // FIXME: It may be faster to do a linear search for layouts without many lines. getLineForVertical(int vertical)1768 public int getLineForVertical(int vertical) { 1769 int high = getLineCount(), low = -1, guess; 1770 1771 while (high - low > 1) { 1772 guess = (high + low) / 2; 1773 1774 if (getLineTop(guess) > vertical) 1775 high = guess; 1776 else 1777 low = guess; 1778 } 1779 1780 if (low < 0) 1781 return 0; 1782 else 1783 return low; 1784 } 1785 1786 /** 1787 * Get the line number on which the specified text offset appears. 1788 * If you ask for a position before 0, you get 0; if you ask for a position 1789 * beyond the end of the text, you get the last line. 1790 */ getLineForOffset(int offset)1791 public int getLineForOffset(int offset) { 1792 int high = getLineCount(), low = -1, guess; 1793 1794 while (high - low > 1) { 1795 guess = (high + low) / 2; 1796 1797 if (getLineStart(guess) > offset) 1798 high = guess; 1799 else 1800 low = guess; 1801 } 1802 1803 if (low < 0) { 1804 return 0; 1805 } else { 1806 return low; 1807 } 1808 } 1809 1810 /** 1811 * Get the character offset on the specified line whose position is 1812 * closest to the specified horizontal position. 1813 */ getOffsetForHorizontal(int line, float horiz)1814 public int getOffsetForHorizontal(int line, float horiz) { 1815 return getOffsetForHorizontal(line, horiz, true); 1816 } 1817 1818 /** 1819 * Get the character offset on the specified line whose position is 1820 * closest to the specified horizontal position. 1821 * 1822 * @param line the line used to find the closest offset 1823 * @param horiz the horizontal position used to find the closest offset 1824 * @param primary whether to use the primary position or secondary position to find the offset 1825 * 1826 * @hide 1827 */ getOffsetForHorizontal(int line, float horiz, boolean primary)1828 public int getOffsetForHorizontal(int line, float horiz, boolean primary) { 1829 // TODO: use Paint.getOffsetForAdvance to avoid binary search 1830 final int lineEndOffset = getLineEnd(line); 1831 final int lineStartOffset = getLineStart(line); 1832 1833 Directions dirs = getLineDirections(line); 1834 1835 TextLine tl = TextLine.obtain(); 1836 // XXX: we don't care about tabs as we just use TextLine#getOffsetToLeftRightOf here. 1837 tl.set(mPaint, mText, lineStartOffset, lineEndOffset, getParagraphDirection(line), dirs, 1838 false, null, 1839 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 1840 isFallbackLineSpacingEnabled()); 1841 final HorizontalMeasurementProvider horizontal = 1842 new HorizontalMeasurementProvider(line, primary); 1843 1844 final int max; 1845 if (line == getLineCount() - 1) { 1846 max = lineEndOffset; 1847 } else { 1848 max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset, 1849 !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset; 1850 } 1851 int best = lineStartOffset; 1852 float bestdist = Math.abs(horizontal.get(lineStartOffset) - horiz); 1853 1854 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1855 int here = lineStartOffset + dirs.mDirections[i]; 1856 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1857 boolean isRtl = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0; 1858 int swap = isRtl ? -1 : 1; 1859 1860 if (there > max) 1861 there = max; 1862 int high = there - 1 + 1, low = here + 1 - 1, guess; 1863 1864 while (high - low > 1) { 1865 guess = (high + low) / 2; 1866 int adguess = getOffsetAtStartOf(guess); 1867 1868 if (horizontal.get(adguess) * swap >= horiz * swap) { 1869 high = guess; 1870 } else { 1871 low = guess; 1872 } 1873 } 1874 1875 if (low < here + 1) 1876 low = here + 1; 1877 1878 if (low < there) { 1879 int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset; 1880 low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset; 1881 if (low >= here && low < there) { 1882 float dist = Math.abs(horizontal.get(low) - horiz); 1883 if (aft < there) { 1884 float other = Math.abs(horizontal.get(aft) - horiz); 1885 1886 if (other < dist) { 1887 dist = other; 1888 low = aft; 1889 } 1890 } 1891 1892 if (dist < bestdist) { 1893 bestdist = dist; 1894 best = low; 1895 } 1896 } 1897 } 1898 1899 float dist = Math.abs(horizontal.get(here) - horiz); 1900 1901 if (dist < bestdist) { 1902 bestdist = dist; 1903 best = here; 1904 } 1905 } 1906 1907 float dist = Math.abs(horizontal.get(max) - horiz); 1908 1909 if (dist <= bestdist) { 1910 best = max; 1911 } 1912 1913 TextLine.recycle(tl); 1914 return best; 1915 } 1916 1917 /** 1918 * Responds to #getHorizontal queries, by selecting the better strategy between: 1919 * - calling #getHorizontal explicitly for each query 1920 * - precomputing all #getHorizontal measurements, and responding to any query in constant time 1921 * The first strategy is used for LTR-only text, while the second is used for all other cases. 1922 * The class is currently only used in #getOffsetForHorizontal, so reuse with care in other 1923 * contexts. 1924 */ 1925 private class HorizontalMeasurementProvider { 1926 private final int mLine; 1927 private final boolean mPrimary; 1928 1929 private float[] mHorizontals; 1930 private int mLineStartOffset; 1931 HorizontalMeasurementProvider(final int line, final boolean primary)1932 HorizontalMeasurementProvider(final int line, final boolean primary) { 1933 mLine = line; 1934 mPrimary = primary; 1935 init(); 1936 } 1937 init()1938 private void init() { 1939 final Directions dirs = getLineDirections(mLine); 1940 if (dirs == DIRS_ALL_LEFT_TO_RIGHT) { 1941 return; 1942 } 1943 1944 mHorizontals = getLineHorizontals(mLine, false, mPrimary); 1945 mLineStartOffset = getLineStart(mLine); 1946 } 1947 get(final int offset)1948 float get(final int offset) { 1949 final int index = offset - mLineStartOffset; 1950 if (mHorizontals == null || index < 0 || index >= mHorizontals.length) { 1951 return getHorizontal(offset, mPrimary); 1952 } else { 1953 return mHorizontals[index]; 1954 } 1955 } 1956 } 1957 1958 /** 1959 * Finds the range of text which is inside the specified rectangle area. The start of the range 1960 * is the start of the first text segment inside the area, and the end of the range is the end 1961 * of the last text segment inside the area. 1962 * 1963 * <p>A text segment is considered to be inside the area according to the provided {@link 1964 * TextInclusionStrategy}. If a text segment spans multiple lines or multiple directional runs 1965 * (e.g. a hyphenated word), the text segment is divided into pieces at the line and run breaks, 1966 * then the text segment is considered to be inside the area if any of its pieces are inside the 1967 * area. 1968 * 1969 * <p>The returned range may also include text segments which are not inside the specified area, 1970 * if those text segments are in between text segments which are inside the area. For example, 1971 * the returned range may be "segment1 segment2 segment3" if "segment1" and "segment3" are 1972 * inside the area and "segment2" is not. 1973 * 1974 * @param area area for which the text range will be found 1975 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a 1976 * text segment 1977 * @param inclusionStrategy strategy for determining whether a text segment is inside the 1978 * specified area 1979 * @return int array of size 2 containing the start (inclusive) and end (exclusive) character 1980 * offsets of the text range, or null if there are no text segments inside the area 1981 */ 1982 @Nullable getRangeForRect(@onNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy)1983 public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder, 1984 @NonNull TextInclusionStrategy inclusionStrategy) { 1985 // Find the first line whose bottom (without line spacing) is below the top of the area. 1986 int startLine = getLineForVertical((int) area.top); 1987 if (area.top > getLineBottom(startLine, /* includeLineSpacing= */ false)) { 1988 startLine++; 1989 if (startLine >= getLineCount()) { 1990 // The entire area is below the last line, so it does not contain any text. 1991 return null; 1992 } 1993 } 1994 1995 // Find the last line whose top is above the bottom of the area. 1996 int endLine = getLineForVertical((int) area.bottom); 1997 if (endLine == 0 && area.bottom < getLineTop(0)) { 1998 // The entire area is above the first line, so it does not contain any text. 1999 return null; 2000 } 2001 if (endLine < startLine) { 2002 // The entire area is between two lines, so it does not contain any text. 2003 return null; 2004 } 2005 2006 int start = getStartOrEndOffsetForAreaWithinLine( 2007 startLine, area, segmentFinder, inclusionStrategy, /* getStart= */ true); 2008 // If the area does not contain any text on this line, keep trying subsequent lines until 2009 // the end line is reached. 2010 while (start == -1 && startLine < endLine) { 2011 startLine++; 2012 start = getStartOrEndOffsetForAreaWithinLine( 2013 startLine, area, segmentFinder, inclusionStrategy, /* getStart= */ true); 2014 } 2015 if (start == -1) { 2016 // All lines were checked, the area does not contain any text. 2017 return null; 2018 } 2019 2020 int end = getStartOrEndOffsetForAreaWithinLine( 2021 endLine, area, segmentFinder, inclusionStrategy, /* getStart= */ false); 2022 // If the area does not contain any text on this line, keep trying previous lines until 2023 // the start line is reached. 2024 while (end == -1 && startLine < endLine) { 2025 endLine--; 2026 end = getStartOrEndOffsetForAreaWithinLine( 2027 endLine, area, segmentFinder, inclusionStrategy, /* getStart= */ false); 2028 } 2029 if (end == -1) { 2030 // All lines were checked, the area does not contain any text. 2031 return null; 2032 } 2033 2034 // If a text segment spans multiple lines or multiple directional runs (e.g. a hyphenated 2035 // word), then getStartOrEndOffsetForAreaWithinLine() can return an offset in the middle of 2036 // a text segment. Adjust the range to include the rest of any partial text segments. If 2037 // start is already the start boundary of a text segment, then this is a no-op. 2038 start = segmentFinder.previousStartBoundary(start + 1); 2039 end = segmentFinder.nextEndBoundary(end - 1); 2040 2041 return new int[] {start, end}; 2042 } 2043 2044 /** 2045 * Finds the start character offset of the first text segment within a line inside the specified 2046 * rectangle area, or the end character offset of the last text segment inside the area. 2047 * 2048 * @param line index of the line to search 2049 * @param area area inside which text segments will be found 2050 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a 2051 * text segment 2052 * @param inclusionStrategy strategy for determining whether a text segment is inside the 2053 * specified area 2054 * @param getStart true to find the start of the first text segment inside the area, false to 2055 * find the end of the last text segment 2056 * @return the start character offset of the first text segment inside the area, or the end 2057 * character offset of the last text segment inside the area. 2058 */ getStartOrEndOffsetForAreaWithinLine( @ntRangefrom = 0) int line, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull TextInclusionStrategy inclusionStrategy, boolean getStart)2059 private int getStartOrEndOffsetForAreaWithinLine( 2060 @IntRange(from = 0) int line, 2061 @NonNull RectF area, 2062 @NonNull SegmentFinder segmentFinder, 2063 @NonNull TextInclusionStrategy inclusionStrategy, 2064 boolean getStart) { 2065 int lineTop = getLineTop(line); 2066 int lineBottom = getLineBottom(line, /* includeLineSpacing= */ false); 2067 2068 int lineStartOffset = getLineStart(line); 2069 int lineEndOffset = getLineEnd(line); 2070 if (lineStartOffset == lineEndOffset) { 2071 return -1; 2072 } 2073 2074 float[] horizontalBounds = new float[2 * (lineEndOffset - lineStartOffset)]; 2075 fillHorizontalBoundsForLine(line, horizontalBounds); 2076 2077 int lineStartPos = getLineStartPos(line, getParagraphLeft(line), getParagraphRight(line)); 2078 2079 // Loop through the runs forwards or backwards depending on getStart value. 2080 Layout.Directions directions = getLineDirections(line); 2081 int runIndex = getStart ? 0 : directions.getRunCount() - 1; 2082 while ((getStart && runIndex < directions.getRunCount()) || (!getStart && runIndex >= 0)) { 2083 // runStartOffset and runEndOffset are offset indices within the line. 2084 int runStartOffset = directions.getRunStart(runIndex); 2085 int runEndOffset = Math.min( 2086 runStartOffset + directions.getRunLength(runIndex), 2087 lineEndOffset - lineStartOffset); 2088 boolean isRtl = directions.isRunRtl(runIndex); 2089 float runLeft = lineStartPos 2090 + (isRtl 2091 ? horizontalBounds[2 * (runEndOffset - 1)] 2092 : horizontalBounds[2 * runStartOffset]); 2093 float runRight = lineStartPos 2094 + (isRtl 2095 ? horizontalBounds[2 * runStartOffset + 1] 2096 : horizontalBounds[2 * (runEndOffset - 1) + 1]); 2097 2098 int result = 2099 getStart 2100 ? getStartOffsetForAreaWithinRun( 2101 area, lineTop, lineBottom, 2102 lineStartOffset, lineStartPos, horizontalBounds, 2103 runStartOffset, runEndOffset, runLeft, runRight, isRtl, 2104 segmentFinder, inclusionStrategy) 2105 : getEndOffsetForAreaWithinRun( 2106 area, lineTop, lineBottom, 2107 lineStartOffset, lineStartPos, horizontalBounds, 2108 runStartOffset, runEndOffset, runLeft, runRight, isRtl, 2109 segmentFinder, inclusionStrategy); 2110 if (result >= 0) { 2111 return result; 2112 } 2113 2114 runIndex += getStart ? 1 : -1; 2115 } 2116 return -1; 2117 } 2118 2119 /** 2120 * Finds the start character offset of the first text segment within a directional run inside 2121 * the specified rectangle area. 2122 * 2123 * @param area area inside which text segments will be found 2124 * @param lineTop top of the line containing this run 2125 * @param lineBottom bottom (not including line spacing) of the line containing this run 2126 * @param lineStartOffset start character offset of the line containing this run 2127 * @param lineStartPos start position of the line containing this run 2128 * @param horizontalBounds array containing the signed horizontal bounds of the characters in 2129 * the line. The left and right bounds of the character at offset i are stored at index (2 * 2130 * i) and index (2 * i + 1). Bounds are relative to {@code lineStartPos}. 2131 * @param runStartOffset start offset of the run relative to {@code lineStartOffset} 2132 * @param runEndOffset end offset of the run relative to {@code lineStartOffset} 2133 * @param runLeft left bound of the run 2134 * @param runRight right bound of the run 2135 * @param isRtl whether the run is right-to-left 2136 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a 2137 * text segment 2138 * @param inclusionStrategy strategy for determining whether a text segment is inside the 2139 * specified area 2140 * @return the start character offset of the first text segment inside the area 2141 */ 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)2142 private static int getStartOffsetForAreaWithinRun( 2143 @NonNull RectF area, 2144 int lineTop, int lineBottom, 2145 @IntRange(from = 0) int lineStartOffset, 2146 @IntRange(from = 0) int lineStartPos, 2147 @NonNull float[] horizontalBounds, 2148 @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset, 2149 float runLeft, float runRight, 2150 boolean isRtl, 2151 @NonNull SegmentFinder segmentFinder, 2152 @NonNull TextInclusionStrategy inclusionStrategy) { 2153 if (runRight < area.left || runLeft > area.right) { 2154 // The run does not overlap the area. 2155 return -1; 2156 } 2157 2158 // Find the first character in the run whose bounds overlap with the area. 2159 // firstCharOffset is an offset index within the line. 2160 int firstCharOffset; 2161 if ((!isRtl && area.left <= runLeft) || (isRtl && area.right >= runRight)) { 2162 firstCharOffset = runStartOffset; 2163 } else { 2164 int low = runStartOffset; 2165 int high = runEndOffset; 2166 int guess; 2167 while (high - low > 1) { 2168 guess = (high + low) / 2; 2169 // Left edge of the character at guess 2170 float pos = lineStartPos + horizontalBounds[2 * guess]; 2171 if ((!isRtl && pos > area.left) || (isRtl && pos < area.right)) { 2172 high = guess; 2173 } else { 2174 low = guess; 2175 } 2176 } 2177 // The area edge is between the left edge of the character at low and the left edge of 2178 // the character at high. For LTR text, this is within the character at low. For RTL 2179 // text, this is within the character at high. 2180 firstCharOffset = isRtl ? high : low; 2181 } 2182 2183 // Find the first text segment containing this character (or, if no text segment contains 2184 // this character, the first text segment after this character). All previous text segments 2185 // in this run are to the left (for LTR) of the area. 2186 int segmentEndOffset = 2187 segmentFinder.nextEndBoundary(lineStartOffset + firstCharOffset); 2188 if (segmentEndOffset == SegmentFinder.DONE) { 2189 // There are no text segments containing or after firstCharOffset, so no text segments 2190 // in this run overlap the area. 2191 return -1; 2192 } 2193 int segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset); 2194 if (segmentStartOffset >= lineStartOffset + runEndOffset) { 2195 // The text segment is after the end of this run, so no text segments in this run 2196 // overlap the area. 2197 return -1; 2198 } 2199 // If the segment extends outside of this run, only consider the piece of the segment within 2200 // this run. 2201 segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset); 2202 segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset); 2203 2204 RectF segmentBounds = new RectF(0, lineTop, 0, lineBottom); 2205 while (true) { 2206 // Start (left for LTR, right for RTL) edge of the character at segmentStartOffset. 2207 float segmentStart = lineStartPos + horizontalBounds[ 2208 2 * (segmentStartOffset - lineStartOffset) + (isRtl ? 1 : 0)]; 2209 if ((!isRtl && segmentStart > area.right) || (isRtl && segmentStart < area.left)) { 2210 // The entire area is to the left (for LTR) of the text segment. So the area does 2211 // not contain any text segments within this run. 2212 return -1; 2213 } 2214 // End (right for LTR, left for RTL) edge of the character at (segmentStartOffset - 1). 2215 float segmentEnd = lineStartPos + horizontalBounds[ 2216 2 * (segmentEndOffset - lineStartOffset - 1) + (isRtl ? 0 : 1)]; 2217 segmentBounds.left = isRtl ? segmentEnd : segmentStart; 2218 segmentBounds.right = isRtl ? segmentStart : segmentEnd; 2219 if (inclusionStrategy.isSegmentInside(segmentBounds, area)) { 2220 return segmentStartOffset; 2221 } 2222 // Try the next text segment. 2223 segmentStartOffset = segmentFinder.nextStartBoundary(segmentStartOffset); 2224 if (segmentStartOffset == SegmentFinder.DONE 2225 || segmentStartOffset >= lineStartOffset + runEndOffset) { 2226 // No more text segments within this run. 2227 return -1; 2228 } 2229 segmentEndOffset = segmentFinder.nextEndBoundary(segmentStartOffset); 2230 // If the segment extends past the end of this run, only consider the piece of the 2231 // segment within this run. 2232 segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset); 2233 } 2234 } 2235 2236 /** 2237 * Finds the end character offset of the last text segment within a directional run inside the 2238 * specified rectangle area. 2239 * 2240 * @param area area inside which text segments will be found 2241 * @param lineTop top of the line containing this run 2242 * @param lineBottom bottom (not including line spacing) of the line containing this run 2243 * @param lineStartOffset start character offset of the line containing this run 2244 * @param lineStartPos start position of the line containing this run 2245 * @param horizontalBounds array containing the signed horizontal bounds of the characters in 2246 * the line. The left and right bounds of the character at offset i are stored at index (2 * 2247 * i) and index (2 * i + 1). Bounds are relative to {@code lineStartPos}. 2248 * @param runStartOffset start offset of the run relative to {@code lineStartOffset} 2249 * @param runEndOffset end offset of the run relative to {@code lineStartOffset} 2250 * @param runLeft left bound of the run 2251 * @param runRight right bound of the run 2252 * @param isRtl whether the run is right-to-left 2253 * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a 2254 * text segment 2255 * @param inclusionStrategy strategy for determining whether a text segment is inside the 2256 * specified area 2257 * @return the end character offset of the last text segment inside the area 2258 */ 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)2259 private static int getEndOffsetForAreaWithinRun( 2260 @NonNull RectF area, 2261 int lineTop, int lineBottom, 2262 @IntRange(from = 0) int lineStartOffset, 2263 @IntRange(from = 0) int lineStartPos, 2264 @NonNull float[] horizontalBounds, 2265 @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset, 2266 float runLeft, float runRight, 2267 boolean isRtl, 2268 @NonNull SegmentFinder segmentFinder, 2269 @NonNull TextInclusionStrategy inclusionStrategy) { 2270 if (runRight < area.left || runLeft > area.right) { 2271 // The run does not overlap the area. 2272 return -1; 2273 } 2274 2275 // Find the last character in the run whose bounds overlap with the area. 2276 // firstCharOffset is an offset index within the line. 2277 int lastCharOffset; 2278 if ((!isRtl && area.right >= runRight) || (isRtl && area.left <= runLeft)) { 2279 lastCharOffset = runEndOffset - 1; 2280 } else { 2281 int low = runStartOffset; 2282 int high = runEndOffset; 2283 int guess; 2284 while (high - low > 1) { 2285 guess = (high + low) / 2; 2286 // Left edge of the character at guess 2287 float pos = lineStartPos + horizontalBounds[2 * guess]; 2288 if ((!isRtl && pos > area.right) || (isRtl && pos < area.left)) { 2289 high = guess; 2290 } else { 2291 low = guess; 2292 } 2293 } 2294 // The area edge is between the left edge of the character at low and the left edge of 2295 // the character at high. For LTR text, this is within the character at low. For RTL 2296 // text, this is within the character at high. 2297 lastCharOffset = isRtl ? high : low; 2298 } 2299 2300 // Find the last text segment containing this character (or, if no text segment contains 2301 // this character, the first text segment before this character). All following text 2302 // segments in this run are to the right (for LTR) of the area. 2303 // + 1 to allow segmentStartOffset = lineStartOffset + lastCharOffset 2304 int segmentStartOffset = 2305 segmentFinder.previousStartBoundary(lineStartOffset + lastCharOffset + 1); 2306 if (segmentStartOffset == SegmentFinder.DONE) { 2307 // There are no text segments containing or before lastCharOffset, so no text segments 2308 // in this run overlap the area. 2309 return -1; 2310 } 2311 int segmentEndOffset = segmentFinder.nextEndBoundary(segmentStartOffset); 2312 if (segmentEndOffset <= lineStartOffset + runStartOffset) { 2313 // The text segment is before the start of this run, so no text segments in this run 2314 // overlap the area. 2315 return -1; 2316 } 2317 // If the segment extends outside of this run, only consider the piece of the segment within 2318 // this run. 2319 segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset); 2320 segmentEndOffset = Math.min(segmentEndOffset, lineStartOffset + runEndOffset); 2321 2322 RectF segmentBounds = new RectF(0, lineTop, 0, lineBottom); 2323 while (true) { 2324 // End (right for LTR, left for RTL) edge of the character at (segmentStartOffset - 1). 2325 float segmentEnd = lineStartPos + horizontalBounds[ 2326 2 * (segmentEndOffset - lineStartOffset - 1) + (isRtl ? 0 : 1)]; 2327 if ((!isRtl && segmentEnd < area.left) || (isRtl && segmentEnd > area.right)) { 2328 // The entire area is to the right (for LTR) of the text segment. So the 2329 // area does not contain any text segments within this run. 2330 return -1; 2331 } 2332 // Start (left for LTR, right for RTL) edge of the character at segmentStartOffset. 2333 float segmentStart = lineStartPos + horizontalBounds[ 2334 2 * (segmentStartOffset - lineStartOffset) + (isRtl ? 1 : 0)]; 2335 segmentBounds.left = isRtl ? segmentEnd : segmentStart; 2336 segmentBounds.right = isRtl ? segmentStart : segmentEnd; 2337 if (inclusionStrategy.isSegmentInside(segmentBounds, area)) { 2338 return segmentEndOffset; 2339 } 2340 // Try the previous text segment. 2341 segmentEndOffset = segmentFinder.previousEndBoundary(segmentEndOffset); 2342 if (segmentEndOffset == SegmentFinder.DONE 2343 || segmentEndOffset <= lineStartOffset + runStartOffset) { 2344 // No more text segments within this run. 2345 return -1; 2346 } 2347 segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset); 2348 // If the segment extends past the start of this run, only consider the piece of the 2349 // segment within this run. 2350 segmentStartOffset = Math.max(segmentStartOffset, lineStartOffset + runStartOffset); 2351 } 2352 } 2353 2354 /** 2355 * Return the text offset after the last character on the specified line. 2356 */ getLineEnd(int line)2357 public final int getLineEnd(int line) { 2358 return getLineStart(line + 1); 2359 } 2360 2361 /** 2362 * Return the text offset after the last visible character (so whitespace 2363 * is not counted) on the specified line. 2364 */ getLineVisibleEnd(int line)2365 public int getLineVisibleEnd(int line) { 2366 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 2367 } 2368 getLineVisibleEnd(int line, int start, int end)2369 private int getLineVisibleEnd(int line, int start, int end) { 2370 CharSequence text = mText; 2371 char ch; 2372 if (line == getLineCount() - 1) { 2373 return end; 2374 } 2375 2376 for (; end > start; end--) { 2377 ch = text.charAt(end - 1); 2378 2379 if (ch == '\n') { 2380 return end - 1; 2381 } 2382 2383 if (!TextLine.isLineEndSpace(ch)) { 2384 break; 2385 } 2386 2387 } 2388 2389 return end; 2390 } 2391 2392 /** 2393 * Return the vertical position of the bottom of the specified line. 2394 */ getLineBottom(int line)2395 public final int getLineBottom(int line) { 2396 return getLineBottom(line, /* includeLineSpacing= */ true); 2397 } 2398 2399 /** 2400 * Return the vertical position of the bottom of the specified line. 2401 * 2402 * @param line index of the line 2403 * @param includeLineSpacing whether to include the line spacing 2404 */ getLineBottom(int line, boolean includeLineSpacing)2405 public int getLineBottom(int line, boolean includeLineSpacing) { 2406 if (includeLineSpacing) { 2407 return getLineTop(line + 1); 2408 } else { 2409 return getLineTop(line + 1) - getLineExtra(line); 2410 } 2411 } 2412 2413 /** 2414 * Return the vertical position of the baseline of the specified line. 2415 */ getLineBaseline(int line)2416 public final int getLineBaseline(int line) { 2417 // getLineTop(line+1) == getLineBottom(line) 2418 return getLineTop(line+1) - getLineDescent(line); 2419 } 2420 2421 /** 2422 * Get the ascent of the text on the specified line. 2423 * The return value is negative to match the Paint.ascent() convention. 2424 */ getLineAscent(int line)2425 public final int getLineAscent(int line) { 2426 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) 2427 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); 2428 } 2429 2430 /** 2431 * Return the extra space added as a result of line spacing attributes 2432 * {@link #getSpacingAdd()} and {@link #getSpacingMultiplier()}. Default value is {@code zero}. 2433 * 2434 * @param line the index of the line, the value should be equal or greater than {@code zero} 2435 * @hide 2436 */ getLineExtra(@ntRangefrom = 0) int line)2437 public int getLineExtra(@IntRange(from = 0) int line) { 2438 return 0; 2439 } 2440 getOffsetToLeftOf(int offset)2441 public int getOffsetToLeftOf(int offset) { 2442 return getOffsetToLeftRightOf(offset, true); 2443 } 2444 getOffsetToRightOf(int offset)2445 public int getOffsetToRightOf(int offset) { 2446 return getOffsetToLeftRightOf(offset, false); 2447 } 2448 getOffsetToLeftRightOf(int caret, boolean toLeft)2449 private int getOffsetToLeftRightOf(int caret, boolean toLeft) { 2450 int line = getLineForOffset(caret); 2451 int lineStart = getLineStart(line); 2452 int lineEnd = getLineEnd(line); 2453 int lineDir = getParagraphDirection(line); 2454 2455 boolean lineChanged = false; 2456 boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); 2457 // if walking off line, look at the line we're headed to 2458 if (advance) { 2459 if (caret == lineEnd) { 2460 if (line < getLineCount() - 1) { 2461 lineChanged = true; 2462 ++line; 2463 } else { 2464 return caret; // at very end, don't move 2465 } 2466 } 2467 } else { 2468 if (caret == lineStart) { 2469 if (line > 0) { 2470 lineChanged = true; 2471 --line; 2472 } else { 2473 return caret; // at very start, don't move 2474 } 2475 } 2476 } 2477 2478 if (lineChanged) { 2479 lineStart = getLineStart(line); 2480 lineEnd = getLineEnd(line); 2481 int newDir = getParagraphDirection(line); 2482 if (newDir != lineDir) { 2483 // unusual case. we want to walk onto the line, but it runs 2484 // in a different direction than this one, so we fake movement 2485 // in the opposite direction. 2486 toLeft = !toLeft; 2487 lineDir = newDir; 2488 } 2489 } 2490 2491 Directions directions = getLineDirections(line); 2492 2493 TextLine tl = TextLine.obtain(); 2494 // XXX: we don't care about tabs 2495 tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null, 2496 getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), 2497 isFallbackLineSpacingEnabled()); 2498 caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); 2499 TextLine.recycle(tl); 2500 return caret; 2501 } 2502 getOffsetAtStartOf(int offset)2503 private int getOffsetAtStartOf(int offset) { 2504 // XXX this probably should skip local reorderings and 2505 // zero-width characters, look at callers 2506 if (offset == 0) 2507 return 0; 2508 2509 CharSequence text = mText; 2510 char c = text.charAt(offset); 2511 2512 if (c >= '\uDC00' && c <= '\uDFFF') { 2513 char c1 = text.charAt(offset - 1); 2514 2515 if (c1 >= '\uD800' && c1 <= '\uDBFF') 2516 offset -= 1; 2517 } 2518 2519 if (mSpannedText) { 2520 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 2521 ReplacementSpan.class); 2522 2523 for (int i = 0; i < spans.length; i++) { 2524 int start = ((Spanned) text).getSpanStart(spans[i]); 2525 int end = ((Spanned) text).getSpanEnd(spans[i]); 2526 2527 if (start < offset && end > offset) 2528 offset = start; 2529 } 2530 } 2531 2532 return offset; 2533 } 2534 2535 /** 2536 * Determine whether we should clamp cursor position. Currently it's 2537 * only robust for left-aligned displays. 2538 * @hide 2539 */ 2540 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) shouldClampCursor(int line)2541 public boolean shouldClampCursor(int line) { 2542 // Only clamp cursor position in left-aligned displays. 2543 switch (getParagraphAlignment(line)) { 2544 case ALIGN_LEFT: 2545 return true; 2546 case ALIGN_NORMAL: 2547 return getParagraphDirection(line) > 0; 2548 default: 2549 return false; 2550 } 2551 2552 } 2553 2554 /** 2555 * Fills in the specified Path with a representation of a cursor 2556 * at the specified offset. This will often be a vertical line 2557 * but can be multiple discontinuous lines in text with multiple 2558 * directionalities. 2559 */ getCursorPath(final int point, final Path dest, final CharSequence editingBuffer)2560 public void getCursorPath(final int point, final Path dest, final CharSequence editingBuffer) { 2561 dest.reset(); 2562 2563 int line = getLineForOffset(point); 2564 int top = getLineTop(line); 2565 int bottom = getLineBottom(line, /* includeLineSpacing= */ false); 2566 2567 boolean clamped = shouldClampCursor(line); 2568 float h1 = getPrimaryHorizontal(point, clamped) - 0.5f; 2569 2570 int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | 2571 TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); 2572 int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON); 2573 int dist = 0; 2574 2575 if (caps != 0 || fn != 0) { 2576 dist = (bottom - top) >> 2; 2577 2578 if (fn != 0) 2579 top += dist; 2580 if (caps != 0) 2581 bottom -= dist; 2582 } 2583 2584 if (h1 < 0.5f) 2585 h1 = 0.5f; 2586 2587 dest.moveTo(h1, top); 2588 dest.lineTo(h1, bottom); 2589 2590 if (caps == 2) { 2591 dest.moveTo(h1, bottom); 2592 dest.lineTo(h1 - dist, bottom + dist); 2593 dest.lineTo(h1, bottom); 2594 dest.lineTo(h1 + dist, bottom + dist); 2595 } else if (caps == 1) { 2596 dest.moveTo(h1, bottom); 2597 dest.lineTo(h1 - dist, bottom + dist); 2598 2599 dest.moveTo(h1 - dist, bottom + dist - 0.5f); 2600 dest.lineTo(h1 + dist, bottom + dist - 0.5f); 2601 2602 dest.moveTo(h1 + dist, bottom + dist); 2603 dest.lineTo(h1, bottom); 2604 } 2605 2606 if (fn == 2) { 2607 dest.moveTo(h1, top); 2608 dest.lineTo(h1 - dist, top - dist); 2609 dest.lineTo(h1, top); 2610 dest.lineTo(h1 + dist, top - dist); 2611 } else if (fn == 1) { 2612 dest.moveTo(h1, top); 2613 dest.lineTo(h1 - dist, top - dist); 2614 2615 dest.moveTo(h1 - dist, top - dist + 0.5f); 2616 dest.lineTo(h1 + dist, top - dist + 0.5f); 2617 2618 dest.moveTo(h1 + dist, top - dist); 2619 dest.lineTo(h1, top); 2620 } 2621 } 2622 addSelection(int line, int start, int end, int top, int bottom, SelectionRectangleConsumer consumer)2623 private void addSelection(int line, int start, int end, 2624 int top, int bottom, SelectionRectangleConsumer consumer) { 2625 int linestart = getLineStart(line); 2626 int lineend = getLineEnd(line); 2627 Directions dirs = getLineDirections(line); 2628 2629 if (lineend > linestart && mText.charAt(lineend - 1) == '\n') { 2630 lineend--; 2631 } 2632 2633 for (int i = 0; i < dirs.mDirections.length; i += 2) { 2634 int here = linestart + dirs.mDirections[i]; 2635 int there = here + (dirs.mDirections[i + 1] & RUN_LENGTH_MASK); 2636 2637 if (there > lineend) { 2638 there = lineend; 2639 } 2640 2641 if (start <= there && end >= here) { 2642 int st = Math.max(start, here); 2643 int en = Math.min(end, there); 2644 2645 if (st != en) { 2646 float h1 = getHorizontal(st, false, line, false /* not clamped */); 2647 float h2 = getHorizontal(en, true, line, false /* not clamped */); 2648 2649 float left = Math.min(h1, h2); 2650 float right = Math.max(h1, h2); 2651 2652 final @TextSelectionLayout int layout = 2653 ((dirs.mDirections[i + 1] & RUN_RTL_FLAG) != 0) 2654 ? TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT 2655 : TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT; 2656 2657 consumer.accept(left, top, right, bottom, layout); 2658 } 2659 } 2660 } 2661 } 2662 2663 /** 2664 * Fills in the specified Path with a representation of a highlight 2665 * between the specified offsets. This will often be a rectangle 2666 * or a potentially discontinuous set of rectangles. If the start 2667 * and end are the same, the returned path is empty. 2668 */ getSelectionPath(int start, int end, Path dest)2669 public void getSelectionPath(int start, int end, Path dest) { 2670 dest.reset(); 2671 getSelection(start, end, (left, top, right, bottom, textSelectionLayout) -> 2672 dest.addRect(left, top, right, bottom, Path.Direction.CW)); 2673 } 2674 2675 /** 2676 * Calculates the rectangles which should be highlighted to indicate a selection between start 2677 * and end and feeds them into the given {@link SelectionRectangleConsumer}. 2678 * 2679 * @param start the starting index of the selection 2680 * @param end the ending index of the selection 2681 * @param consumer the {@link SelectionRectangleConsumer} which will receive the generated 2682 * rectangles. It will be called every time a rectangle is generated. 2683 * @hide 2684 * @see #getSelectionPath(int, int, Path) 2685 */ getSelection(int start, int end, final SelectionRectangleConsumer consumer)2686 public final void getSelection(int start, int end, final SelectionRectangleConsumer consumer) { 2687 if (start == end) { 2688 return; 2689 } 2690 2691 if (end < start) { 2692 int temp = end; 2693 end = start; 2694 start = temp; 2695 } 2696 2697 final int startline = getLineForOffset(start); 2698 final int endline = getLineForOffset(end); 2699 2700 int top = getLineTop(startline); 2701 int bottom = getLineBottom(endline, /* includeLineSpacing= */ false); 2702 2703 if (startline == endline) { 2704 addSelection(startline, start, end, top, bottom, consumer); 2705 } else { 2706 final float width = mWidth; 2707 2708 addSelection(startline, start, getLineEnd(startline), 2709 top, getLineBottom(startline), consumer); 2710 2711 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) { 2712 consumer.accept(getLineLeft(startline), top, 0, getLineBottom(startline), 2713 TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); 2714 } else { 2715 consumer.accept(getLineRight(startline), top, width, getLineBottom(startline), 2716 TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT); 2717 } 2718 2719 for (int i = startline + 1; i < endline; i++) { 2720 top = getLineTop(i); 2721 bottom = getLineBottom(i); 2722 if (getParagraphDirection(i) == DIR_RIGHT_TO_LEFT) { 2723 consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); 2724 } else { 2725 consumer.accept(0, top, width, bottom, TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT); 2726 } 2727 } 2728 2729 top = getLineTop(endline); 2730 bottom = getLineBottom(endline, /* includeLineSpacing= */ false); 2731 2732 addSelection(endline, getLineStart(endline), end, top, bottom, consumer); 2733 2734 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) { 2735 consumer.accept(width, top, getLineRight(endline), bottom, 2736 TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT); 2737 } else { 2738 consumer.accept(0, top, getLineLeft(endline), bottom, 2739 TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT); 2740 } 2741 } 2742 } 2743 2744 /** 2745 * Get the alignment of the specified paragraph, taking into account 2746 * markup attached to it. 2747 */ getParagraphAlignment(int line)2748 public final Alignment getParagraphAlignment(int line) { 2749 Alignment align = mAlignment; 2750 2751 if (mSpannedText) { 2752 Spanned sp = (Spanned) mText; 2753 AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line), 2754 getLineEnd(line), 2755 AlignmentSpan.class); 2756 2757 int spanLength = spans.length; 2758 if (spanLength > 0) { 2759 align = spans[spanLength-1].getAlignment(); 2760 } 2761 } 2762 2763 return align; 2764 } 2765 2766 /** 2767 * Get the left edge of the specified paragraph, inset by left margins. 2768 */ getParagraphLeft(int line)2769 public final int getParagraphLeft(int line) { 2770 int left = 0; 2771 int dir = getParagraphDirection(line); 2772 if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) { 2773 return left; // leading margin has no impact, or no styles 2774 } 2775 return getParagraphLeadingMargin(line); 2776 } 2777 2778 /** 2779 * Get the right edge of the specified paragraph, inset by right margins. 2780 */ getParagraphRight(int line)2781 public final int getParagraphRight(int line) { 2782 int right = mWidth; 2783 int dir = getParagraphDirection(line); 2784 if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) { 2785 return right; // leading margin has no impact, or no styles 2786 } 2787 return right - getParagraphLeadingMargin(line); 2788 } 2789 2790 /** 2791 * Returns the effective leading margin (unsigned) for this line, 2792 * taking into account LeadingMarginSpan and LeadingMarginSpan2. 2793 * @param line the line index 2794 * @return the leading margin of this line 2795 */ getParagraphLeadingMargin(int line)2796 private int getParagraphLeadingMargin(int line) { 2797 if (!mSpannedText) { 2798 return 0; 2799 } 2800 Spanned spanned = (Spanned) mText; 2801 2802 int lineStart = getLineStart(line); 2803 int lineEnd = getLineEnd(line); 2804 int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd, 2805 LeadingMarginSpan.class); 2806 LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd, 2807 LeadingMarginSpan.class); 2808 if (spans.length == 0) { 2809 return 0; // no leading margin span; 2810 } 2811 2812 int margin = 0; 2813 2814 boolean useFirstLineMargin = lineStart == 0 || spanned.charAt(lineStart - 1) == '\n'; 2815 for (int i = 0; i < spans.length; i++) { 2816 if (spans[i] instanceof LeadingMarginSpan2) { 2817 int spStart = spanned.getSpanStart(spans[i]); 2818 int spanLine = getLineForOffset(spStart); 2819 int count = ((LeadingMarginSpan2) spans[i]).getLeadingMarginLineCount(); 2820 // if there is more than one LeadingMarginSpan2, use the count that is greatest 2821 useFirstLineMargin |= line < spanLine + count; 2822 } 2823 } 2824 for (int i = 0; i < spans.length; i++) { 2825 LeadingMarginSpan span = spans[i]; 2826 margin += span.getLeadingMargin(useFirstLineMargin); 2827 } 2828 2829 return margin; 2830 } 2831 2832 private static float measurePara(TextPaint paint, CharSequence text, int start, int end, 2833 TextDirectionHeuristic textDir) { 2834 MeasuredParagraph mt = null; 2835 TextLine tl = TextLine.obtain(); 2836 try { 2837 mt = MeasuredParagraph.buildForBidi(text, start, end, textDir, mt); 2838 final char[] chars = mt.getChars(); 2839 final int len = chars.length; 2840 final Directions directions = mt.getDirections(0, len); 2841 final int dir = mt.getParagraphDir(); 2842 boolean hasTabs = false; 2843 TabStops tabStops = null; 2844 // leading margins should be taken into account when measuring a paragraph 2845 int margin = 0; 2846 if (text instanceof Spanned) { 2847 Spanned spanned = (Spanned) text; 2848 LeadingMarginSpan[] spans = getParagraphSpans(spanned, start, end, 2849 LeadingMarginSpan.class); 2850 for (LeadingMarginSpan lms : spans) { 2851 margin += lms.getLeadingMargin(true); 2852 } 2853 } 2854 for (int i = 0; i < len; ++i) { 2855 if (chars[i] == '\t') { 2856 hasTabs = true; 2857 if (text instanceof Spanned) { 2858 Spanned spanned = (Spanned) text; 2859 int spanEnd = spanned.nextSpanTransition(start, end, 2860 TabStopSpan.class); 2861 TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd, 2862 TabStopSpan.class); 2863 if (spans.length > 0) { 2864 tabStops = new TabStops(TAB_INCREMENT, spans); 2865 } 2866 } 2867 break; 2868 } 2869 } 2870 tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops, 2871 0 /* ellipsisStart */, 0 /* ellipsisEnd */, 2872 false /* use fallback line spacing. unused */); 2873 return margin + Math.abs(tl.metrics(null)); 2874 } finally { 2875 TextLine.recycle(tl); 2876 if (mt != null) { 2877 mt.recycle(); 2878 } 2879 } 2880 } 2881 2882 /** 2883 * @hide 2884 */ 2885 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 2886 public static class TabStops { 2887 private float[] mStops; 2888 private int mNumStops; 2889 private float mIncrement; 2890 2891 public TabStops(float increment, Object[] spans) { 2892 reset(increment, spans); 2893 } 2894 2895 void reset(float increment, Object[] spans) { 2896 this.mIncrement = increment; 2897 2898 int ns = 0; 2899 if (spans != null) { 2900 float[] stops = this.mStops; 2901 for (Object o : spans) { 2902 if (o instanceof TabStopSpan) { 2903 if (stops == null) { 2904 stops = new float[10]; 2905 } else if (ns == stops.length) { 2906 float[] nstops = new float[ns * 2]; 2907 for (int i = 0; i < ns; ++i) { 2908 nstops[i] = stops[i]; 2909 } 2910 stops = nstops; 2911 } 2912 stops[ns++] = ((TabStopSpan) o).getTabStop(); 2913 } 2914 } 2915 if (ns > 1) { 2916 Arrays.sort(stops, 0, ns); 2917 } 2918 if (stops != this.mStops) { 2919 this.mStops = stops; 2920 } 2921 } 2922 this.mNumStops = ns; 2923 } 2924 2925 float nextTab(float h) { 2926 int ns = this.mNumStops; 2927 if (ns > 0) { 2928 float[] stops = this.mStops; 2929 for (int i = 0; i < ns; ++i) { 2930 float stop = stops[i]; 2931 if (stop > h) { 2932 return stop; 2933 } 2934 } 2935 } 2936 return nextDefaultStop(h, mIncrement); 2937 } 2938 2939 /** 2940 * Returns the position of next tab stop. 2941 */ 2942 public static float nextDefaultStop(float h, float inc) { 2943 return ((int) ((h + inc) / inc)) * inc; 2944 } 2945 } 2946 2947 /** 2948 * Returns the position of the next tab stop after h on the line. 2949 * 2950 * @param text the text 2951 * @param start start of the line 2952 * @param end limit of the line 2953 * @param h the current horizontal offset 2954 * @param tabs the tabs, can be null. If it is null, any tabs in effect 2955 * on the line will be used. If there are no tabs, a default offset 2956 * will be used to compute the tab stop. 2957 * @return the offset of the next tab stop. 2958 */ 2959 /* package */ static float nextTab(CharSequence text, int start, int end, 2960 float h, Object[] tabs) { 2961 float nh = Float.MAX_VALUE; 2962 boolean alltabs = false; 2963 2964 if (text instanceof Spanned) { 2965 if (tabs == null) { 2966 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class); 2967 alltabs = true; 2968 } 2969 2970 for (int i = 0; i < tabs.length; i++) { 2971 if (!alltabs) { 2972 if (!(tabs[i] instanceof TabStopSpan)) 2973 continue; 2974 } 2975 2976 int where = ((TabStopSpan) tabs[i]).getTabStop(); 2977 2978 if (where < nh && where > h) 2979 nh = where; 2980 } 2981 2982 if (nh != Float.MAX_VALUE) 2983 return nh; 2984 } 2985 2986 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; 2987 } 2988 2989 protected final boolean isSpanned() { 2990 return mSpannedText; 2991 } 2992 2993 /** 2994 * Returns the same as <code>text.getSpans()</code>, except where 2995 * <code>start</code> and <code>end</code> are the same and are not 2996 * at the very beginning of the text, in which case an empty array 2997 * is returned instead. 2998 * <p> 2999 * This is needed because of the special case that <code>getSpans()</code> 3000 * on an empty range returns the spans adjacent to that range, which is 3001 * primarily for the sake of <code>TextWatchers</code> so they will get 3002 * notifications when text goes from empty to non-empty. But it also 3003 * has the unfortunate side effect that if the text ends with an empty 3004 * paragraph, that paragraph accidentally picks up the styles of the 3005 * preceding paragraph (even though those styles will not be picked up 3006 * by new text that is inserted into the empty paragraph). 3007 * <p> 3008 * The reason it just checks whether <code>start</code> and <code>end</code> 3009 * is the same is that the only time a line can contain 0 characters 3010 * is if it is the final paragraph of the Layout; otherwise any line will 3011 * contain at least one printing or newline character. The reason for the 3012 * additional check if <code>start</code> is greater than 0 is that 3013 * if the empty paragraph is the entire content of the buffer, paragraph 3014 * styles that are already applied to the buffer will apply to text that 3015 * is inserted into it. 3016 */ 3017 /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) { 3018 if (start == end && start > 0) { 3019 return ArrayUtils.emptyArray(type); 3020 } 3021 3022 if(text instanceof SpannableStringBuilder) { 3023 return ((SpannableStringBuilder) text).getSpans(start, end, type, false); 3024 } else { 3025 return text.getSpans(start, end, type); 3026 } 3027 } 3028 3029 private void ellipsize(int start, int end, int line, 3030 char[] dest, int destoff, TextUtils.TruncateAt method) { 3031 final int ellipsisCount = getEllipsisCount(line); 3032 if (ellipsisCount == 0) { 3033 return; 3034 } 3035 final int ellipsisStart = getEllipsisStart(line); 3036 final int lineStart = getLineStart(line); 3037 3038 final String ellipsisString = TextUtils.getEllipsisString(method); 3039 final int ellipsisStringLen = ellipsisString.length(); 3040 // Use the ellipsis string only if there are that at least as many characters to replace. 3041 final boolean useEllipsisString = ellipsisCount >= ellipsisStringLen; 3042 final int min = Math.max(0, start - ellipsisStart - lineStart); 3043 final int max = Math.min(ellipsisCount, end - ellipsisStart - lineStart); 3044 3045 for (int i = min; i < max; i++) { 3046 final char c; 3047 if (useEllipsisString && i < ellipsisStringLen) { 3048 c = ellipsisString.charAt(i); 3049 } else { 3050 c = TextUtils.ELLIPSIS_FILLER; 3051 } 3052 3053 final int a = i + ellipsisStart + lineStart; 3054 dest[destoff + a - start] = c; 3055 } 3056 } 3057 3058 /** 3059 * Stores information about bidirectional (left-to-right or right-to-left) 3060 * text within the layout of a line. 3061 */ 3062 public static class Directions { 3063 /** 3064 * Directions represents directional runs within a line of text. Runs are pairs of ints 3065 * listed in visual order, starting from the leading margin. The first int of each pair is 3066 * the offset from the first character of the line to the start of the run. The second int 3067 * represents both the length and level of the run. The length is in the lower bits, 3068 * accessed by masking with RUN_LENGTH_MASK. The level is in the higher bits, accessed by 3069 * shifting by RUN_LEVEL_SHIFT and masking by RUN_LEVEL_MASK. To simply test for an RTL 3070 * direction, test the bit using RUN_RTL_FLAG, if set then the direction is rtl. 3071 * @hide 3072 */ 3073 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 3074 public int[] mDirections; 3075 3076 /** 3077 * @hide 3078 */ 3079 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) Directions(int[] dirs)3080 public Directions(int[] dirs) { 3081 mDirections = dirs; 3082 } 3083 3084 /** 3085 * Returns number of BiDi runs. 3086 * 3087 * @hide 3088 */ getRunCount()3089 public @IntRange(from = 0) int getRunCount() { 3090 return mDirections.length / 2; 3091 } 3092 3093 /** 3094 * Returns the start offset of the BiDi run. 3095 * 3096 * @param runIndex the index of the BiDi run 3097 * @return the start offset of the BiDi run. 3098 * @hide 3099 */ getRunStart(@ntRangefrom = 0) int runIndex)3100 public @IntRange(from = 0) int getRunStart(@IntRange(from = 0) int runIndex) { 3101 return mDirections[runIndex * 2]; 3102 } 3103 3104 /** 3105 * Returns the length of the BiDi run. 3106 * 3107 * Note that this method may return too large number due to reducing the number of object 3108 * allocations. The too large number means the remaining part is assigned to this run. The 3109 * caller must clamp the returned value. 3110 * 3111 * @param runIndex the index of the BiDi run 3112 * @return the length of the BiDi run. 3113 * @hide 3114 */ getRunLength(@ntRangefrom = 0) int runIndex)3115 public @IntRange(from = 0) int getRunLength(@IntRange(from = 0) int runIndex) { 3116 return mDirections[runIndex * 2 + 1] & RUN_LENGTH_MASK; 3117 } 3118 3119 /** 3120 * Returns the BiDi level of this run. 3121 * 3122 * @param runIndex the index of the BiDi run 3123 * @return the BiDi level of this run. 3124 * @hide 3125 */ 3126 @IntRange(from = 0) getRunLevel(int runIndex)3127 public int getRunLevel(int runIndex) { 3128 return (mDirections[runIndex * 2 + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 3129 } 3130 3131 /** 3132 * Returns true if the BiDi run is RTL. 3133 * 3134 * @param runIndex the index of the BiDi run 3135 * @return true if the BiDi run is RTL. 3136 * @hide 3137 */ isRunRtl(int runIndex)3138 public boolean isRunRtl(int runIndex) { 3139 return (mDirections[runIndex * 2 + 1] & RUN_RTL_FLAG) != 0; 3140 } 3141 } 3142 3143 /** 3144 * Return the offset of the first character to be ellipsized away, 3145 * relative to the start of the line. (So 0 if the beginning of the 3146 * line is ellipsized, not getLineStart().) 3147 */ 3148 public abstract int getEllipsisStart(int line); 3149 3150 /** 3151 * Returns the number of characters to be ellipsized away, or 0 if 3152 * no ellipsis is to take place. 3153 */ 3154 public abstract int getEllipsisCount(int line); 3155 3156 /* package */ static class Ellipsizer implements CharSequence, GetChars { 3157 /* package */ CharSequence mText; 3158 /* package */ Layout mLayout; 3159 /* package */ int mWidth; 3160 /* package */ TextUtils.TruncateAt mMethod; 3161 Ellipsizer(CharSequence s)3162 public Ellipsizer(CharSequence s) { 3163 mText = s; 3164 } 3165 charAt(int off)3166 public char charAt(int off) { 3167 char[] buf = TextUtils.obtain(1); 3168 getChars(off, off + 1, buf, 0); 3169 char ret = buf[0]; 3170 3171 TextUtils.recycle(buf); 3172 return ret; 3173 } 3174 getChars(int start, int end, char[] dest, int destoff)3175 public void getChars(int start, int end, char[] dest, int destoff) { 3176 int line1 = mLayout.getLineForOffset(start); 3177 int line2 = mLayout.getLineForOffset(end); 3178 3179 TextUtils.getChars(mText, start, end, dest, destoff); 3180 3181 for (int i = line1; i <= line2; i++) { 3182 mLayout.ellipsize(start, end, i, dest, destoff, mMethod); 3183 } 3184 } 3185 length()3186 public int length() { 3187 return mText.length(); 3188 } 3189 subSequence(int start, int end)3190 public CharSequence subSequence(int start, int end) { 3191 char[] s = new char[end - start]; 3192 getChars(start, end, s, 0); 3193 return new String(s); 3194 } 3195 3196 @Override toString()3197 public String toString() { 3198 char[] s = new char[length()]; 3199 getChars(0, length(), s, 0); 3200 return new String(s); 3201 } 3202 3203 } 3204 3205 /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned { 3206 private Spanned mSpanned; 3207 SpannedEllipsizer(CharSequence display)3208 public SpannedEllipsizer(CharSequence display) { 3209 super(display); 3210 mSpanned = (Spanned) display; 3211 } 3212 getSpans(int start, int end, Class<T> type)3213 public <T> T[] getSpans(int start, int end, Class<T> type) { 3214 return mSpanned.getSpans(start, end, type); 3215 } 3216 getSpanStart(Object tag)3217 public int getSpanStart(Object tag) { 3218 return mSpanned.getSpanStart(tag); 3219 } 3220 getSpanEnd(Object tag)3221 public int getSpanEnd(Object tag) { 3222 return mSpanned.getSpanEnd(tag); 3223 } 3224 getSpanFlags(Object tag)3225 public int getSpanFlags(Object tag) { 3226 return mSpanned.getSpanFlags(tag); 3227 } 3228 3229 @SuppressWarnings("rawtypes") nextSpanTransition(int start, int limit, Class type)3230 public int nextSpanTransition(int start, int limit, Class type) { 3231 return mSpanned.nextSpanTransition(start, limit, type); 3232 } 3233 3234 @Override subSequence(int start, int end)3235 public CharSequence subSequence(int start, int end) { 3236 char[] s = new char[end - start]; 3237 getChars(start, end, s, 0); 3238 3239 SpannableString ss = new SpannableString(new String(s)); 3240 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); 3241 return ss; 3242 } 3243 } 3244 3245 private CharSequence mText; 3246 @UnsupportedAppUsage 3247 private TextPaint mPaint; 3248 private TextPaint mWorkPaint = new TextPaint(); 3249 private int mWidth; 3250 private Alignment mAlignment = Alignment.ALIGN_NORMAL; 3251 private float mSpacingMult; 3252 private float mSpacingAdd; 3253 private static final Rect sTempRect = new Rect(); 3254 private boolean mSpannedText; 3255 private TextDirectionHeuristic mTextDir; 3256 private SpanSet<LineBackgroundSpan> mLineBackgroundSpans; 3257 private int mJustificationMode; 3258 3259 /** @hide */ 3260 @IntDef(prefix = { "DIR_" }, value = { 3261 DIR_LEFT_TO_RIGHT, 3262 DIR_RIGHT_TO_LEFT 3263 }) 3264 @Retention(RetentionPolicy.SOURCE) 3265 public @interface Direction {} 3266 3267 public static final int DIR_LEFT_TO_RIGHT = 1; 3268 public static final int DIR_RIGHT_TO_LEFT = -1; 3269 3270 /* package */ static final int DIR_REQUEST_LTR = 1; 3271 /* package */ static final int DIR_REQUEST_RTL = -1; 3272 @UnsupportedAppUsage 3273 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; 3274 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; 3275 3276 /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; 3277 /* package */ static final int RUN_LEVEL_SHIFT = 26; 3278 /* package */ static final int RUN_LEVEL_MASK = 0x3f; 3279 /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; 3280 3281 public enum Alignment { 3282 ALIGN_NORMAL, 3283 ALIGN_OPPOSITE, 3284 ALIGN_CENTER, 3285 /** @hide */ 3286 @UnsupportedAppUsage 3287 ALIGN_LEFT, 3288 /** @hide */ 3289 @UnsupportedAppUsage 3290 ALIGN_RIGHT, 3291 } 3292 3293 private static final float TAB_INCREMENT = 20; 3294 3295 /** @hide */ 3296 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 3297 @UnsupportedAppUsage 3298 public static final Directions DIRS_ALL_LEFT_TO_RIGHT = 3299 new Directions(new int[] { 0, RUN_LENGTH_MASK }); 3300 3301 /** @hide */ 3302 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 3303 @UnsupportedAppUsage 3304 public static final Directions DIRS_ALL_RIGHT_TO_LEFT = 3305 new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); 3306 3307 /** @hide */ 3308 @Retention(RetentionPolicy.SOURCE) 3309 @IntDef(prefix = { "TEXT_SELECTION_LAYOUT_" }, value = { 3310 TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT, 3311 TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT 3312 }) 3313 public @interface TextSelectionLayout {} 3314 3315 /** @hide */ 3316 public static final int TEXT_SELECTION_LAYOUT_RIGHT_TO_LEFT = 0; 3317 /** @hide */ 3318 public static final int TEXT_SELECTION_LAYOUT_LEFT_TO_RIGHT = 1; 3319 3320 /** @hide */ 3321 @FunctionalInterface 3322 public interface SelectionRectangleConsumer { 3323 /** 3324 * Performs this operation on the given rectangle. 3325 * 3326 * @param left the left edge of the rectangle 3327 * @param top the top edge of the rectangle 3328 * @param right the right edge of the rectangle 3329 * @param bottom the bottom edge of the rectangle 3330 * @param textSelectionLayout the layout (RTL or LTR) of the text covered by this 3331 * selection rectangle 3332 */ 3333 void accept(float left, float top, float right, float bottom, 3334 @TextSelectionLayout int textSelectionLayout); 3335 } 3336 3337 /** 3338 * Strategy for determining whether a text segment is inside a rectangle area. 3339 * 3340 * @see #getRangeForRect(RectF, SegmentFinder, TextInclusionStrategy) 3341 */ 3342 @FunctionalInterface 3343 public interface TextInclusionStrategy { 3344 /** 3345 * Returns true if this {@link TextInclusionStrategy} considers the segment with bounds 3346 * {@code segmentBounds} to be inside {@code area}. 3347 * 3348 * <p>The segment is a range of text which does not cross line boundaries or directional run 3349 * boundaries. The horizontal bounds of the segment are the start bound of the first 3350 * character to the end bound of the last character. The vertical bounds match the line 3351 * bounds ({@code getLineTop(line)} and {@code getLineBottom(line, false)}). 3352 */ 3353 boolean isSegmentInside(@NonNull RectF segmentBounds, @NonNull RectF area); 3354 } 3355 } 3356