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