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