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