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