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