1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.text; 18 19 import android.annotation.IntDef; 20 import android.annotation.IntRange; 21 import android.annotation.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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 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 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 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 */ 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 */ 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 */ 757 public final TextPaint getPaint() { 758 return mPaint; 759 } 760 761 /** 762 * Return the width of this layout. 763 */ 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 */ 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 */ 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 */ 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 */ 803 public int getHeight(boolean cap) { 804 return getHeight(); 805 } 806 807 /** 808 * Return the base alignment of this layout. 809 */ 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 */ 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 */ 824 public final float getSpacingAdd() { 825 return mSpacingAdd; 826 } 827 828 /** 829 * Return the heuristic used to determine paragraph text direction. 830 * @hide 831 */ 832 public final TextDirectionHeuristic getTextDirectionHeuristic() { 833 return mTextDir; 834 } 835 836 /** 837 * Return the number of lines of text in this layout. 838 */ 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 */ 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 */ 865 public abstract int getLineTop(int line); 866 867 /** 868 * Return the descent of the specified line(0…getLineCount() - 1). 869 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 911 public abstract int getBottomPadding(); 912 913 /** 914 * Returns the start hyphen edit for a line. 915 * 916 * @hide 917 */ 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 */ 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 */ 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 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 */ 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 */ 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 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 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 */ 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 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 */ 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 1178 public float getSecondaryHorizontal(int offset, boolean clamped) { 1179 boolean trailing = primaryIsTrailingPrevious(offset); 1180 return getHorizontal(offset, !trailing, clamped); 1181 } 1182 1183 private float getHorizontal(int offset, boolean primary) { 1184 return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset); 1185 } 1186 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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. 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 */ 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 */ 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 */ 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 1645 HorizontalMeasurementProvider(final int line, final boolean primary) { 1646 mLine = line; 1647 mPrimary = primary; 1648 init(); 1649 } 1650 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 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 */ 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 */ 1682 public int getLineVisibleEnd(int line) { 1683 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 1684 } 1685 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 */ 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 */ 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 */ 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 */ 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 */ 1750 public int getLineExtra(@IntRange(from = 0) int line) { 1751 return 0; 1752 } 1753 1754 public int getOffsetToLeftOf(int offset) { 1755 return getOffsetToLeftRightOf(offset, true); 1756 } 1757 1758 public int getOffsetToRightOf(int offset) { 1759 return getOffsetToLeftRightOf(offset, false); 1760 } 1761 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 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 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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) 2390 public Directions(int[] dirs) { 2391 mDirections = dirs; 2392 } 2393 2394 /** 2395 * Returns number of BiDi runs. 2396 * 2397 * @hide 2398 */ 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 */ 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 */ 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 */ 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 2460 public Ellipsizer(CharSequence s) { 2461 mText = s; 2462 } 2463 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 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 2484 public int length() { 2485 return mText.length(); 2486 } 2487 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 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 2506 public SpannedEllipsizer(CharSequence display) { 2507 super(display); 2508 mSpanned = (Spanned) display; 2509 } 2510 2511 public <T> T[] getSpans(int start, int end, Class<T> type) { 2512 return mSpanned.getSpans(start, end, type); 2513 } 2514 2515 public int getSpanStart(Object tag) { 2516 return mSpanned.getSpanStart(tag); 2517 } 2518 2519 public int getSpanEnd(Object tag) { 2520 return mSpanned.getSpanEnd(tag); 2521 } 2522 2523 public int getSpanFlags(Object tag) { 2524 return mSpanned.getSpanFlags(tag); 2525 } 2526 2527 @SuppressWarnings("rawtypes") 2528 public int nextSpanTransition(int start, int limit, Class type) { 2529 return mSpanned.nextSpanTransition(start, limit, type); 2530 } 2531 2532 @Override 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