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