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