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