Layout.java revision c0ccf0c47c00942a9d0f2670600a8f2d4d7adb5b
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 17package android.text; 18 19import com.android.internal.util.ArrayUtils; 20 21import android.emoji.EmojiFactory; 22import android.graphics.Canvas; 23import android.graphics.Paint; 24import android.graphics.Path; 25import android.graphics.Rect; 26import android.text.method.TextKeyListener; 27import android.text.style.AlignmentSpan; 28import android.text.style.LeadingMarginSpan; 29import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; 30import android.text.style.LineBackgroundSpan; 31import android.text.style.ParagraphStyle; 32import android.text.style.ReplacementSpan; 33import android.text.style.TabStopSpan; 34 35import java.util.Arrays; 36 37/** 38 * A base class that manages text layout in visual elements on 39 * the screen. 40 * <p>For text that will be edited, use a {@link DynamicLayout}, 41 * which will be updated as the text changes. 42 * For text that will not change, use a {@link StaticLayout}. 43 */ 44public abstract class Layout { 45 private static final ParagraphStyle[] NO_PARA_SPANS = 46 ArrayUtils.emptyArray(ParagraphStyle.class); 47 48 /* package */ static final EmojiFactory EMOJI_FACTORY = 49 EmojiFactory.newAvailableInstance(); 50 /* package */ static final int MIN_EMOJI, MAX_EMOJI; 51 52 static { 53 if (EMOJI_FACTORY != null) { 54 MIN_EMOJI = EMOJI_FACTORY.getMinimumAndroidPua(); 55 MAX_EMOJI = EMOJI_FACTORY.getMaximumAndroidPua(); 56 } else { 57 MIN_EMOJI = -1; 58 MAX_EMOJI = -1; 59 } 60 } 61 62 /** 63 * Return how wide a layout must be in order to display the 64 * specified text with one line per paragraph. 65 */ 66 public static float getDesiredWidth(CharSequence source, 67 TextPaint paint) { 68 return getDesiredWidth(source, 0, source.length(), paint); 69 } 70 71 /** 72 * Return how wide a layout must be in order to display the 73 * specified text slice with one line per paragraph. 74 */ 75 public static float getDesiredWidth(CharSequence source, 76 int start, int end, 77 TextPaint paint) { 78 float need = 0; 79 TextPaint workPaint = new TextPaint(); 80 81 int next; 82 for (int i = start; i <= end; i = next) { 83 next = TextUtils.indexOf(source, '\n', i, end); 84 85 if (next < 0) 86 next = end; 87 88 // note, omits trailing paragraph char 89 float w = measurePara(paint, workPaint, source, i, next); 90 91 if (w > need) 92 need = w; 93 94 next++; 95 } 96 97 return need; 98 } 99 100 /** 101 * Subclasses of Layout use this constructor to set the display text, 102 * width, and other standard properties. 103 * @param text the text to render 104 * @param paint the default paint for the layout. Styles can override 105 * various attributes of the paint. 106 * @param width the wrapping width for the text. 107 * @param align whether to left, right, or center the text. Styles can 108 * override the alignment. 109 * @param spacingMult factor by which to scale the font size to get the 110 * default line spacing 111 * @param spacingAdd amount to add to the default line spacing 112 */ 113 protected Layout(CharSequence text, TextPaint paint, 114 int width, Alignment align, 115 float spacingMult, float spacingAdd) { 116 if (width < 0) 117 throw new IllegalArgumentException("Layout: " + width + " < 0"); 118 119 // Ensure paint doesn't have baselineShift set. 120 // While normally we don't modify the paint the user passed in, 121 // we were already doing this in Styled.drawUniformRun with both 122 // baselineShift and bgColor. We probably should reevaluate bgColor. 123 if (paint != null) { 124 paint.bgColor = 0; 125 paint.baselineShift = 0; 126 } 127 128 mText = text; 129 mPaint = paint; 130 mWorkPaint = new TextPaint(); 131 mWidth = width; 132 mAlignment = align; 133 mSpacingMult = spacingMult; 134 mSpacingAdd = spacingAdd; 135 mSpannedText = text instanceof Spanned; 136 } 137 138 /** 139 * Replace constructor properties of this Layout with new ones. Be careful. 140 */ 141 /* package */ void replaceWith(CharSequence text, TextPaint paint, 142 int width, Alignment align, 143 float spacingmult, float spacingadd) { 144 if (width < 0) { 145 throw new IllegalArgumentException("Layout: " + width + " < 0"); 146 } 147 148 mText = text; 149 mPaint = paint; 150 mWidth = width; 151 mAlignment = align; 152 mSpacingMult = spacingmult; 153 mSpacingAdd = spacingadd; 154 mSpannedText = text instanceof Spanned; 155 } 156 157 /** 158 * Draw this Layout on the specified Canvas. 159 */ 160 public void draw(Canvas c) { 161 draw(c, null, null, 0); 162 } 163 164 /** 165 * Draw this Layout on the specified canvas, with the highlight path drawn 166 * between the background and the text. 167 * 168 * @param c the canvas 169 * @param highlight the path of the highlight or cursor; can be null 170 * @param highlightPaint the paint for the highlight 171 * @param cursorOffsetVertical the amount to temporarily translate the 172 * canvas while rendering the highlight 173 */ 174 public void draw(Canvas c, Path highlight, Paint highlightPaint, 175 int cursorOffsetVertical) { 176 int dtop, dbottom; 177 178 synchronized (sTempRect) { 179 if (!c.getClipBounds(sTempRect)) { 180 return; 181 } 182 183 dtop = sTempRect.top; 184 dbottom = sTempRect.bottom; 185 } 186 187 int top = 0; 188 int bottom = getLineTop(getLineCount()); 189 190 if (dtop > top) { 191 top = dtop; 192 } 193 if (dbottom < bottom) { 194 bottom = dbottom; 195 } 196 197 int first = getLineForVertical(top); 198 int last = getLineForVertical(bottom); 199 200 int previousLineBottom = getLineTop(first); 201 int previousLineEnd = getLineStart(first); 202 203 TextPaint paint = mPaint; 204 CharSequence buf = mText; 205 int width = mWidth; 206 boolean spannedText = mSpannedText; 207 208 ParagraphStyle[] spans = NO_PARA_SPANS; 209 int spanEnd = 0; 210 int textLength = 0; 211 212 // First, draw LineBackgroundSpans. 213 // LineBackgroundSpans know nothing about the alignment, margins, or 214 // direction of the layout or line. XXX: Should they? 215 // They are evaluated at each line. 216 if (spannedText) { 217 Spanned sp = (Spanned) buf; 218 textLength = buf.length(); 219 for (int i = first; i <= last; i++) { 220 int start = previousLineEnd; 221 int end = getLineStart(i+1); 222 previousLineEnd = end; 223 224 int ltop = previousLineBottom; 225 int lbottom = getLineTop(i+1); 226 previousLineBottom = lbottom; 227 int lbaseline = lbottom - getLineDescent(i); 228 229 if (start >= spanEnd) { 230 // These should be infrequent, so we'll use this so that 231 // we don't have to check as often. 232 spanEnd = sp.nextSpanTransition(start, textLength, 233 LineBackgroundSpan.class); 234 // All LineBackgroundSpans on a line contribute to its 235 // background. 236 spans = getParagraphSpans(sp, start, end, LineBackgroundSpan.class); 237 } 238 239 for (int n = 0; n < spans.length; n++) { 240 LineBackgroundSpan back = (LineBackgroundSpan) spans[n]; 241 242 back.drawBackground(c, paint, 0, width, 243 ltop, lbaseline, lbottom, 244 buf, start, end, 245 i); 246 } 247 } 248 // reset to their original values 249 spanEnd = 0; 250 previousLineBottom = getLineTop(first); 251 previousLineEnd = getLineStart(first); 252 spans = NO_PARA_SPANS; 253 } 254 255 // There can be a highlight even without spans if we are drawing 256 // a non-spanned transformation of a spanned editing buffer. 257 if (highlight != null) { 258 if (cursorOffsetVertical != 0) { 259 c.translate(0, cursorOffsetVertical); 260 } 261 262 c.drawPath(highlight, highlightPaint); 263 264 if (cursorOffsetVertical != 0) { 265 c.translate(0, -cursorOffsetVertical); 266 } 267 } 268 269 Alignment paraAlign = mAlignment; 270 TabStops tabStops = null; 271 boolean tabStopsIsInitialized = false; 272 273 TextLine tl = TextLine.obtain(); 274 275 // Next draw the lines, one at a time. 276 // the baseline is the top of the following line minus the current 277 // line's descent. 278 for (int i = first; i <= last; i++) { 279 int start = previousLineEnd; 280 281 previousLineEnd = getLineStart(i+1); 282 int end = getLineVisibleEnd(i, start, previousLineEnd); 283 284 int ltop = previousLineBottom; 285 int lbottom = getLineTop(i+1); 286 previousLineBottom = lbottom; 287 int lbaseline = lbottom - getLineDescent(i); 288 289 int dir = getParagraphDirection(i); 290 int left = 0; 291 int right = mWidth; 292 293 if (spannedText) { 294 Spanned sp = (Spanned) buf; 295 boolean isFirstParaLine = (start == 0 || 296 buf.charAt(start - 1) == '\n'); 297 298 // New batch of paragraph styles, collect into spans array. 299 // Compute the alignment, last alignment style wins. 300 // Reset tabStops, we'll rebuild if we encounter a line with 301 // tabs. 302 // We expect paragraph spans to be relatively infrequent, use 303 // spanEnd so that we can check less frequently. Since 304 // paragraph styles ought to apply to entire paragraphs, we can 305 // just collect the ones present at the start of the paragraph. 306 // If spanEnd is before the end of the paragraph, that's not 307 // our problem. 308 if (start >= spanEnd && (i == first || isFirstParaLine)) { 309 spanEnd = sp.nextSpanTransition(start, textLength, 310 ParagraphStyle.class); 311 spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class); 312 313 paraAlign = mAlignment; 314 for (int n = spans.length-1; n >= 0; n--) { 315 if (spans[n] instanceof AlignmentSpan) { 316 paraAlign = ((AlignmentSpan) spans[n]).getAlignment(); 317 break; 318 } 319 } 320 321 tabStopsIsInitialized = false; 322 } 323 324 // Draw all leading margin spans. Adjust left or right according 325 // to the paragraph direction of the line. 326 final int length = spans.length; 327 for (int n = 0; n < length; n++) { 328 if (spans[n] instanceof LeadingMarginSpan) { 329 LeadingMarginSpan margin = (LeadingMarginSpan) spans[n]; 330 boolean useFirstLineMargin = isFirstParaLine; 331 if (margin instanceof LeadingMarginSpan2) { 332 int count = ((LeadingMarginSpan2) margin).getLeadingMarginLineCount(); 333 int startLine = getLineForOffset(sp.getSpanStart(margin)); 334 useFirstLineMargin = i < startLine + count; 335 } 336 337 if (dir == DIR_RIGHT_TO_LEFT) { 338 margin.drawLeadingMargin(c, paint, right, dir, ltop, 339 lbaseline, lbottom, buf, 340 start, end, isFirstParaLine, this); 341 right -= margin.getLeadingMargin(useFirstLineMargin); 342 } else { 343 margin.drawLeadingMargin(c, paint, left, dir, ltop, 344 lbaseline, lbottom, buf, 345 start, end, isFirstParaLine, this); 346 left += margin.getLeadingMargin(useFirstLineMargin); 347 } 348 } 349 } 350 } 351 352 boolean hasTabOrEmoji = getLineContainsTab(i); 353 // Can't tell if we have tabs for sure, currently 354 if (hasTabOrEmoji && !tabStopsIsInitialized) { 355 if (tabStops == null) { 356 tabStops = new TabStops(TAB_INCREMENT, spans); 357 } else { 358 tabStops.reset(TAB_INCREMENT, spans); 359 } 360 tabStopsIsInitialized = true; 361 } 362 363 // Determine whether the line aligns to normal, opposite, or center. 364 Alignment align = paraAlign; 365 if (align == Alignment.ALIGN_LEFT) { 366 align = (dir == DIR_LEFT_TO_RIGHT) ? 367 Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE; 368 } else if (align == Alignment.ALIGN_RIGHT) { 369 align = (dir == DIR_LEFT_TO_RIGHT) ? 370 Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL; 371 } 372 373 int x; 374 if (align == Alignment.ALIGN_NORMAL) { 375 if (dir == DIR_LEFT_TO_RIGHT) { 376 x = left; 377 } else { 378 x = right; 379 } 380 } else { 381 int max = (int)getLineExtent(i, tabStops, false); 382 if (align == Alignment.ALIGN_OPPOSITE) { 383 if (dir == DIR_LEFT_TO_RIGHT) { 384 x = right - max; 385 } else { 386 x = left - max; 387 } 388 } else { // Alignment.ALIGN_CENTER 389 max = max & ~1; 390 x = (right + left - max) >> 1; 391 } 392 } 393 394 Directions directions = getLineDirections(i); 395 if (directions == DIRS_ALL_LEFT_TO_RIGHT && 396 !spannedText && !hasTabOrEmoji) { 397 // XXX: assumes there's nothing additional to be done 398 c.drawText(buf, start, end, x, lbaseline, paint); 399 } else { 400 tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops); 401 tl.draw(c, x, ltop, lbaseline, lbottom); 402 } 403 } 404 405 TextLine.recycle(tl); 406 } 407 408 /** 409 * Return the start position of the line, given the left and right bounds 410 * of the margins. 411 * 412 * @param line the line index 413 * @param left the left bounds (0, or leading margin if ltr para) 414 * @param right the right bounds (width, minus leading margin if rtl para) 415 * @return the start position of the line (to right of line if rtl para) 416 */ 417 private int getLineStartPos(int line, int left, int right) { 418 // Adjust the point at which to start rendering depending on the 419 // alignment of the paragraph. 420 Alignment align = getParagraphAlignment(line); 421 int dir = getParagraphDirection(line); 422 423 int x; 424 if (align == Alignment.ALIGN_LEFT) { 425 x = left; 426 } else if (align == Alignment.ALIGN_NORMAL) { 427 if (dir == DIR_LEFT_TO_RIGHT) { 428 x = left; 429 } else { 430 x = right; 431 } 432 } else { 433 TabStops tabStops = null; 434 if (mSpannedText && getLineContainsTab(line)) { 435 Spanned spanned = (Spanned) mText; 436 int start = getLineStart(line); 437 int spanEnd = spanned.nextSpanTransition(start, spanned.length(), 438 TabStopSpan.class); 439 TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd, TabStopSpan.class); 440 if (tabSpans.length > 0) { 441 tabStops = new TabStops(TAB_INCREMENT, tabSpans); 442 } 443 } 444 int max = (int)getLineExtent(line, tabStops, false); 445 if (align == Alignment.ALIGN_RIGHT) { 446 x = right - max; 447 } else if (align == Alignment.ALIGN_OPPOSITE) { 448 if (dir == DIR_LEFT_TO_RIGHT) { 449 x = right - max; 450 } else { 451 x = left - max; 452 } 453 } else { // Alignment.ALIGN_CENTER 454 max = max & ~1; 455 x = (left + right - max) >> 1; 456 } 457 } 458 return x; 459 } 460 461 /** 462 * Return the text that is displayed by this Layout. 463 */ 464 public final CharSequence getText() { 465 return mText; 466 } 467 468 /** 469 * Return the base Paint properties for this layout. 470 * Do NOT change the paint, which may result in funny 471 * drawing for this layout. 472 */ 473 public final TextPaint getPaint() { 474 return mPaint; 475 } 476 477 /** 478 * Return the width of this layout. 479 */ 480 public final int getWidth() { 481 return mWidth; 482 } 483 484 /** 485 * Return the width to which this Layout is ellipsizing, or 486 * {@link #getWidth} if it is not doing anything special. 487 */ 488 public int getEllipsizedWidth() { 489 return mWidth; 490 } 491 492 /** 493 * Increase the width of this layout to the specified width. 494 * Be careful to use this only when you know it is appropriate— 495 * it does not cause the text to reflow to use the full new width. 496 */ 497 public final void increaseWidthTo(int wid) { 498 if (wid < mWidth) { 499 throw new RuntimeException("attempted to reduce Layout width"); 500 } 501 502 mWidth = wid; 503 } 504 505 /** 506 * Return the total height of this layout. 507 */ 508 public int getHeight() { 509 return getLineTop(getLineCount()); 510 } 511 512 /** 513 * Return the base alignment of this layout. 514 */ 515 public final Alignment getAlignment() { 516 return mAlignment; 517 } 518 519 /** 520 * Return what the text height is multiplied by to get the line height. 521 */ 522 public final float getSpacingMultiplier() { 523 return mSpacingMult; 524 } 525 526 /** 527 * Return the number of units of leading that are added to each line. 528 */ 529 public final float getSpacingAdd() { 530 return mSpacingAdd; 531 } 532 533 /** 534 * Return the number of lines of text in this layout. 535 */ 536 public abstract int getLineCount(); 537 538 /** 539 * Return the baseline for the specified line (0…getLineCount() - 1) 540 * If bounds is not null, return the top, left, right, bottom extents 541 * of the specified line in it. 542 * @param line which line to examine (0..getLineCount() - 1) 543 * @param bounds Optional. If not null, it returns the extent of the line 544 * @return the Y-coordinate of the baseline 545 */ 546 public int getLineBounds(int line, Rect bounds) { 547 if (bounds != null) { 548 bounds.left = 0; // ??? 549 bounds.top = getLineTop(line); 550 bounds.right = mWidth; // ??? 551 bounds.bottom = getLineTop(line + 1); 552 } 553 return getLineBaseline(line); 554 } 555 556 /** 557 * Return the vertical position of the top of the specified line 558 * (0…getLineCount()). 559 * If the specified line is equal to the line count, returns the 560 * bottom of the last line. 561 */ 562 public abstract int getLineTop(int line); 563 564 /** 565 * Return the descent of the specified line(0…getLineCount() - 1). 566 */ 567 public abstract int getLineDescent(int line); 568 569 /** 570 * Return the text offset of the beginning of the specified line ( 571 * 0…getLineCount()). If the specified line is equal to the line 572 * count, returns the length of the text. 573 */ 574 public abstract int getLineStart(int line); 575 576 /** 577 * Returns the primary directionality of the paragraph containing the 578 * specified line, either 1 for left-to-right lines, or -1 for right-to-left 579 * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}). 580 */ 581 public abstract int getParagraphDirection(int line); 582 583 /** 584 * Returns whether the specified line contains one or more 585 * characters that need to be handled specially, like tabs 586 * or emoji. 587 */ 588 public abstract boolean getLineContainsTab(int line); 589 590 /** 591 * Returns the directional run information for the specified line. 592 * The array alternates counts of characters in left-to-right 593 * and right-to-left segments of the line. 594 * 595 * <p>NOTE: this is inadequate to support bidirectional text, and will change. 596 */ 597 public abstract Directions getLineDirections(int line); 598 599 /** 600 * Returns the (negative) number of extra pixels of ascent padding in the 601 * top line of the Layout. 602 */ 603 public abstract int getTopPadding(); 604 605 /** 606 * Returns the number of extra pixels of descent padding in the 607 * bottom line of the Layout. 608 */ 609 public abstract int getBottomPadding(); 610 611 612 /** 613 * Returns true if the character at offset and the preceding character 614 * are at different run levels (and thus there's a split caret). 615 * @param offset the offset 616 * @return true if at a level boundary 617 * @hide 618 */ 619 public boolean isLevelBoundary(int offset) { 620 int line = getLineForOffset(offset); 621 Directions dirs = getLineDirections(line); 622 if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) { 623 return false; 624 } 625 626 int[] runs = dirs.mDirections; 627 int lineStart = getLineStart(line); 628 int lineEnd = getLineEnd(line); 629 if (offset == lineStart || offset == lineEnd) { 630 int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1; 631 int runIndex = offset == lineStart ? 0 : runs.length - 2; 632 return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel; 633 } 634 635 offset -= lineStart; 636 for (int i = 0; i < runs.length; i += 2) { 637 if (offset == runs[i]) { 638 return true; 639 } 640 } 641 return false; 642 } 643 644 private boolean primaryIsTrailingPrevious(int offset) { 645 int line = getLineForOffset(offset); 646 int lineStart = getLineStart(line); 647 int lineEnd = getLineEnd(line); 648 int[] runs = getLineDirections(line).mDirections; 649 650 int levelAt = -1; 651 for (int i = 0; i < runs.length; i += 2) { 652 int start = lineStart + runs[i]; 653 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 654 if (limit > lineEnd) { 655 limit = lineEnd; 656 } 657 if (offset >= start && offset < limit) { 658 if (offset > start) { 659 // Previous character is at same level, so don't use trailing. 660 return false; 661 } 662 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 663 break; 664 } 665 } 666 if (levelAt == -1) { 667 // Offset was limit of line. 668 levelAt = getParagraphDirection(line) == 1 ? 0 : 1; 669 } 670 671 // At level boundary, check previous level. 672 int levelBefore = -1; 673 if (offset == lineStart) { 674 levelBefore = getParagraphDirection(line) == 1 ? 0 : 1; 675 } else { 676 offset -= 1; 677 for (int i = 0; i < runs.length; i += 2) { 678 int start = lineStart + runs[i]; 679 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 680 if (limit > lineEnd) { 681 limit = lineEnd; 682 } 683 if (offset >= start && offset < limit) { 684 levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 685 break; 686 } 687 } 688 } 689 690 return levelBefore < levelAt; 691 } 692 693 /** 694 * Get the primary horizontal position for the specified text offset. 695 * This is the location where a new character would be inserted in 696 * the paragraph's primary direction. 697 */ 698 public float getPrimaryHorizontal(int offset) { 699 boolean trailing = primaryIsTrailingPrevious(offset); 700 return getHorizontal(offset, trailing); 701 } 702 703 /** 704 * Get the secondary horizontal position for the specified text offset. 705 * This is the location where a new character would be inserted in 706 * the direction other than the paragraph's primary direction. 707 */ 708 public float getSecondaryHorizontal(int offset) { 709 boolean trailing = primaryIsTrailingPrevious(offset); 710 return getHorizontal(offset, !trailing); 711 } 712 713 private float getHorizontal(int offset, boolean trailing) { 714 int line = getLineForOffset(offset); 715 716 return getHorizontal(offset, trailing, line); 717 } 718 719 private float getHorizontal(int offset, boolean trailing, int line) { 720 int start = getLineStart(line); 721 int end = getLineEnd(line); 722 int dir = getParagraphDirection(line); 723 boolean hasTabOrEmoji = getLineContainsTab(line); 724 Directions directions = getLineDirections(line); 725 726 TabStops tabStops = null; 727 if (hasTabOrEmoji && mText instanceof Spanned) { 728 // Just checking this line should be good enough, tabs should be 729 // consistent across all lines in a paragraph. 730 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 731 if (tabs.length > 0) { 732 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 733 } 734 } 735 736 TextLine tl = TextLine.obtain(); 737 tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops); 738 float wid = tl.measure(offset - start, trailing, null); 739 TextLine.recycle(tl); 740 741 int left = getParagraphLeft(line); 742 int right = getParagraphRight(line); 743 744 return getLineStartPos(line, left, right) + wid; 745 } 746 747 /** 748 * Get the leftmost position that should be exposed for horizontal 749 * scrolling on the specified line. 750 */ 751 public float getLineLeft(int line) { 752 int dir = getParagraphDirection(line); 753 Alignment align = getParagraphAlignment(line); 754 755 if (align == Alignment.ALIGN_LEFT) { 756 return 0; 757 } else if (align == Alignment.ALIGN_NORMAL) { 758 if (dir == DIR_RIGHT_TO_LEFT) 759 return getParagraphRight(line) - getLineMax(line); 760 else 761 return 0; 762 } else if (align == Alignment.ALIGN_RIGHT) { 763 return mWidth - getLineMax(line); 764 } else if (align == Alignment.ALIGN_OPPOSITE) { 765 if (dir == DIR_RIGHT_TO_LEFT) 766 return 0; 767 else 768 return mWidth - getLineMax(line); 769 } else { /* align == Alignment.ALIGN_CENTER */ 770 int left = getParagraphLeft(line); 771 int right = getParagraphRight(line); 772 int max = ((int) getLineMax(line)) & ~1; 773 774 return left + ((right - left) - max) / 2; 775 } 776 } 777 778 /** 779 * Get the rightmost position that should be exposed for horizontal 780 * scrolling on the specified line. 781 */ 782 public float getLineRight(int line) { 783 int dir = getParagraphDirection(line); 784 Alignment align = getParagraphAlignment(line); 785 786 if (align == Alignment.ALIGN_LEFT) { 787 return getParagraphLeft(line) + getLineMax(line); 788 } else if (align == Alignment.ALIGN_NORMAL) { 789 if (dir == DIR_RIGHT_TO_LEFT) 790 return mWidth; 791 else 792 return getParagraphLeft(line) + getLineMax(line); 793 } else if (align == Alignment.ALIGN_RIGHT) { 794 return mWidth; 795 } else if (align == Alignment.ALIGN_OPPOSITE) { 796 if (dir == DIR_RIGHT_TO_LEFT) 797 return getLineMax(line); 798 else 799 return mWidth; 800 } else { /* align == Alignment.ALIGN_CENTER */ 801 int left = getParagraphLeft(line); 802 int right = getParagraphRight(line); 803 int max = ((int) getLineMax(line)) & ~1; 804 805 return right - ((right - left) - max) / 2; 806 } 807 } 808 809 /** 810 * Gets the unsigned horizontal extent of the specified line, including 811 * leading margin indent, but excluding trailing whitespace. 812 */ 813 public float getLineMax(int line) { 814 float margin = getParagraphLeadingMargin(line); 815 float signedExtent = getLineExtent(line, false); 816 return margin + signedExtent >= 0 ? signedExtent : -signedExtent; 817 } 818 819 /** 820 * Gets the unsigned horizontal extent of the specified line, including 821 * leading margin indent and trailing whitespace. 822 */ 823 public float getLineWidth(int line) { 824 float margin = getParagraphLeadingMargin(line); 825 float signedExtent = getLineExtent(line, true); 826 return margin + signedExtent >= 0 ? signedExtent : -signedExtent; 827 } 828 829 /** 830 * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the 831 * tab stops instead of using the ones passed in. 832 * @param line the index of the line 833 * @param full whether to include trailing whitespace 834 * @return the extent of the line 835 */ 836 private float getLineExtent(int line, boolean full) { 837 int start = getLineStart(line); 838 int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 839 840 boolean hasTabsOrEmoji = getLineContainsTab(line); 841 TabStops tabStops = null; 842 if (hasTabsOrEmoji && mText instanceof Spanned) { 843 // Just checking this line should be good enough, tabs should be 844 // consistent across all lines in a paragraph. 845 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 846 if (tabs.length > 0) { 847 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 848 } 849 } 850 Directions directions = getLineDirections(line); 851 int dir = getParagraphDirection(line); 852 853 TextLine tl = TextLine.obtain(); 854 tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); 855 float width = tl.metrics(null); 856 TextLine.recycle(tl); 857 return width; 858 } 859 860 /** 861 * Returns the signed horizontal extent of the specified line, excluding 862 * leading margin. If full is false, excludes trailing whitespace. 863 * @param line the index of the line 864 * @param tabStops the tab stops, can be null if we know they're not used. 865 * @param full whether to include trailing whitespace 866 * @return the extent of the text on this line 867 */ 868 private float getLineExtent(int line, TabStops tabStops, boolean full) { 869 int start = getLineStart(line); 870 int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 871 boolean hasTabsOrEmoji = getLineContainsTab(line); 872 Directions directions = getLineDirections(line); 873 int dir = getParagraphDirection(line); 874 875 TextLine tl = TextLine.obtain(); 876 tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); 877 float width = tl.metrics(null); 878 TextLine.recycle(tl); 879 return width; 880 } 881 882 /** 883 * Get the line number corresponding to the specified vertical position. 884 * If you ask for a position above 0, you get 0; if you ask for a position 885 * below the bottom of the text, you get the last line. 886 */ 887 // FIXME: It may be faster to do a linear search for layouts without many lines. 888 public int getLineForVertical(int vertical) { 889 int high = getLineCount(), low = -1, guess; 890 891 while (high - low > 1) { 892 guess = (high + low) / 2; 893 894 if (getLineTop(guess) > vertical) 895 high = guess; 896 else 897 low = guess; 898 } 899 900 if (low < 0) 901 return 0; 902 else 903 return low; 904 } 905 906 /** 907 * Get the line number on which the specified text offset appears. 908 * If you ask for a position before 0, you get 0; if you ask for a position 909 * beyond the end of the text, you get the last line. 910 */ 911 public int getLineForOffset(int offset) { 912 int high = getLineCount(), low = -1, guess; 913 914 while (high - low > 1) { 915 guess = (high + low) / 2; 916 917 if (getLineStart(guess) > offset) 918 high = guess; 919 else 920 low = guess; 921 } 922 923 if (low < 0) 924 return 0; 925 else 926 return low; 927 } 928 929 /** 930 * Get the character offset on the specified line whose position is 931 * closest to the specified horizontal position. 932 */ 933 public int getOffsetForHorizontal(int line, float horiz) { 934 int max = getLineEnd(line) - 1; 935 int min = getLineStart(line); 936 Directions dirs = getLineDirections(line); 937 938 if (line == getLineCount() - 1) 939 max++; 940 941 int best = min; 942 float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); 943 944 for (int i = 0; i < dirs.mDirections.length; i += 2) { 945 int here = min + dirs.mDirections[i]; 946 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 947 int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1; 948 949 if (there > max) 950 there = max; 951 int high = there - 1 + 1, low = here + 1 - 1, guess; 952 953 while (high - low > 1) { 954 guess = (high + low) / 2; 955 int adguess = getOffsetAtStartOf(guess); 956 957 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap) 958 high = guess; 959 else 960 low = guess; 961 } 962 963 if (low < here + 1) 964 low = here + 1; 965 966 if (low < there) { 967 low = getOffsetAtStartOf(low); 968 969 float dist = Math.abs(getPrimaryHorizontal(low) - horiz); 970 971 int aft = TextUtils.getOffsetAfter(mText, low); 972 if (aft < there) { 973 float other = Math.abs(getPrimaryHorizontal(aft) - horiz); 974 975 if (other < dist) { 976 dist = other; 977 low = aft; 978 } 979 } 980 981 if (dist < bestdist) { 982 bestdist = dist; 983 best = low; 984 } 985 } 986 987 float dist = Math.abs(getPrimaryHorizontal(here) - horiz); 988 989 if (dist < bestdist) { 990 bestdist = dist; 991 best = here; 992 } 993 } 994 995 float dist = Math.abs(getPrimaryHorizontal(max) - horiz); 996 997 if (dist < bestdist) { 998 bestdist = dist; 999 best = max; 1000 } 1001 1002 return best; 1003 } 1004 1005 /** 1006 * Return the text offset after the last character on the specified line. 1007 */ 1008 public final int getLineEnd(int line) { 1009 return getLineStart(line + 1); 1010 } 1011 1012 /** 1013 * Return the text offset after the last visible character (so whitespace 1014 * is not counted) on the specified line. 1015 */ 1016 public int getLineVisibleEnd(int line) { 1017 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 1018 } 1019 1020 private int getLineVisibleEnd(int line, int start, int end) { 1021 CharSequence text = mText; 1022 char ch; 1023 if (line == getLineCount() - 1) { 1024 return end; 1025 } 1026 1027 for (; end > start; end--) { 1028 ch = text.charAt(end - 1); 1029 1030 if (ch == '\n') { 1031 return end - 1; 1032 } 1033 1034 if (ch != ' ' && ch != '\t') { 1035 break; 1036 } 1037 1038 } 1039 1040 return end; 1041 } 1042 1043 /** 1044 * Return the vertical position of the bottom of the specified line. 1045 */ 1046 public final int getLineBottom(int line) { 1047 return getLineTop(line + 1); 1048 } 1049 1050 /** 1051 * Return the vertical position of the baseline of the specified line. 1052 */ 1053 public final int getLineBaseline(int line) { 1054 // getLineTop(line+1) == getLineTop(line) 1055 return getLineTop(line+1) - getLineDescent(line); 1056 } 1057 1058 /** 1059 * Get the ascent of the text on the specified line. 1060 * The return value is negative to match the Paint.ascent() convention. 1061 */ 1062 public final int getLineAscent(int line) { 1063 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) 1064 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); 1065 } 1066 1067 public int getOffsetToLeftOf(int offset) { 1068 return getOffsetToLeftRightOf(offset, true); 1069 } 1070 1071 public int getOffsetToRightOf(int offset) { 1072 return getOffsetToLeftRightOf(offset, false); 1073 } 1074 1075 private int getOffsetToLeftRightOf(int caret, boolean toLeft) { 1076 int line = getLineForOffset(caret); 1077 int lineStart = getLineStart(line); 1078 int lineEnd = getLineEnd(line); 1079 int lineDir = getParagraphDirection(line); 1080 1081 boolean lineChanged = false; 1082 boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); 1083 // if walking off line, look at the line we're headed to 1084 if (advance) { 1085 if (caret == lineEnd) { 1086 if (line < getLineCount() - 1) { 1087 lineChanged = true; 1088 ++line; 1089 } else { 1090 return caret; // at very end, don't move 1091 } 1092 } 1093 } else { 1094 if (caret == lineStart) { 1095 if (line > 0) { 1096 lineChanged = true; 1097 --line; 1098 } else { 1099 return caret; // at very start, don't move 1100 } 1101 } 1102 } 1103 1104 if (lineChanged) { 1105 lineStart = getLineStart(line); 1106 lineEnd = getLineEnd(line); 1107 int newDir = getParagraphDirection(line); 1108 if (newDir != lineDir) { 1109 // unusual case. we want to walk onto the line, but it runs 1110 // in a different direction than this one, so we fake movement 1111 // in the opposite direction. 1112 toLeft = !toLeft; 1113 lineDir = newDir; 1114 } 1115 } 1116 1117 Directions directions = getLineDirections(line); 1118 1119 TextLine tl = TextLine.obtain(); 1120 // XXX: we don't care about tabs 1121 tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null); 1122 caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); 1123 tl = TextLine.recycle(tl); 1124 return caret; 1125 } 1126 1127 private int getOffsetAtStartOf(int offset) { 1128 // XXX this probably should skip local reorderings and 1129 // zero-width characters, look at callers 1130 if (offset == 0) 1131 return 0; 1132 1133 CharSequence text = mText; 1134 char c = text.charAt(offset); 1135 1136 if (c >= '\uDC00' && c <= '\uDFFF') { 1137 char c1 = text.charAt(offset - 1); 1138 1139 if (c1 >= '\uD800' && c1 <= '\uDBFF') 1140 offset -= 1; 1141 } 1142 1143 if (mSpannedText) { 1144 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 1145 ReplacementSpan.class); 1146 1147 for (int i = 0; i < spans.length; i++) { 1148 int start = ((Spanned) text).getSpanStart(spans[i]); 1149 int end = ((Spanned) text).getSpanEnd(spans[i]); 1150 1151 if (start < offset && end > offset) 1152 offset = start; 1153 } 1154 } 1155 1156 return offset; 1157 } 1158 1159 /** 1160 * Fills in the specified Path with a representation of a cursor 1161 * at the specified offset. This will often be a vertical line 1162 * but can be multiple discontinuous lines in text with multiple 1163 * directionalities. 1164 */ 1165 public void getCursorPath(int point, Path dest, 1166 CharSequence editingBuffer) { 1167 dest.reset(); 1168 1169 int line = getLineForOffset(point); 1170 int top = getLineTop(line); 1171 int bottom = getLineTop(line+1); 1172 1173 float h1 = getPrimaryHorizontal(point) - 0.5f; 1174 float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point) - 0.5f : h1; 1175 1176 int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | 1177 TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); 1178 int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON); 1179 int dist = 0; 1180 1181 if (caps != 0 || fn != 0) { 1182 dist = (bottom - top) >> 2; 1183 1184 if (fn != 0) 1185 top += dist; 1186 if (caps != 0) 1187 bottom -= dist; 1188 } 1189 1190 if (h1 < 0.5f) 1191 h1 = 0.5f; 1192 if (h2 < 0.5f) 1193 h2 = 0.5f; 1194 1195 if (Float.compare(h1, h2) == 0) { 1196 dest.moveTo(h1, top); 1197 dest.lineTo(h1, bottom); 1198 } else { 1199 dest.moveTo(h1, top); 1200 dest.lineTo(h1, (top + bottom) >> 1); 1201 1202 dest.moveTo(h2, (top + bottom) >> 1); 1203 dest.lineTo(h2, bottom); 1204 } 1205 1206 if (caps == 2) { 1207 dest.moveTo(h2, bottom); 1208 dest.lineTo(h2 - dist, bottom + dist); 1209 dest.lineTo(h2, bottom); 1210 dest.lineTo(h2 + dist, bottom + dist); 1211 } else if (caps == 1) { 1212 dest.moveTo(h2, bottom); 1213 dest.lineTo(h2 - dist, bottom + dist); 1214 1215 dest.moveTo(h2 - dist, bottom + dist - 0.5f); 1216 dest.lineTo(h2 + dist, bottom + dist - 0.5f); 1217 1218 dest.moveTo(h2 + dist, bottom + dist); 1219 dest.lineTo(h2, bottom); 1220 } 1221 1222 if (fn == 2) { 1223 dest.moveTo(h1, top); 1224 dest.lineTo(h1 - dist, top - dist); 1225 dest.lineTo(h1, top); 1226 dest.lineTo(h1 + dist, top - dist); 1227 } else if (fn == 1) { 1228 dest.moveTo(h1, top); 1229 dest.lineTo(h1 - dist, top - dist); 1230 1231 dest.moveTo(h1 - dist, top - dist + 0.5f); 1232 dest.lineTo(h1 + dist, top - dist + 0.5f); 1233 1234 dest.moveTo(h1 + dist, top - dist); 1235 dest.lineTo(h1, top); 1236 } 1237 } 1238 1239 private void addSelection(int line, int start, int end, 1240 int top, int bottom, Path dest) { 1241 int linestart = getLineStart(line); 1242 int lineend = getLineEnd(line); 1243 Directions dirs = getLineDirections(line); 1244 1245 if (lineend > linestart && mText.charAt(lineend - 1) == '\n') 1246 lineend--; 1247 1248 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1249 int here = linestart + dirs.mDirections[i]; 1250 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1251 1252 if (there > lineend) 1253 there = lineend; 1254 1255 if (start <= there && end >= here) { 1256 int st = Math.max(start, here); 1257 int en = Math.min(end, there); 1258 1259 if (st != en) { 1260 float h1 = getHorizontal(st, false, line); 1261 float h2 = getHorizontal(en, true, line); 1262 1263 dest.addRect(h1, top, h2, bottom, Path.Direction.CW); 1264 } 1265 } 1266 } 1267 } 1268 1269 /** 1270 * Fills in the specified Path with a representation of a highlight 1271 * between the specified offsets. This will often be a rectangle 1272 * or a potentially discontinuous set of rectangles. If the start 1273 * and end are the same, the returned path is empty. 1274 */ 1275 public void getSelectionPath(int start, int end, Path dest) { 1276 dest.reset(); 1277 1278 if (start == end) 1279 return; 1280 1281 if (end < start) { 1282 int temp = end; 1283 end = start; 1284 start = temp; 1285 } 1286 1287 int startline = getLineForOffset(start); 1288 int endline = getLineForOffset(end); 1289 1290 int top = getLineTop(startline); 1291 int bottom = getLineBottom(endline); 1292 1293 if (startline == endline) { 1294 addSelection(startline, start, end, top, bottom, dest); 1295 } else { 1296 final float width = mWidth; 1297 1298 addSelection(startline, start, getLineEnd(startline), 1299 top, getLineBottom(startline), dest); 1300 1301 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) 1302 dest.addRect(getLineLeft(startline), top, 1303 0, getLineBottom(startline), Path.Direction.CW); 1304 else 1305 dest.addRect(getLineRight(startline), top, 1306 width, getLineBottom(startline), Path.Direction.CW); 1307 1308 for (int i = startline + 1; i < endline; i++) { 1309 top = getLineTop(i); 1310 bottom = getLineBottom(i); 1311 dest.addRect(0, top, width, bottom, Path.Direction.CW); 1312 } 1313 1314 top = getLineTop(endline); 1315 bottom = getLineBottom(endline); 1316 1317 addSelection(endline, getLineStart(endline), end, 1318 top, bottom, dest); 1319 1320 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) 1321 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW); 1322 else 1323 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW); 1324 } 1325 } 1326 1327 /** 1328 * Get the alignment of the specified paragraph, taking into account 1329 * markup attached to it. 1330 */ 1331 public final Alignment getParagraphAlignment(int line) { 1332 Alignment align = mAlignment; 1333 1334 if (mSpannedText) { 1335 Spanned sp = (Spanned) mText; 1336 AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line), 1337 getLineEnd(line), 1338 AlignmentSpan.class); 1339 1340 int spanLength = spans.length; 1341 if (spanLength > 0) { 1342 align = spans[spanLength-1].getAlignment(); 1343 } 1344 } 1345 1346 return align; 1347 } 1348 1349 /** 1350 * Get the left edge of the specified paragraph, inset by left margins. 1351 */ 1352 public final int getParagraphLeft(int line) { 1353 int left = 0; 1354 int dir = getParagraphDirection(line); 1355 if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) { 1356 return left; // leading margin has no impact, or no styles 1357 } 1358 return getParagraphLeadingMargin(line); 1359 } 1360 1361 /** 1362 * Get the right edge of the specified paragraph, inset by right margins. 1363 */ 1364 public final int getParagraphRight(int line) { 1365 int right = mWidth; 1366 int dir = getParagraphDirection(line); 1367 if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) { 1368 return right; // leading margin has no impact, or no styles 1369 } 1370 return right - getParagraphLeadingMargin(line); 1371 } 1372 1373 /** 1374 * Returns the effective leading margin (unsigned) for this line, 1375 * taking into account LeadingMarginSpan and LeadingMarginSpan2. 1376 * @param line the line index 1377 * @return the leading margin of this line 1378 */ 1379 private int getParagraphLeadingMargin(int line) { 1380 if (!mSpannedText) { 1381 return 0; 1382 } 1383 Spanned spanned = (Spanned) mText; 1384 1385 int lineStart = getLineStart(line); 1386 int lineEnd = getLineEnd(line); 1387 int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd, 1388 LeadingMarginSpan.class); 1389 LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd, 1390 LeadingMarginSpan.class); 1391 if (spans.length == 0) { 1392 return 0; // no leading margin span; 1393 } 1394 1395 int margin = 0; 1396 1397 boolean isFirstParaLine = lineStart == 0 || 1398 spanned.charAt(lineStart - 1) == '\n'; 1399 1400 for (int i = 0; i < spans.length; i++) { 1401 LeadingMarginSpan span = spans[i]; 1402 boolean useFirstLineMargin = isFirstParaLine; 1403 if (span instanceof LeadingMarginSpan2) { 1404 int spStart = spanned.getSpanStart(span); 1405 int spanLine = getLineForOffset(spStart); 1406 int count = ((LeadingMarginSpan2)span).getLeadingMarginLineCount(); 1407 useFirstLineMargin = line < spanLine + count; 1408 } 1409 margin += span.getLeadingMargin(useFirstLineMargin); 1410 } 1411 1412 return margin; 1413 } 1414 1415 /* package */ 1416 static float measurePara(TextPaint paint, TextPaint workPaint, 1417 CharSequence text, int start, int end) { 1418 1419 MeasuredText mt = MeasuredText.obtain(); 1420 TextLine tl = TextLine.obtain(); 1421 try { 1422 mt.setPara(text, start, end, DIR_REQUEST_LTR); 1423 Directions directions; 1424 int dir; 1425 if (mt.mEasy) { 1426 directions = DIRS_ALL_LEFT_TO_RIGHT; 1427 dir = Layout.DIR_LEFT_TO_RIGHT; 1428 } else { 1429 directions = AndroidBidi.directions(mt.mDir, mt.mLevels, 1430 0, mt.mChars, 0, mt.mLen); 1431 dir = mt.mDir; 1432 } 1433 char[] chars = mt.mChars; 1434 int len = mt.mLen; 1435 boolean hasTabs = false; 1436 TabStops tabStops = null; 1437 for (int i = 0; i < len; ++i) { 1438 if (chars[i] == '\t') { 1439 hasTabs = true; 1440 if (text instanceof Spanned) { 1441 Spanned spanned = (Spanned) text; 1442 int spanEnd = spanned.nextSpanTransition(start, end, 1443 TabStopSpan.class); 1444 TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd, 1445 TabStopSpan.class); 1446 if (spans.length > 0) { 1447 tabStops = new TabStops(TAB_INCREMENT, spans); 1448 } 1449 } 1450 break; 1451 } 1452 } 1453 tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops); 1454 return tl.metrics(null); 1455 } finally { 1456 TextLine.recycle(tl); 1457 MeasuredText.recycle(mt); 1458 } 1459 } 1460 1461 /** 1462 * @hide 1463 */ 1464 /* package */ static class TabStops { 1465 private int[] mStops; 1466 private int mNumStops; 1467 private int mIncrement; 1468 1469 TabStops(int increment, Object[] spans) { 1470 reset(increment, spans); 1471 } 1472 1473 void reset(int increment, Object[] spans) { 1474 this.mIncrement = increment; 1475 1476 int ns = 0; 1477 if (spans != null) { 1478 int[] stops = this.mStops; 1479 for (Object o : spans) { 1480 if (o instanceof TabStopSpan) { 1481 if (stops == null) { 1482 stops = new int[10]; 1483 } else if (ns == stops.length) { 1484 int[] nstops = new int[ns * 2]; 1485 for (int i = 0; i < ns; ++i) { 1486 nstops[i] = stops[i]; 1487 } 1488 stops = nstops; 1489 } 1490 stops[ns++] = ((TabStopSpan) o).getTabStop(); 1491 } 1492 } 1493 if (ns > 1) { 1494 Arrays.sort(stops, 0, ns); 1495 } 1496 if (stops != this.mStops) { 1497 this.mStops = stops; 1498 } 1499 } 1500 this.mNumStops = ns; 1501 } 1502 1503 float nextTab(float h) { 1504 int ns = this.mNumStops; 1505 if (ns > 0) { 1506 int[] stops = this.mStops; 1507 for (int i = 0; i < ns; ++i) { 1508 int stop = stops[i]; 1509 if (stop > h) { 1510 return stop; 1511 } 1512 } 1513 } 1514 return nextDefaultStop(h, mIncrement); 1515 } 1516 1517 public static float nextDefaultStop(float h, int inc) { 1518 return ((int) ((h + inc) / inc)) * inc; 1519 } 1520 } 1521 1522 /** 1523 * Returns the position of the next tab stop after h on the line. 1524 * 1525 * @param text the text 1526 * @param start start of the line 1527 * @param end limit of the line 1528 * @param h the current horizontal offset 1529 * @param tabs the tabs, can be null. If it is null, any tabs in effect 1530 * on the line will be used. If there are no tabs, a default offset 1531 * will be used to compute the tab stop. 1532 * @return the offset of the next tab stop. 1533 */ 1534 /* package */ static float nextTab(CharSequence text, int start, int end, 1535 float h, Object[] tabs) { 1536 float nh = Float.MAX_VALUE; 1537 boolean alltabs = false; 1538 1539 if (text instanceof Spanned) { 1540 if (tabs == null) { 1541 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class); 1542 alltabs = true; 1543 } 1544 1545 for (int i = 0; i < tabs.length; i++) { 1546 if (!alltabs) { 1547 if (!(tabs[i] instanceof TabStopSpan)) 1548 continue; 1549 } 1550 1551 int where = ((TabStopSpan) tabs[i]).getTabStop(); 1552 1553 if (where < nh && where > h) 1554 nh = where; 1555 } 1556 1557 if (nh != Float.MAX_VALUE) 1558 return nh; 1559 } 1560 1561 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; 1562 } 1563 1564 protected final boolean isSpanned() { 1565 return mSpannedText; 1566 } 1567 1568 /** 1569 * Returns the same as <code>text.getSpans()</code>, except where 1570 * <code>start</code> and <code>end</code> are the same and are not 1571 * at the very beginning of the text, in which case an empty array 1572 * is returned instead. 1573 * <p> 1574 * This is needed because of the special case that <code>getSpans()</code> 1575 * on an empty range returns the spans adjacent to that range, which is 1576 * primarily for the sake of <code>TextWatchers</code> so they will get 1577 * notifications when text goes from empty to non-empty. But it also 1578 * has the unfortunate side effect that if the text ends with an empty 1579 * paragraph, that paragraph accidentally picks up the styles of the 1580 * preceding paragraph (even though those styles will not be picked up 1581 * by new text that is inserted into the empty paragraph). 1582 * <p> 1583 * The reason it just checks whether <code>start</code> and <code>end</code> 1584 * is the same is that the only time a line can contain 0 characters 1585 * is if it is the final paragraph of the Layout; otherwise any line will 1586 * contain at least one printing or newline character. The reason for the 1587 * additional check if <code>start</code> is greater than 0 is that 1588 * if the empty paragraph is the entire content of the buffer, paragraph 1589 * styles that are already applied to the buffer will apply to text that 1590 * is inserted into it. 1591 */ 1592 /* package */ static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) { 1593 if (start == end && start > 0) { 1594 return (T[]) ArrayUtils.emptyArray(type); 1595 } 1596 1597 return text.getSpans(start, end, type); 1598 } 1599 1600 private void ellipsize(int start, int end, int line, 1601 char[] dest, int destoff) { 1602 int ellipsisCount = getEllipsisCount(line); 1603 1604 if (ellipsisCount == 0) { 1605 return; 1606 } 1607 1608 int ellipsisStart = getEllipsisStart(line); 1609 int linestart = getLineStart(line); 1610 1611 for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { 1612 char c; 1613 1614 if (i == ellipsisStart) { 1615 c = '\u2026'; // ellipsis 1616 } else { 1617 c = '\uFEFF'; // 0-width space 1618 } 1619 1620 int a = i + linestart; 1621 1622 if (a >= start && a < end) { 1623 dest[destoff + a - start] = c; 1624 } 1625 } 1626 } 1627 1628 /** 1629 * Stores information about bidirectional (left-to-right or right-to-left) 1630 * text within the layout of a line. 1631 */ 1632 public static class Directions { 1633 // Directions represents directional runs within a line of text. 1634 // Runs are pairs of ints listed in visual order, starting from the 1635 // leading margin. The first int of each pair is the offset from 1636 // the first character of the line to the start of the run. The 1637 // second int represents both the length and level of the run. 1638 // The length is in the lower bits, accessed by masking with 1639 // DIR_LENGTH_MASK. The level is in the higher bits, accessed 1640 // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. 1641 // To simply test for an RTL direction, test the bit using 1642 // DIR_RTL_FLAG, if set then the direction is rtl. 1643 1644 /* package */ int[] mDirections; 1645 /* package */ Directions(int[] dirs) { 1646 mDirections = dirs; 1647 } 1648 } 1649 1650 /** 1651 * Return the offset of the first character to be ellipsized away, 1652 * relative to the start of the line. (So 0 if the beginning of the 1653 * line is ellipsized, not getLineStart().) 1654 */ 1655 public abstract int getEllipsisStart(int line); 1656 1657 /** 1658 * Returns the number of characters to be ellipsized away, or 0 if 1659 * no ellipsis is to take place. 1660 */ 1661 public abstract int getEllipsisCount(int line); 1662 1663 /* package */ static class Ellipsizer implements CharSequence, GetChars { 1664 /* package */ CharSequence mText; 1665 /* package */ Layout mLayout; 1666 /* package */ int mWidth; 1667 /* package */ TextUtils.TruncateAt mMethod; 1668 1669 public Ellipsizer(CharSequence s) { 1670 mText = s; 1671 } 1672 1673 public char charAt(int off) { 1674 char[] buf = TextUtils.obtain(1); 1675 getChars(off, off + 1, buf, 0); 1676 char ret = buf[0]; 1677 1678 TextUtils.recycle(buf); 1679 return ret; 1680 } 1681 1682 public void getChars(int start, int end, char[] dest, int destoff) { 1683 int line1 = mLayout.getLineForOffset(start); 1684 int line2 = mLayout.getLineForOffset(end); 1685 1686 TextUtils.getChars(mText, start, end, dest, destoff); 1687 1688 for (int i = line1; i <= line2; i++) { 1689 mLayout.ellipsize(start, end, i, dest, destoff); 1690 } 1691 } 1692 1693 public int length() { 1694 return mText.length(); 1695 } 1696 1697 public CharSequence subSequence(int start, int end) { 1698 char[] s = new char[end - start]; 1699 getChars(start, end, s, 0); 1700 return new String(s); 1701 } 1702 1703 @Override 1704 public String toString() { 1705 char[] s = new char[length()]; 1706 getChars(0, length(), s, 0); 1707 return new String(s); 1708 } 1709 1710 } 1711 1712 /* package */ static class SpannedEllipsizer 1713 extends Ellipsizer implements Spanned { 1714 private Spanned mSpanned; 1715 1716 public SpannedEllipsizer(CharSequence display) { 1717 super(display); 1718 mSpanned = (Spanned) display; 1719 } 1720 1721 public <T> T[] getSpans(int start, int end, Class<T> type) { 1722 return mSpanned.getSpans(start, end, type); 1723 } 1724 1725 public int getSpanStart(Object tag) { 1726 return mSpanned.getSpanStart(tag); 1727 } 1728 1729 public int getSpanEnd(Object tag) { 1730 return mSpanned.getSpanEnd(tag); 1731 } 1732 1733 public int getSpanFlags(Object tag) { 1734 return mSpanned.getSpanFlags(tag); 1735 } 1736 1737 public int nextSpanTransition(int start, int limit, Class type) { 1738 return mSpanned.nextSpanTransition(start, limit, type); 1739 } 1740 1741 @Override 1742 public CharSequence subSequence(int start, int end) { 1743 char[] s = new char[end - start]; 1744 getChars(start, end, s, 0); 1745 1746 SpannableString ss = new SpannableString(new String(s)); 1747 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); 1748 return ss; 1749 } 1750 } 1751 1752 /** 1753 * Inform this layout that not all of its lines will be displayed, because a maximum number of 1754 * lines has been set on the associated TextView. 1755 * 1756 * A non strictly positive value means that all lines are displayed. 1757 * 1758 * @param lineCount number of visible lines 1759 * @hide 1760 */ 1761 public void setMaximumVisibleLineCount(int lineCount) {} 1762 1763 private CharSequence mText; 1764 private TextPaint mPaint; 1765 /* package */ TextPaint mWorkPaint; 1766 private int mWidth; 1767 private Alignment mAlignment = Alignment.ALIGN_NORMAL; 1768 private float mSpacingMult; 1769 private float mSpacingAdd; 1770 private static final Rect sTempRect = new Rect(); 1771 private boolean mSpannedText; 1772 1773 public static final int DIR_LEFT_TO_RIGHT = 1; 1774 public static final int DIR_RIGHT_TO_LEFT = -1; 1775 1776 /* package */ static final int DIR_REQUEST_LTR = 1; 1777 /* package */ static final int DIR_REQUEST_RTL = -1; 1778 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; 1779 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; 1780 1781 /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; 1782 /* package */ static final int RUN_LEVEL_SHIFT = 26; 1783 /* package */ static final int RUN_LEVEL_MASK = 0x3f; 1784 /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; 1785 1786 public enum Alignment { 1787 ALIGN_NORMAL, 1788 ALIGN_OPPOSITE, 1789 ALIGN_CENTER, 1790 /** @hide */ 1791 ALIGN_LEFT, 1792 /** @hide */ 1793 ALIGN_RIGHT, 1794 } 1795 1796 private static final int TAB_INCREMENT = 20; 1797 1798 /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = 1799 new Directions(new int[] { 0, RUN_LENGTH_MASK }); 1800 /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = 1801 new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); 1802 1803} 1804