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