Layout.java revision 2fb503f5102dd32a8ec391b26911528852703b90
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 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 if (line != getLineCount() - 1) 753 max = TextUtils.getOffsetBefore(mText, getLineEnd(line)); 754 755 int best = min; 756 float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); 757 758 int here = min; 759 for (int i = 0; i < dirs.mDirections.length; i++) { 760 int there = here + dirs.mDirections[i]; 761 int swap = ((i & 1) == 0) ? 1 : -1; 762 763 if (there > max) 764 there = max; 765 766 int high = there - 1 + 1, low = here + 1 - 1, guess; 767 768 while (high - low > 1) { 769 guess = (high + low) / 2; 770 int adguess = getOffsetAtStartOf(guess); 771 772 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap) 773 high = guess; 774 else 775 low = guess; 776 } 777 778 if (low < here + 1) 779 low = here + 1; 780 781 if (low < there) { 782 low = getOffsetAtStartOf(low); 783 784 float dist = Math.abs(getPrimaryHorizontal(low) - horiz); 785 786 int aft = TextUtils.getOffsetAfter(mText, low); 787 if (aft < there) { 788 float other = Math.abs(getPrimaryHorizontal(aft) - horiz); 789 790 if (other < dist) { 791 dist = other; 792 low = aft; 793 } 794 } 795 796 if (dist < bestdist) { 797 bestdist = dist; 798 best = low; 799 } 800 } 801 802 float dist = Math.abs(getPrimaryHorizontal(here) - horiz); 803 804 if (dist < bestdist) { 805 bestdist = dist; 806 best = here; 807 } 808 809 here = there; 810 } 811 812 float dist = Math.abs(getPrimaryHorizontal(max) - horiz); 813 814 if (dist < bestdist) { 815 bestdist = dist; 816 best = max; 817 } 818 819 return best; 820 } 821 822 /** 823 * Return the text offset after the last character on the specified line. 824 */ 825 public final int getLineEnd(int line) { 826 return getLineStart(line + 1); 827 } 828 829 /** 830 * Return the text offset after the last visible character (so whitespace 831 * is not counted) on the specified line. 832 */ 833 public int getLineVisibleEnd(int line) { 834 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 835 } 836 837 private int getLineVisibleEnd(int line, int start, int end) { 838 if (DEBUG) { 839 Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end); 840 } 841 842 CharSequence text = mText; 843 char ch; 844 if (line == getLineCount() - 1) { 845 return end; 846 } 847 848 for (; end > start; end--) { 849 ch = text.charAt(end - 1); 850 851 if (ch == '\n') { 852 return end - 1; 853 } 854 855 if (ch != ' ' && ch != '\t') { 856 break; 857 } 858 859 } 860 861 return end; 862 } 863 864 /** 865 * Return the vertical position of the bottom of the specified line. 866 */ 867 public final int getLineBottom(int line) { 868 return getLineTop(line + 1); 869 } 870 871 /** 872 * Return the vertical position of the baseline of the specified line. 873 */ 874 public final int getLineBaseline(int line) { 875 // getLineTop(line+1) == getLineTop(line) 876 return getLineTop(line+1) - getLineDescent(line); 877 } 878 879 /** 880 * Get the ascent of the text on the specified line. 881 * The return value is negative to match the Paint.ascent() convention. 882 */ 883 public final int getLineAscent(int line) { 884 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) 885 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); 886 } 887 888 /** 889 * Return the text offset that would be reached by moving left 890 * (possibly onto another line) from the specified offset. 891 */ 892 public int getOffsetToLeftOf(int offset) { 893 int line = getLineForOffset(offset); 894 int start = getLineStart(line); 895 int end = getLineEnd(line); 896 Directions dirs = getLineDirections(line); 897 898 if (line != getLineCount() - 1) 899 end = TextUtils.getOffsetBefore(mText, end); 900 901 float horiz = getPrimaryHorizontal(offset); 902 903 int best = offset; 904 float besth = Integer.MIN_VALUE; 905 int candidate; 906 907 candidate = TextUtils.getOffsetBefore(mText, offset); 908 if (candidate >= start && candidate <= end) { 909 float h = getPrimaryHorizontal(candidate); 910 911 if (h < horiz && h > besth) { 912 best = candidate; 913 besth = h; 914 } 915 } 916 917 candidate = TextUtils.getOffsetAfter(mText, offset); 918 if (candidate >= start && candidate <= end) { 919 float h = getPrimaryHorizontal(candidate); 920 921 if (h < horiz && h > besth) { 922 best = candidate; 923 besth = h; 924 } 925 } 926 927 int here = start; 928 for (int i = 0; i < dirs.mDirections.length; i++) { 929 int there = here + dirs.mDirections[i]; 930 if (there > end) 931 there = end; 932 933 float h = getPrimaryHorizontal(here); 934 935 if (h < horiz && h > besth) { 936 best = here; 937 besth = h; 938 } 939 940 candidate = TextUtils.getOffsetAfter(mText, here); 941 if (candidate >= start && candidate <= end) { 942 h = getPrimaryHorizontal(candidate); 943 944 if (h < horiz && h > besth) { 945 best = candidate; 946 besth = h; 947 } 948 } 949 950 candidate = TextUtils.getOffsetBefore(mText, there); 951 if (candidate >= start && candidate <= end) { 952 h = getPrimaryHorizontal(candidate); 953 954 if (h < horiz && h > besth) { 955 best = candidate; 956 besth = h; 957 } 958 } 959 960 here = there; 961 } 962 963 float h = getPrimaryHorizontal(end); 964 965 if (h < horiz && h > besth) { 966 best = end; 967 besth = h; 968 } 969 970 if (best != offset) 971 return best; 972 973 int dir = getParagraphDirection(line); 974 975 if (dir > 0) { 976 if (line == 0) 977 return best; 978 else 979 return getOffsetForHorizontal(line - 1, 10000); 980 } else { 981 if (line == getLineCount() - 1) 982 return best; 983 else 984 return getOffsetForHorizontal(line + 1, 10000); 985 } 986 } 987 988 /** 989 * Return the text offset that would be reached by moving right 990 * (possibly onto another line) from the specified offset. 991 */ 992 public int getOffsetToRightOf(int offset) { 993 int line = getLineForOffset(offset); 994 int start = getLineStart(line); 995 int end = getLineEnd(line); 996 Directions dirs = getLineDirections(line); 997 998 if (line != getLineCount() - 1) 999 end = TextUtils.getOffsetBefore(mText, end); 1000 1001 float horiz = getPrimaryHorizontal(offset); 1002 1003 int best = offset; 1004 float besth = Integer.MAX_VALUE; 1005 int candidate; 1006 1007 candidate = TextUtils.getOffsetBefore(mText, offset); 1008 if (candidate >= start && candidate <= end) { 1009 float h = getPrimaryHorizontal(candidate); 1010 1011 if (h > horiz && h < besth) { 1012 best = candidate; 1013 besth = h; 1014 } 1015 } 1016 1017 candidate = TextUtils.getOffsetAfter(mText, offset); 1018 if (candidate >= start && candidate <= end) { 1019 float h = getPrimaryHorizontal(candidate); 1020 1021 if (h > horiz && h < besth) { 1022 best = candidate; 1023 besth = h; 1024 } 1025 } 1026 1027 int here = start; 1028 for (int i = 0; i < dirs.mDirections.length; i++) { 1029 int there = here + dirs.mDirections[i]; 1030 if (there > end) 1031 there = end; 1032 1033 float h = getPrimaryHorizontal(here); 1034 1035 if (h > horiz && h < besth) { 1036 best = here; 1037 besth = h; 1038 } 1039 1040 candidate = TextUtils.getOffsetAfter(mText, here); 1041 if (candidate >= start && candidate <= end) { 1042 h = getPrimaryHorizontal(candidate); 1043 1044 if (h > horiz && h < besth) { 1045 best = candidate; 1046 besth = h; 1047 } 1048 } 1049 1050 candidate = TextUtils.getOffsetBefore(mText, there); 1051 if (candidate >= start && candidate <= end) { 1052 h = getPrimaryHorizontal(candidate); 1053 1054 if (h > horiz && h < besth) { 1055 best = candidate; 1056 besth = h; 1057 } 1058 } 1059 1060 here = there; 1061 } 1062 1063 float h = getPrimaryHorizontal(end); 1064 1065 if (h > horiz && h < besth) { 1066 best = end; 1067 besth = h; 1068 } 1069 1070 if (best != offset) 1071 return best; 1072 1073 int dir = getParagraphDirection(line); 1074 1075 if (dir > 0) { 1076 if (line == getLineCount() - 1) 1077 return best; 1078 else 1079 return getOffsetForHorizontal(line + 1, -10000); 1080 } else { 1081 if (line == 0) 1082 return best; 1083 else 1084 return getOffsetForHorizontal(line - 1, -10000); 1085 } 1086 } 1087 1088 private int getOffsetAtStartOf(int offset) { 1089 if (offset == 0) 1090 return 0; 1091 1092 CharSequence text = mText; 1093 char c = text.charAt(offset); 1094 1095 if (c >= '\uDC00' && c <= '\uDFFF') { 1096 char c1 = text.charAt(offset - 1); 1097 1098 if (c1 >= '\uD800' && c1 <= '\uDBFF') 1099 offset -= 1; 1100 } 1101 1102 if (mSpannedText) { 1103 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 1104 ReplacementSpan.class); 1105 1106 for (int i = 0; i < spans.length; i++) { 1107 int start = ((Spanned) text).getSpanStart(spans[i]); 1108 int end = ((Spanned) text).getSpanEnd(spans[i]); 1109 1110 if (start < offset && end > offset) 1111 offset = start; 1112 } 1113 } 1114 1115 return offset; 1116 } 1117 1118 /** 1119 * Fills in the specified Path with a representation of a cursor 1120 * at the specified offset. This will often be a vertical line 1121 * but can be multiple discontinous lines in text with multiple 1122 * directionalities. 1123 */ 1124 public void getCursorPath(int point, Path dest, 1125 CharSequence editingBuffer) { 1126 dest.reset(); 1127 1128 int line = getLineForOffset(point); 1129 int top = getLineTop(line); 1130 int bottom = getLineTop(line+1); 1131 1132 float h1 = getPrimaryHorizontal(point) - 0.5f; 1133 float h2 = getSecondaryHorizontal(point) - 0.5f; 1134 1135 int caps = TextKeyListener.getMetaState(editingBuffer, 1136 KeyEvent.META_SHIFT_ON) | 1137 TextKeyListener.getMetaState(editingBuffer, 1138 TextKeyListener.META_SELECTING); 1139 int fn = TextKeyListener.getMetaState(editingBuffer, 1140 KeyEvent.META_ALT_ON); 1141 int dist = 0; 1142 1143 if (caps != 0 || fn != 0) { 1144 dist = (bottom - top) >> 2; 1145 1146 if (fn != 0) 1147 top += dist; 1148 if (caps != 0) 1149 bottom -= dist; 1150 } 1151 1152 if (h1 < 0.5f) 1153 h1 = 0.5f; 1154 if (h2 < 0.5f) 1155 h2 = 0.5f; 1156 1157 if (Float.compare(h1, h2) == 0) { 1158 dest.moveTo(h1, top); 1159 dest.lineTo(h1, bottom); 1160 } else { 1161 dest.moveTo(h1, top); 1162 dest.lineTo(h1, (top + bottom) >> 1); 1163 1164 dest.moveTo(h2, (top + bottom) >> 1); 1165 dest.lineTo(h2, bottom); 1166 } 1167 1168 if (caps == 2) { 1169 dest.moveTo(h2, bottom); 1170 dest.lineTo(h2 - dist, bottom + dist); 1171 dest.lineTo(h2, bottom); 1172 dest.lineTo(h2 + dist, bottom + dist); 1173 } else if (caps == 1) { 1174 dest.moveTo(h2, bottom); 1175 dest.lineTo(h2 - dist, bottom + dist); 1176 1177 dest.moveTo(h2 - dist, bottom + dist - 0.5f); 1178 dest.lineTo(h2 + dist, bottom + dist - 0.5f); 1179 1180 dest.moveTo(h2 + dist, bottom + dist); 1181 dest.lineTo(h2, bottom); 1182 } 1183 1184 if (fn == 2) { 1185 dest.moveTo(h1, top); 1186 dest.lineTo(h1 - dist, top - dist); 1187 dest.lineTo(h1, top); 1188 dest.lineTo(h1 + dist, top - dist); 1189 } else if (fn == 1) { 1190 dest.moveTo(h1, top); 1191 dest.lineTo(h1 - dist, top - dist); 1192 1193 dest.moveTo(h1 - dist, top - dist + 0.5f); 1194 dest.lineTo(h1 + dist, top - dist + 0.5f); 1195 1196 dest.moveTo(h1 + dist, top - dist); 1197 dest.lineTo(h1, top); 1198 } 1199 } 1200 1201 private void addSelection(int line, int start, int end, 1202 int top, int bottom, Path dest) { 1203 int linestart = getLineStart(line); 1204 int lineend = getLineEnd(line); 1205 Directions dirs = getLineDirections(line); 1206 1207 if (lineend > linestart && mText.charAt(lineend - 1) == '\n') 1208 lineend--; 1209 1210 int here = linestart; 1211 for (int i = 0; i < dirs.mDirections.length; i++) { 1212 int there = here + dirs.mDirections[i]; 1213 if (there > lineend) 1214 there = lineend; 1215 1216 if (start <= there && end >= here) { 1217 int st = Math.max(start, here); 1218 int en = Math.min(end, there); 1219 1220 if (st != en) { 1221 float h1 = getHorizontal(st, false, false, line); 1222 float h2 = getHorizontal(en, true, false, line); 1223 1224 dest.addRect(h1, top, h2, bottom, Path.Direction.CW); 1225 } 1226 } 1227 1228 here = there; 1229 } 1230 } 1231 1232 /** 1233 * Fills in the specified Path with a representation of a highlight 1234 * between the specified offsets. This will often be a rectangle 1235 * or a potentially discontinuous set of rectangles. If the start 1236 * and end are the same, the returned path is empty. 1237 */ 1238 public void getSelectionPath(int start, int end, Path dest) { 1239 dest.reset(); 1240 1241 if (start == end) 1242 return; 1243 1244 if (end < start) { 1245 int temp = end; 1246 end = start; 1247 start = temp; 1248 } 1249 1250 int startline = getLineForOffset(start); 1251 int endline = getLineForOffset(end); 1252 1253 int top = getLineTop(startline); 1254 int bottom = getLineBottom(endline); 1255 1256 if (startline == endline) { 1257 addSelection(startline, start, end, top, bottom, dest); 1258 } else { 1259 final float width = mWidth; 1260 1261 addSelection(startline, start, getLineEnd(startline), 1262 top, getLineBottom(startline), dest); 1263 1264 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) 1265 dest.addRect(getLineLeft(startline), top, 1266 0, getLineBottom(startline), Path.Direction.CW); 1267 else 1268 dest.addRect(getLineRight(startline), top, 1269 width, getLineBottom(startline), Path.Direction.CW); 1270 1271 for (int i = startline + 1; i < endline; i++) { 1272 top = getLineTop(i); 1273 bottom = getLineBottom(i); 1274 dest.addRect(0, top, width, bottom, Path.Direction.CW); 1275 } 1276 1277 top = getLineTop(endline); 1278 bottom = getLineBottom(endline); 1279 1280 addSelection(endline, getLineStart(endline), end, 1281 top, bottom, dest); 1282 1283 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) 1284 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW); 1285 else 1286 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW); 1287 } 1288 } 1289 1290 /** 1291 * Get the alignment of the specified paragraph, taking into account 1292 * markup attached to it. 1293 */ 1294 public final Alignment getParagraphAlignment(int line) { 1295 Alignment align = mAlignment; 1296 1297 if (mSpannedText) { 1298 Spanned sp = (Spanned) mText; 1299 AlignmentSpan[] spans = sp.getSpans(getLineStart(line), 1300 getLineEnd(line), 1301 AlignmentSpan.class); 1302 1303 int spanLength = spans.length; 1304 if (spanLength > 0) { 1305 align = spans[spanLength-1].getAlignment(); 1306 } 1307 } 1308 1309 return align; 1310 } 1311 1312 /** 1313 * Get the left edge of the specified paragraph, inset by left margins. 1314 */ 1315 public final int getParagraphLeft(int line) { 1316 int dir = getParagraphDirection(line); 1317 1318 int left = 0; 1319 1320 boolean par = false; 1321 int off = getLineStart(line); 1322 if (off == 0 || mText.charAt(off - 1) == '\n') 1323 par = true; 1324 1325 if (dir == DIR_LEFT_TO_RIGHT) { 1326 if (mSpannedText) { 1327 Spanned sp = (Spanned) mText; 1328 LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), 1329 getLineEnd(line), 1330 LeadingMarginSpan.class); 1331 1332 for (int i = 0; i < spans.length; i++) { 1333 boolean margin = par; 1334 LeadingMarginSpan span = spans[i]; 1335 if (span instanceof LeadingMarginSpan.LeadingMarginSpan2) { 1336 int count = ((LeadingMarginSpan.LeadingMarginSpan2)span).getLeadingMarginLineCount(); 1337 margin = count >= line; 1338 } 1339 left += span.getLeadingMargin(margin); 1340 } 1341 } 1342 } 1343 1344 return left; 1345 } 1346 1347 /** 1348 * Get the right edge of the specified paragraph, inset by right margins. 1349 */ 1350 public final int getParagraphRight(int line) { 1351 int dir = getParagraphDirection(line); 1352 1353 int right = mWidth; 1354 1355 boolean par = false; 1356 int off = getLineStart(line); 1357 if (off == 0 || mText.charAt(off - 1) == '\n') 1358 par = true; 1359 1360 1361 if (dir == DIR_RIGHT_TO_LEFT) { 1362 if (mSpannedText) { 1363 Spanned sp = (Spanned) mText; 1364 LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), 1365 getLineEnd(line), 1366 LeadingMarginSpan.class); 1367 1368 for (int i = 0; i < spans.length; i++) { 1369 right -= spans[i].getLeadingMargin(par); 1370 } 1371 } 1372 } 1373 1374 return right; 1375 } 1376 1377 private void drawText(Canvas canvas, 1378 CharSequence text, int start, int end, 1379 int dir, Directions directions, 1380 float x, int top, int y, int bottom, 1381 TextPaint paint, 1382 TextPaint workPaint, 1383 boolean hasTabs, Object[] parspans) { 1384 char[] buf; 1385 if (!hasTabs) { 1386 if (directions == DIRS_ALL_LEFT_TO_RIGHT) { 1387 if (DEBUG) { 1388 Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir); 1389 } 1390 Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false); 1391 return; 1392 } 1393 buf = null; 1394 } else { 1395 buf = TextUtils.obtain(end - start); 1396 TextUtils.getChars(text, start, end, buf, 0); 1397 } 1398 1399 float h = 0; 1400 1401 int here = 0; 1402 for (int i = 0; i < directions.mDirections.length; i++) { 1403 int there = here + directions.mDirections[i]; 1404 if (there > end - start) 1405 there = end - start; 1406 1407 int segstart = here; 1408 for (int j = hasTabs ? here : there; j <= there; j++) { 1409 if (j == there || buf[j] == '\t') { 1410 h += Styled.drawText(canvas, text, 1411 start + segstart, start + j, 1412 dir, (i & 1) != 0, x + h, 1413 top, y, bottom, paint, workPaint, 1414 start + j != end); 1415 1416 if (j != there && buf[j] == '\t') 1417 h = dir * nextTab(text, start, end, h * dir, parspans); 1418 1419 segstart = j + 1; 1420 } else if (hasTabs && buf[j] >= 0xD800 && buf[j] <= 0xDFFF && j + 1 < there) { 1421 int emoji = Character.codePointAt(buf, j); 1422 1423 if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { 1424 Bitmap bm = EMOJI_FACTORY. 1425 getBitmapFromAndroidPua(emoji); 1426 1427 if (bm != null) { 1428 h += Styled.drawText(canvas, text, 1429 start + segstart, start + j, 1430 dir, (i & 1) != 0, x + h, 1431 top, y, bottom, paint, workPaint, 1432 start + j != end); 1433 1434 if (mEmojiRect == null) { 1435 mEmojiRect = new RectF(); 1436 } 1437 1438 workPaint.set(paint); 1439 Styled.measureText(paint, workPaint, text, 1440 start + j, start + j + 1, 1441 null); 1442 1443 float bitmapHeight = bm.getHeight(); 1444 float textHeight = -workPaint.ascent(); 1445 float scale = textHeight / bitmapHeight; 1446 float width = bm.getWidth() * scale; 1447 1448 mEmojiRect.set(x + h, y - textHeight, 1449 x + h + width, y); 1450 1451 canvas.drawBitmap(bm, null, mEmojiRect, paint); 1452 h += width; 1453 1454 j++; 1455 segstart = j + 1; 1456 } 1457 } 1458 } 1459 } 1460 1461 here = there; 1462 } 1463 1464 if (hasTabs) 1465 TextUtils.recycle(buf); 1466 } 1467 1468 private static float measureText(TextPaint paint, 1469 TextPaint workPaint, 1470 CharSequence text, 1471 int start, int offset, int end, 1472 int dir, Directions directions, 1473 boolean trailing, boolean alt, 1474 boolean hasTabs, Object[] tabs) { 1475 char[] buf = null; 1476 1477 if (hasTabs) { 1478 buf = TextUtils.obtain(end - start); 1479 TextUtils.getChars(text, start, end, buf, 0); 1480 } 1481 1482 float h = 0; 1483 1484 if (alt) { 1485 if (dir == DIR_RIGHT_TO_LEFT) 1486 trailing = !trailing; 1487 } 1488 1489 int here = 0; 1490 for (int i = 0; i < directions.mDirections.length; i++) { 1491 if (alt) 1492 trailing = !trailing; 1493 1494 int there = here + directions.mDirections[i]; 1495 if (there > end - start) 1496 there = end - start; 1497 1498 int segstart = here; 1499 for (int j = hasTabs ? here : there; j <= there; j++) { 1500 int codept = 0; 1501 Bitmap bm = null; 1502 1503 if (hasTabs && j < there) { 1504 codept = buf[j]; 1505 } 1506 1507 if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) { 1508 codept = Character.codePointAt(buf, j); 1509 1510 if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { 1511 bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); 1512 } 1513 } 1514 1515 if (j == there || codept == '\t' || bm != null) { 1516 float segw; 1517 1518 if (offset < start + j || 1519 (trailing && offset <= start + j)) { 1520 if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) { 1521 h += Styled.measureText(paint, workPaint, text, 1522 start + segstart, offset, 1523 null); 1524 return h; 1525 } 1526 1527 if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) { 1528 h -= Styled.measureText(paint, workPaint, text, 1529 start + segstart, offset, 1530 null); 1531 return h; 1532 } 1533 } 1534 1535 segw = Styled.measureText(paint, workPaint, text, 1536 start + segstart, start + j, 1537 null); 1538 1539 if (offset < start + j || 1540 (trailing && offset <= start + j)) { 1541 if (dir == DIR_LEFT_TO_RIGHT) { 1542 h += segw - Styled.measureText(paint, workPaint, 1543 text, 1544 start + segstart, 1545 offset, null); 1546 return h; 1547 } 1548 1549 if (dir == DIR_RIGHT_TO_LEFT) { 1550 h -= segw - Styled.measureText(paint, workPaint, 1551 text, 1552 start + segstart, 1553 offset, null); 1554 return h; 1555 } 1556 } 1557 1558 if (dir == DIR_RIGHT_TO_LEFT) 1559 h -= segw; 1560 else 1561 h += segw; 1562 1563 if (j != there && buf[j] == '\t') { 1564 if (offset == start + j) 1565 return h; 1566 1567 h = dir * nextTab(text, start, end, h * dir, tabs); 1568 } 1569 1570 if (j != there && bm != null) { 1571 if (offset == start + j) return h; 1572 workPaint.set(paint); 1573 Styled.measureText(paint, workPaint, text, 1574 j, j + 2, null); 1575 1576 float wid = (float) bm.getWidth() * 1577 -workPaint.ascent() / bm.getHeight(); 1578 1579 if (dir == DIR_RIGHT_TO_LEFT) { 1580 h -= wid; 1581 } else { 1582 h += wid; 1583 } 1584 1585 j++; 1586 } 1587 1588 segstart = j + 1; 1589 } 1590 } 1591 1592 here = there; 1593 } 1594 1595 if (hasTabs) 1596 TextUtils.recycle(buf); 1597 1598 return h; 1599 } 1600 1601 /** 1602 * Measure width of a run of text on a single line that is known to all be 1603 * in the same direction as the paragraph base direction. Returns the width, 1604 * and the line metrics in fm if fm is not null. 1605 * 1606 * @param paint the paint for the text; will not be modified 1607 * @param workPaint paint available for modification 1608 * @param text text 1609 * @param start start of the line 1610 * @param end limit of the line 1611 * @param fm object to return integer metrics in, can be null 1612 * @param hasTabs true if it is known that the line has tabs 1613 * @param tabs tab position information 1614 * @return the width of the text from start to end 1615 */ 1616 /* package */ static float measureText(TextPaint paint, 1617 TextPaint workPaint, 1618 CharSequence text, 1619 int start, int end, 1620 Paint.FontMetricsInt fm, 1621 boolean hasTabs, Object[] tabs) { 1622 char[] buf = null; 1623 1624 if (hasTabs) { 1625 buf = TextUtils.obtain(end - start); 1626 TextUtils.getChars(text, start, end, buf, 0); 1627 } 1628 1629 int len = end - start; 1630 1631 int lastPos = 0; 1632 float width = 0; 1633 int ascent = 0, descent = 0, top = 0, bottom = 0; 1634 1635 if (fm != null) { 1636 fm.ascent = 0; 1637 fm.descent = 0; 1638 } 1639 1640 for (int pos = hasTabs ? 0 : len; pos <= len; pos++) { 1641 int codept = 0; 1642 Bitmap bm = null; 1643 1644 if (hasTabs && pos < len) { 1645 codept = buf[pos]; 1646 } 1647 1648 if (codept >= 0xD800 && codept <= 0xDFFF && pos < len) { 1649 codept = Character.codePointAt(buf, pos); 1650 1651 if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { 1652 bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); 1653 } 1654 } 1655 1656 if (pos == len || codept == '\t' || bm != null) { 1657 workPaint.baselineShift = 0; 1658 1659 width += Styled.measureText(paint, workPaint, text, 1660 start + lastPos, start + pos, 1661 fm); 1662 1663 if (fm != null) { 1664 if (workPaint.baselineShift < 0) { 1665 fm.ascent += workPaint.baselineShift; 1666 fm.top += workPaint.baselineShift; 1667 } else { 1668 fm.descent += workPaint.baselineShift; 1669 fm.bottom += workPaint.baselineShift; 1670 } 1671 } 1672 1673 if (pos != len) { 1674 if (bm == null) { 1675 // no emoji, must have hit a tab 1676 width = nextTab(text, start, end, width, tabs); 1677 } else { 1678 // This sets up workPaint with the font on the emoji 1679 // text, so that we can extract the ascent and scale. 1680 1681 // We can't use the result of the previous call to 1682 // measureText because the emoji might have its own style. 1683 // We have to initialize workPaint here because if the 1684 // text is unstyled measureText might not use workPaint 1685 // at all. 1686 workPaint.set(paint); 1687 Styled.measureText(paint, workPaint, text, 1688 start + pos, start + pos + 1, null); 1689 1690 width += (float) bm.getWidth() * 1691 -workPaint.ascent() / bm.getHeight(); 1692 1693 // Since we had an emoji, we bump past the second half 1694 // of the surrogate pair. 1695 pos++; 1696 } 1697 } 1698 1699 if (fm != null) { 1700 if (fm.ascent < ascent) { 1701 ascent = fm.ascent; 1702 } 1703 if (fm.descent > descent) { 1704 descent = fm.descent; 1705 } 1706 1707 if (fm.top < top) { 1708 top = fm.top; 1709 } 1710 if (fm.bottom > bottom) { 1711 bottom = fm.bottom; 1712 } 1713 1714 // No need to take bitmap height into account here, 1715 // since it is scaled to match the text height. 1716 } 1717 1718 lastPos = pos + 1; 1719 } 1720 } 1721 1722 if (fm != null) { 1723 fm.ascent = ascent; 1724 fm.descent = descent; 1725 fm.top = top; 1726 fm.bottom = bottom; 1727 } 1728 1729 if (hasTabs) 1730 TextUtils.recycle(buf); 1731 1732 return width; 1733 } 1734 1735 /** 1736 * Returns the position of the next tab stop after h on the line. 1737 * 1738 * @param text the text 1739 * @param start start of the line 1740 * @param end limit of the line 1741 * @param h the current horizontal offset 1742 * @param tabs the tabs, can be null. If it is null, any tabs in effect 1743 * on the line will be used. If there are no tabs, a default offset 1744 * will be used to compute the tab stop. 1745 * @return the offset of the next tab stop. 1746 */ 1747 /* package */ static float nextTab(CharSequence text, int start, int end, 1748 float h, Object[] tabs) { 1749 float nh = Float.MAX_VALUE; 1750 boolean alltabs = false; 1751 1752 if (text instanceof Spanned) { 1753 if (tabs == null) { 1754 tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class); 1755 alltabs = true; 1756 } 1757 1758 for (int i = 0; i < tabs.length; i++) { 1759 if (!alltabs) { 1760 if (!(tabs[i] instanceof TabStopSpan)) 1761 continue; 1762 } 1763 1764 int where = ((TabStopSpan) tabs[i]).getTabStop(); 1765 1766 if (where < nh && where > h) 1767 nh = where; 1768 } 1769 1770 if (nh != Float.MAX_VALUE) 1771 return nh; 1772 } 1773 1774 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; 1775 } 1776 1777 protected final boolean isSpanned() { 1778 return mSpannedText; 1779 } 1780 1781 private void ellipsize(int start, int end, int line, 1782 char[] dest, int destoff) { 1783 int ellipsisCount = getEllipsisCount(line); 1784 1785 if (ellipsisCount == 0) { 1786 return; 1787 } 1788 1789 int ellipsisStart = getEllipsisStart(line); 1790 int linestart = getLineStart(line); 1791 1792 for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { 1793 char c; 1794 1795 if (i == ellipsisStart) { 1796 c = '\u2026'; // ellipsis 1797 } else { 1798 c = '\uFEFF'; // 0-width space 1799 } 1800 1801 int a = i + linestart; 1802 1803 if (a >= start && a < end) { 1804 dest[destoff + a - start] = c; 1805 } 1806 } 1807 } 1808 1809 /** 1810 * Stores information about bidirectional (left-to-right or right-to-left) 1811 * text within the layout of a line. TODO: This work is not complete 1812 * or correct and will be fleshed out in a later revision. 1813 */ 1814 public static class Directions { 1815 private short[] mDirections; 1816 1817 // The values in mDirections are the offsets from the first character 1818 // in the line to the next flip in direction. Runs at even indices 1819 // are left-to-right, the others are right-to-left. So, for example, 1820 // a line that starts with a right-to-left run has 0 at mDirections[0], 1821 // since the 'first' (ltr) run is zero length. 1822 // 1823 // The code currently assumes that each run is adjacent to the previous 1824 // one, progressing in the base line direction. This isn't sufficient 1825 // to handle nested runs, for example numeric text in an rtl context 1826 // in an ltr paragraph. 1827 /* package */ Directions(short[] dirs) { 1828 mDirections = dirs; 1829 } 1830 } 1831 1832 /** 1833 * Return the offset of the first character to be ellipsized away, 1834 * relative to the start of the line. (So 0 if the beginning of the 1835 * line is ellipsized, not getLineStart().) 1836 */ 1837 public abstract int getEllipsisStart(int line); 1838 /** 1839 * Returns the number of characters to be ellipsized away, or 0 if 1840 * no ellipsis is to take place. 1841 */ 1842 public abstract int getEllipsisCount(int line); 1843 1844 /* package */ static class Ellipsizer implements CharSequence, GetChars { 1845 /* package */ CharSequence mText; 1846 /* package */ Layout mLayout; 1847 /* package */ int mWidth; 1848 /* package */ TextUtils.TruncateAt mMethod; 1849 1850 public Ellipsizer(CharSequence s) { 1851 mText = s; 1852 } 1853 1854 public char charAt(int off) { 1855 char[] buf = TextUtils.obtain(1); 1856 getChars(off, off + 1, buf, 0); 1857 char ret = buf[0]; 1858 1859 TextUtils.recycle(buf); 1860 return ret; 1861 } 1862 1863 public void getChars(int start, int end, char[] dest, int destoff) { 1864 int line1 = mLayout.getLineForOffset(start); 1865 int line2 = mLayout.getLineForOffset(end); 1866 1867 TextUtils.getChars(mText, start, end, dest, destoff); 1868 1869 for (int i = line1; i <= line2; i++) { 1870 mLayout.ellipsize(start, end, i, dest, destoff); 1871 } 1872 } 1873 1874 public int length() { 1875 return mText.length(); 1876 } 1877 1878 public CharSequence subSequence(int start, int end) { 1879 char[] s = new char[end - start]; 1880 getChars(start, end, s, 0); 1881 return new String(s); 1882 } 1883 1884 public String toString() { 1885 char[] s = new char[length()]; 1886 getChars(0, length(), s, 0); 1887 return new String(s); 1888 } 1889 1890 } 1891 1892 /* package */ static class SpannedEllipsizer 1893 extends Ellipsizer implements Spanned { 1894 private Spanned mSpanned; 1895 1896 public SpannedEllipsizer(CharSequence display) { 1897 super(display); 1898 mSpanned = (Spanned) display; 1899 } 1900 1901 public <T> T[] getSpans(int start, int end, Class<T> type) { 1902 return mSpanned.getSpans(start, end, type); 1903 } 1904 1905 public int getSpanStart(Object tag) { 1906 return mSpanned.getSpanStart(tag); 1907 } 1908 1909 public int getSpanEnd(Object tag) { 1910 return mSpanned.getSpanEnd(tag); 1911 } 1912 1913 public int getSpanFlags(Object tag) { 1914 return mSpanned.getSpanFlags(tag); 1915 } 1916 1917 public int nextSpanTransition(int start, int limit, Class type) { 1918 return mSpanned.nextSpanTransition(start, limit, type); 1919 } 1920 1921 public CharSequence subSequence(int start, int end) { 1922 char[] s = new char[end - start]; 1923 getChars(start, end, s, 0); 1924 1925 SpannableString ss = new SpannableString(new String(s)); 1926 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); 1927 return ss; 1928 } 1929 } 1930 1931 private CharSequence mText; 1932 private TextPaint mPaint; 1933 /* package */ TextPaint mWorkPaint; 1934 private int mWidth; 1935 private Alignment mAlignment = Alignment.ALIGN_NORMAL; 1936 private float mSpacingMult; 1937 private float mSpacingAdd; 1938 private static Rect sTempRect = new Rect(); 1939 private boolean mSpannedText; 1940 1941 public static final int DIR_LEFT_TO_RIGHT = 1; 1942 public static final int DIR_RIGHT_TO_LEFT = -1; 1943 1944 /* package */ static final int DIR_REQUEST_LTR = 1; 1945 /* package */ static final int DIR_REQUEST_RTL = -1; 1946 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; 1947 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; 1948 1949 public enum Alignment { 1950 ALIGN_NORMAL, 1951 ALIGN_OPPOSITE, 1952 ALIGN_CENTER, 1953 // XXX ALIGN_LEFT, 1954 // XXX ALIGN_RIGHT, 1955 } 1956 1957 private static final int TAB_INCREMENT = 20; 1958 1959 /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = 1960 new Directions(new short[] { 32767 }); 1961 /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = 1962 new Directions(new short[] { 0, 32767 }); 1963 1964} 1965