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