Layout.java revision 9f7a4442b89cc06cb8cae6992484e7ae795323ab
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 // return the level of the character at offset. 534 // XXX remove if not needed 535 private int getRunLevelAtOffset(int offset) { 536 int line = getLineForOffset(offset); 537 int lineStart = getLineStart(line); 538 int lineEnd = getLineVisibleEnd(line); 539 int[] runs = getLineDirections(line).mDirections; 540 for (int i = 0; i < runs.length; i += 2) { 541 int start = runs[i]; 542 if (offset >= start) { 543 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 544 if (limit > lineEnd) { 545 limit = lineEnd; 546 } 547 if (offset < limit) { 548 return (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 549 } 550 } 551 } 552 return getParagraphDirection(line) == 1 ? 0 : 1; 553 } 554 555 private boolean primaryIsTrailingPrevious(int offset) { 556 int line = getLineForOffset(offset); 557 int lineStart = getLineStart(line); 558 int lineEnd = getLineVisibleEnd(line); 559 int[] runs = getLineDirections(line).mDirections; 560 561 int levelAt = -1; 562 for (int i = 0; i < runs.length; i += 2) { 563 int start = lineStart + runs[i]; 564 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 565 if (limit > lineEnd) { 566 limit = lineEnd; 567 } 568 if (offset >= start && offset < limit) { 569 if (offset > start) { 570 // Previous character is at same level, so don't use trailing. 571 return false; 572 } 573 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 574 break; 575 } 576 } 577 if (levelAt == -1) { 578 // Offset was limit of line. 579 levelAt = getParagraphDirection(line) == 1 ? 0 : 1; 580 } 581 582 // At level boundary, check previous level. 583 int levelBefore = -1; 584 if (offset == lineStart) { 585 levelBefore = getParagraphDirection(line) == 1 ? 0 : 1; 586 } else { 587 offset -= 1; 588 for (int i = 0; i < runs.length; i += 2) { 589 int start = lineStart + runs[i]; 590 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 591 if (limit > lineEnd) { 592 limit = lineEnd; 593 } 594 if (offset >= start && offset < limit) { 595 levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 596 break; 597 } 598 } 599 } 600 601 return levelBefore < levelAt; 602 } 603 604 /** 605 * Get the primary horizontal position for the specified text offset. 606 * This is the location where a new character would be inserted in 607 * the paragraph's primary direction. 608 */ 609 public float getPrimaryHorizontal(int offset) { 610 boolean trailing = primaryIsTrailingPrevious(offset); 611 return getHorizontal(offset, trailing); 612 } 613 614 /** 615 * Get the secondary horizontal position for the specified text offset. 616 * This is the location where a new character would be inserted in 617 * the direction other than the paragraph's primary direction. 618 */ 619 public float getSecondaryHorizontal(int offset) { 620 boolean trailing = primaryIsTrailingPrevious(offset); 621 return getHorizontal(offset, !trailing); 622 } 623 624 private float getHorizontal(int offset, boolean trailing) { 625 int line = getLineForOffset(offset); 626 627 return getHorizontal(offset, trailing, line); 628 } 629 630 private float getHorizontal(int offset, boolean trailing, int line) { 631 int start = getLineStart(line); 632 int end = getLineVisibleEnd(line); 633 int dir = getParagraphDirection(line); 634 boolean tab = getLineContainsTab(line); 635 Directions directions = getLineDirections(line); 636 637 TabStopSpan[] tabs = null; 638 if (tab && mText instanceof Spanned) { 639 tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); 640 } 641 642 float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end, 643 dir, directions, trailing, tab, tabs); 644 645 if (offset > end) { 646 if (dir == DIR_RIGHT_TO_LEFT) 647 wid -= measureText(mPaint, mWorkPaint, 648 mText, end, offset, null, tab, tabs); 649 else 650 wid += measureText(mPaint, mWorkPaint, 651 mText, end, offset, null, tab, tabs); 652 } 653 654 Alignment align = getParagraphAlignment(line); 655 int left = getParagraphLeft(line); 656 int right = getParagraphRight(line); 657 658 if (align == Alignment.ALIGN_NORMAL) { 659 if (dir == DIR_RIGHT_TO_LEFT) 660 return right + wid; 661 else 662 return left + wid; 663 } 664 665 float max = getLineMax(line); 666 667 if (align == Alignment.ALIGN_OPPOSITE) { 668 if (dir == DIR_RIGHT_TO_LEFT) 669 return left + max + wid; 670 else 671 return right - max + wid; 672 } else { /* align == Alignment.ALIGN_CENTER */ 673 int imax = ((int) max) & ~1; 674 675 if (dir == DIR_RIGHT_TO_LEFT) 676 return right - (((right - left) - imax) / 2) + wid; 677 else 678 return left + ((right - left) - imax) / 2 + wid; 679 } 680 } 681 682 /** 683 * Get the leftmost position that should be exposed for horizontal 684 * scrolling on the specified line. 685 */ 686 public float getLineLeft(int line) { 687 int dir = getParagraphDirection(line); 688 Alignment align = getParagraphAlignment(line); 689 690 if (align == Alignment.ALIGN_NORMAL) { 691 if (dir == DIR_RIGHT_TO_LEFT) 692 return getParagraphRight(line) - getLineMax(line); 693 else 694 return 0; 695 } else if (align == Alignment.ALIGN_OPPOSITE) { 696 if (dir == DIR_RIGHT_TO_LEFT) 697 return 0; 698 else 699 return mWidth - getLineMax(line); 700 } else { /* align == Alignment.ALIGN_CENTER */ 701 int left = getParagraphLeft(line); 702 int right = getParagraphRight(line); 703 int max = ((int) getLineMax(line)) & ~1; 704 705 return left + ((right - left) - max) / 2; 706 } 707 } 708 709 /** 710 * Get the rightmost position that should be exposed for horizontal 711 * scrolling on the specified line. 712 */ 713 public float getLineRight(int line) { 714 int dir = getParagraphDirection(line); 715 Alignment align = getParagraphAlignment(line); 716 717 if (align == Alignment.ALIGN_NORMAL) { 718 if (dir == DIR_RIGHT_TO_LEFT) 719 return mWidth; 720 else 721 return getParagraphLeft(line) + getLineMax(line); 722 } else if (align == Alignment.ALIGN_OPPOSITE) { 723 if (dir == DIR_RIGHT_TO_LEFT) 724 return getLineMax(line); 725 else 726 return mWidth; 727 } else { /* align == Alignment.ALIGN_CENTER */ 728 int left = getParagraphLeft(line); 729 int right = getParagraphRight(line); 730 int max = ((int) getLineMax(line)) & ~1; 731 732 return right - ((right - left) - max) / 2; 733 } 734 } 735 736 /** 737 * Gets the horizontal extent of the specified line, excluding 738 * trailing whitespace. 739 */ 740 public float getLineMax(int line) { 741 return getLineMax(line, null, false); 742 } 743 744 /** 745 * Gets the horizontal extent of the specified line, including 746 * trailing whitespace. 747 */ 748 public float getLineWidth(int line) { 749 return getLineMax(line, null, true); 750 } 751 752 private float getLineMax(int line, Object[] tabs, boolean full) { 753 int start = getLineStart(line); 754 int end; 755 756 if (full) { 757 end = getLineEnd(line); 758 } else { 759 end = getLineVisibleEnd(line); 760 } 761 boolean tab = getLineContainsTab(line); 762 763 if (tabs == null && tab && mText instanceof Spanned) { 764 tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); 765 } 766 767 return measureText(mPaint, mWorkPaint, 768 mText, start, end, null, tab, tabs); 769 } 770 771 /** 772 * Get the line number corresponding to the specified vertical position. 773 * If you ask for a position above 0, you get 0; if you ask for a position 774 * below the bottom of the text, you get the last line. 775 */ 776 // FIXME: It may be faster to do a linear search for layouts without many lines. 777 public int getLineForVertical(int vertical) { 778 int high = getLineCount(), low = -1, guess; 779 780 while (high - low > 1) { 781 guess = (high + low) / 2; 782 783 if (getLineTop(guess) > vertical) 784 high = guess; 785 else 786 low = guess; 787 } 788 789 if (low < 0) 790 return 0; 791 else 792 return low; 793 } 794 795 /** 796 * Get the line number on which the specified text offset appears. 797 * If you ask for a position before 0, you get 0; if you ask for a position 798 * beyond the end of the text, you get the last line. 799 */ 800 public int getLineForOffset(int offset) { 801 int high = getLineCount(), low = -1, guess; 802 803 while (high - low > 1) { 804 guess = (high + low) / 2; 805 806 if (getLineStart(guess) > offset) 807 high = guess; 808 else 809 low = guess; 810 } 811 812 if (low < 0) 813 return 0; 814 else 815 return low; 816 } 817 818 /** 819 * Get the character offset on the specified line whose position is 820 * closest to the specified horizontal position. 821 */ 822 public int getOffsetForHorizontal(int line, float horiz) { 823 int max = getLineEnd(line) - 1; 824 int min = getLineStart(line); 825 Directions dirs = getLineDirections(line); 826 827 if (line == getLineCount() - 1) 828 max++; 829 830 int best = min; 831 float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); 832 833 for (int i = 0; i < dirs.mDirections.length; i += 2) { 834 int here = min + dirs.mDirections[i]; 835 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 836 int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1; 837 838 if (there > max) 839 there = max; 840 int high = there - 1 + 1, low = here + 1 - 1, guess; 841 842 while (high - low > 1) { 843 guess = (high + low) / 2; 844 int adguess = getOffsetAtStartOf(guess); 845 846 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap) 847 high = guess; 848 else 849 low = guess; 850 } 851 852 if (low < here + 1) 853 low = here + 1; 854 855 if (low < there) { 856 low = getOffsetAtStartOf(low); 857 858 float dist = Math.abs(getPrimaryHorizontal(low) - horiz); 859 860 int aft = TextUtils.getOffsetAfter(mText, low); 861 if (aft < there) { 862 float other = Math.abs(getPrimaryHorizontal(aft) - horiz); 863 864 if (other < dist) { 865 dist = other; 866 low = aft; 867 } 868 } 869 870 if (dist < bestdist) { 871 bestdist = dist; 872 best = low; 873 } 874 } 875 876 float dist = Math.abs(getPrimaryHorizontal(here) - horiz); 877 878 if (dist < bestdist) { 879 bestdist = dist; 880 best = here; 881 } 882 } 883 884 float dist = Math.abs(getPrimaryHorizontal(max) - horiz); 885 886 if (dist < bestdist) { 887 bestdist = dist; 888 best = max; 889 } 890 891 return best; 892 } 893 894 /** 895 * Return the text offset after the last character on the specified line. 896 */ 897 public final int getLineEnd(int line) { 898 return getLineStart(line + 1); 899 } 900 901 /** 902 * Return the text offset after the last visible character (so whitespace 903 * is not counted) on the specified line. 904 */ 905 public int getLineVisibleEnd(int line) { 906 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 907 } 908 909 private int getLineVisibleEnd(int line, int start, int end) { 910 if (DEBUG) { 911 Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end); 912 } 913 914 CharSequence text = mText; 915 char ch; 916 if (line == getLineCount() - 1) { 917 return end; 918 } 919 920 for (; end > start; end--) { 921 ch = text.charAt(end - 1); 922 923 if (ch == '\n') { 924 return end - 1; 925 } 926 927 if (ch != ' ' && ch != '\t') { 928 break; 929 } 930 931 } 932 933 return end; 934 } 935 936 /** 937 * Return the vertical position of the bottom of the specified line. 938 */ 939 public final int getLineBottom(int line) { 940 return getLineTop(line + 1); 941 } 942 943 /** 944 * Return the vertical position of the baseline of the specified line. 945 */ 946 public final int getLineBaseline(int line) { 947 // getLineTop(line+1) == getLineTop(line) 948 return getLineTop(line+1) - getLineDescent(line); 949 } 950 951 /** 952 * Get the ascent of the text on the specified line. 953 * The return value is negative to match the Paint.ascent() convention. 954 */ 955 public final int getLineAscent(int line) { 956 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) 957 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); 958 } 959 960 public int getOffsetToLeftOf(int offset) { 961 return getOffsetToLeftRightOf(offset, true); 962 } 963 964 public int getOffsetToRightOf(int offset) { 965 return getOffsetToLeftRightOf(offset, false); 966 } 967 968 // 1) The caret marks the leading edge of a character. The character 969 // logically before it might be on a different level, and the active caret 970 // position is on the character at the lower level. If that character 971 // was the previous character, the caret is on its trailing edge. 972 // 2) Take this character/edge and move it in the indicated direction. 973 // This gives you a new character and a new edge. 974 // 3) This position is between two visually adjacent characters. One of 975 // these might be at a lower level. The active position is on the 976 // character at the lower level. 977 // 4) If the active position is on the trailing edge of the character, 978 // the new caret position is the following logical character, else it 979 // is the character. 980 private int getOffsetToLeftRightOf(int caret, boolean toLeft) { 981 int line = getLineForOffset(caret); 982 int lineStart = getLineStart(line); 983 int lineEnd = getLineEnd(line); 984 985 boolean paraIsRtl = getParagraphDirection(line) == -1; 986 int[] runs = getLineDirections(line).mDirections; 987 988 int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1; 989 boolean trailing = false; 990 991 if (caret == lineStart) { 992 runIndex = -2; 993 } else if (caret == lineEnd) { 994 runIndex = runs.length; 995 } else { 996 // First, get information about the run containing the character with 997 // the active caret. 998 for (runIndex = 0; runIndex < runs.length; runIndex += 2) { 999 runStart = lineStart + runs[runIndex]; 1000 if (caret >= runStart) { 1001 runLimit = runStart + (runs[runIndex+1] & RUN_LENGTH_MASK); 1002 if (runLimit > lineEnd) { 1003 runLimit = lineEnd; 1004 } 1005 if (caret < runLimit) { 1006 runLevel = (runs[runIndex+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 1007 if (caret == runStart) { 1008 // The caret is on a run boundary, see if we should 1009 // use the position on the trailing edge of the previous 1010 // logical character instead. 1011 int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit; 1012 int pos = caret - 1; 1013 for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) { 1014 prevRunStart = lineStart + runs[prevRunIndex]; 1015 if (pos >= prevRunStart) { 1016 prevRunLimit = prevRunStart + (runs[prevRunIndex+1] & RUN_LENGTH_MASK); 1017 if (prevRunLimit > lineEnd) { 1018 prevRunLimit = lineEnd; 1019 } 1020 if (pos < prevRunLimit) { 1021 prevRunLevel = (runs[prevRunIndex+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 1022 if (prevRunLevel < runLevel) { 1023 // Start from logically previous character. 1024 runIndex = prevRunIndex; 1025 runLevel = prevRunLevel; 1026 runStart = prevRunStart; 1027 runLimit = prevRunLimit; 1028 trailing = true; 1029 break; 1030 } 1031 } 1032 } 1033 } 1034 } 1035 break; 1036 } 1037 } 1038 } 1039 1040 // caret might be = lineEnd. This is generally a space or paragraph 1041 // separator and has an associated run, but might be the end of 1042 // text, in which case it doesn't. If that happens, we ran off the 1043 // end of the run list, and runIndex == runs.length. In this case, 1044 // we are at a run boundary so we skip the below test. 1045 if (runIndex != runs.length) { 1046 boolean rtlRun = (runLevel & 0x1) != 0; 1047 boolean advance = toLeft == rtlRun; 1048 if (caret != (advance ? runLimit : runStart) || advance != trailing) { 1049 // Moving within or into the run, so we can move logically. 1050 newCaret = getOffsetBeforeAfter(caret, advance); 1051 // If the new position is internal to the run, we're at the strong 1052 // position already so we're finished. 1053 if (newCaret != (advance ? runLimit : runStart)) { 1054 return newCaret; 1055 } 1056 } 1057 } 1058 } 1059 1060 // If newCaret is -1, we're starting at a run boundary and crossing 1061 // into another run. Otherwise we've arrived at a run boundary, and 1062 // need to figure out which character to attach to. Note we might 1063 // need to run this twice, if we cross a run boundary and end up at 1064 // another run boundary. 1065 while (true) { 1066 boolean advance = toLeft == paraIsRtl; 1067 int otherRunIndex = runIndex + (advance ? 2 : -2); 1068 if (otherRunIndex >= 0 && otherRunIndex < runs.length) { 1069 int otherRunStart = lineStart + runs[otherRunIndex]; 1070 int otherRunLimit = otherRunStart + (runs[otherRunIndex+1] & RUN_LENGTH_MASK); 1071 if (otherRunLimit > lineEnd) { 1072 otherRunLimit = lineEnd; 1073 } 1074 int otherRunLevel = runs[otherRunIndex+1] >>> RUN_LEVEL_SHIFT & RUN_LEVEL_MASK; 1075 boolean otherRunIsRtl = (otherRunLevel & 1) != 0; 1076 1077 advance = toLeft == otherRunIsRtl; 1078 if (newCaret == -1) { 1079 newCaret = getOffsetBeforeAfter(advance ? otherRunStart : otherRunLimit, advance); 1080 if (newCaret == (advance ? otherRunLimit : otherRunStart)) { 1081 // Crossed and ended up at a new boundary, repeat a second and final time. 1082 runIndex = otherRunIndex; 1083 runLevel = otherRunLevel; 1084 continue; 1085 } 1086 break; 1087 } 1088 1089 // The new caret is at a boundary. 1090 if (otherRunLevel < runLevel) { 1091 // The strong character is in the other run. 1092 newCaret = advance ? otherRunStart : otherRunLimit; 1093 } 1094 break; 1095 } 1096 1097 if (newCaret == -1) { 1098 // We're walking off the end of the line. The paragraph 1099 // level is always equal to or lower than any internal level, so 1100 // the boundaries get the strong caret. 1101 newCaret = getOffsetBeforeAfter(caret, advance); 1102 break; 1103 } 1104 // Else we've arrived at the end of the line. That's a strong position. 1105 // We might have arrived here by crossing over a run with no internal 1106 // breaks and dropping out of the above loop before advancing one final 1107 // time, so reset the caret. 1108 // Note, we use '<=' below to handle a situation where the only run 1109 // on the line is a counter-directional run. If we're not advancing, 1110 // we can end up at the 'lineEnd' position but the caret we want is at 1111 // the lineStart. 1112 if (newCaret <= lineEnd) { 1113 newCaret = advance ? lineEnd : lineStart; 1114 } 1115 break; 1116 } 1117 1118 return newCaret; 1119 } 1120 1121 // utility, maybe just roll into the above. 1122 private int getOffsetBeforeAfter(int offset, boolean after) { 1123 if (after) { 1124 return TextUtils.getOffsetAfter(mText, offset); 1125 } 1126 return TextUtils.getOffsetBefore(mText, offset); 1127 } 1128 1129 private int getOffsetAtStartOf(int offset) { 1130 // XXX this probably should skip local reorderings and 1131 // zero-width characters, look at callers 1132 if (offset == 0) 1133 return 0; 1134 1135 CharSequence text = mText; 1136 char c = text.charAt(offset); 1137 1138 if (c >= '\uDC00' && c <= '\uDFFF') { 1139 char c1 = text.charAt(offset - 1); 1140 1141 if (c1 >= '\uD800' && c1 <= '\uDBFF') 1142 offset -= 1; 1143 } 1144 1145 if (mSpannedText) { 1146 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 1147 ReplacementSpan.class); 1148 1149 for (int i = 0; i < spans.length; i++) { 1150 int start = ((Spanned) text).getSpanStart(spans[i]); 1151 int end = ((Spanned) text).getSpanEnd(spans[i]); 1152 1153 if (start < offset && end > offset) 1154 offset = start; 1155 } 1156 } 1157 1158 return offset; 1159 } 1160 1161 /** 1162 * Fills in the specified Path with a representation of a cursor 1163 * at the specified offset. This will often be a vertical line 1164 * but can be multiple discontinous lines in text with multiple 1165 * directionalities. 1166 */ 1167 public void getCursorPath(int point, Path dest, 1168 CharSequence editingBuffer) { 1169 dest.reset(); 1170 1171 int line = getLineForOffset(point); 1172 int top = getLineTop(line); 1173 int bottom = getLineTop(line+1); 1174 1175 float h1 = getPrimaryHorizontal(point) - 0.5f; 1176 float h2 = getSecondaryHorizontal(point) - 0.5f; 1177 1178 int caps = TextKeyListener.getMetaState(editingBuffer, 1179 KeyEvent.META_SHIFT_ON) | 1180 TextKeyListener.getMetaState(editingBuffer, 1181 TextKeyListener.META_SELECTING); 1182 int fn = TextKeyListener.getMetaState(editingBuffer, 1183 KeyEvent.META_ALT_ON); 1184 int dist = 0; 1185 1186 if (caps != 0 || fn != 0) { 1187 dist = (bottom - top) >> 2; 1188 1189 if (fn != 0) 1190 top += dist; 1191 if (caps != 0) 1192 bottom -= dist; 1193 } 1194 1195 if (h1 < 0.5f) 1196 h1 = 0.5f; 1197 if (h2 < 0.5f) 1198 h2 = 0.5f; 1199 1200 if (h1 == h2) { 1201 dest.moveTo(h1, top); 1202 dest.lineTo(h1, bottom); 1203 } else { 1204 dest.moveTo(h1, top); 1205 dest.lineTo(h1, (top + bottom) >> 1); 1206 1207 dest.moveTo(h2, (top + bottom) >> 1); 1208 dest.lineTo(h2, bottom); 1209 } 1210 1211 if (caps == 2) { 1212 dest.moveTo(h2, bottom); 1213 dest.lineTo(h2 - dist, bottom + dist); 1214 dest.lineTo(h2, bottom); 1215 dest.lineTo(h2 + dist, bottom + dist); 1216 } else if (caps == 1) { 1217 dest.moveTo(h2, bottom); 1218 dest.lineTo(h2 - dist, bottom + dist); 1219 1220 dest.moveTo(h2 - dist, bottom + dist - 0.5f); 1221 dest.lineTo(h2 + dist, bottom + dist - 0.5f); 1222 1223 dest.moveTo(h2 + dist, bottom + dist); 1224 dest.lineTo(h2, bottom); 1225 } 1226 1227 if (fn == 2) { 1228 dest.moveTo(h1, top); 1229 dest.lineTo(h1 - dist, top - dist); 1230 dest.lineTo(h1, top); 1231 dest.lineTo(h1 + dist, top - dist); 1232 } else if (fn == 1) { 1233 dest.moveTo(h1, top); 1234 dest.lineTo(h1 - dist, top - dist); 1235 1236 dest.moveTo(h1 - dist, top - dist + 0.5f); 1237 dest.lineTo(h1 + dist, top - dist + 0.5f); 1238 1239 dest.moveTo(h1 + dist, top - dist); 1240 dest.lineTo(h1, top); 1241 } 1242 } 1243 1244 private void addSelection(int line, int start, int end, 1245 int top, int bottom, Path dest) { 1246 int linestart = getLineStart(line); 1247 int lineend = getLineEnd(line); 1248 Directions dirs = getLineDirections(line); 1249 1250 if (lineend > linestart && mText.charAt(lineend - 1) == '\n') 1251 lineend--; 1252 1253 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1254 int here = linestart + dirs.mDirections[i]; 1255 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1256 1257 if (there > lineend) 1258 there = lineend; 1259 1260 if (start <= there && end >= here) { 1261 int st = Math.max(start, here); 1262 int en = Math.min(end, there); 1263 1264 if (st != en) { 1265 float h1 = getHorizontal(st, false, line); 1266 float h2 = getHorizontal(en, true, line); 1267 1268 dest.addRect(h1, top, h2, bottom, Path.Direction.CW); 1269 } 1270 } 1271 } 1272 } 1273 1274 /** 1275 * Fills in the specified Path with a representation of a highlight 1276 * between the specified offsets. This will often be a rectangle 1277 * or a potentially discontinuous set of rectangles. If the start 1278 * and end are the same, the returned path is empty. 1279 */ 1280 public void getSelectionPath(int start, int end, Path dest) { 1281 dest.reset(); 1282 1283 if (start == end) 1284 return; 1285 1286 if (end < start) { 1287 int temp = end; 1288 end = start; 1289 start = temp; 1290 } 1291 1292 int startline = getLineForOffset(start); 1293 int endline = getLineForOffset(end); 1294 1295 int top = getLineTop(startline); 1296 int bottom = getLineBottom(endline); 1297 1298 if (startline == endline) { 1299 addSelection(startline, start, end, top, bottom, dest); 1300 } else { 1301 final float width = mWidth; 1302 1303 addSelection(startline, start, getLineEnd(startline), 1304 top, getLineBottom(startline), dest); 1305 1306 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) 1307 dest.addRect(getLineLeft(startline), top, 1308 0, getLineBottom(startline), Path.Direction.CW); 1309 else 1310 dest.addRect(getLineRight(startline), top, 1311 width, getLineBottom(startline), Path.Direction.CW); 1312 1313 for (int i = startline + 1; i < endline; i++) { 1314 top = getLineTop(i); 1315 bottom = getLineBottom(i); 1316 dest.addRect(0, top, width, bottom, Path.Direction.CW); 1317 } 1318 1319 top = getLineTop(endline); 1320 bottom = getLineBottom(endline); 1321 1322 addSelection(endline, getLineStart(endline), end, 1323 top, bottom, dest); 1324 1325 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) 1326 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW); 1327 else 1328 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW); 1329 } 1330 } 1331 1332 /** 1333 * Get the alignment of the specified paragraph, taking into account 1334 * markup attached to it. 1335 */ 1336 public final Alignment getParagraphAlignment(int line) { 1337 Alignment align = mAlignment; 1338 1339 if (mSpannedText) { 1340 Spanned sp = (Spanned) mText; 1341 AlignmentSpan[] spans = sp.getSpans(getLineStart(line), 1342 getLineEnd(line), 1343 AlignmentSpan.class); 1344 1345 int spanLength = spans.length; 1346 if (spanLength > 0) { 1347 align = spans[spanLength-1].getAlignment(); 1348 } 1349 } 1350 1351 return align; 1352 } 1353 1354 /** 1355 * Get the left edge of the specified paragraph, inset by left margins. 1356 */ 1357 public final int getParagraphLeft(int line) { 1358 int dir = getParagraphDirection(line); 1359 1360 int left = 0; 1361 1362 boolean par = false; 1363 int off = getLineStart(line); 1364 if (off == 0 || mText.charAt(off - 1) == '\n') 1365 par = true; 1366 1367 if (dir == DIR_LEFT_TO_RIGHT) { 1368 if (mSpannedText) { 1369 Spanned sp = (Spanned) mText; 1370 LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), 1371 getLineEnd(line), 1372 LeadingMarginSpan.class); 1373 1374 for (int i = 0; i < spans.length; i++) { 1375 boolean margin = par; 1376 LeadingMarginSpan span = spans[i]; 1377 if (span instanceof LeadingMarginSpan.LeadingMarginSpan2) { 1378 int count = ((LeadingMarginSpan.LeadingMarginSpan2)span).getLeadingMarginLineCount(); 1379 margin = count >= line; 1380 } 1381 left += span.getLeadingMargin(margin); 1382 } 1383 } 1384 } 1385 1386 return left; 1387 } 1388 1389 /** 1390 * Get the right edge of the specified paragraph, inset by right margins. 1391 */ 1392 public final int getParagraphRight(int line) { 1393 int dir = getParagraphDirection(line); 1394 1395 int right = mWidth; 1396 1397 boolean par = false; 1398 int off = getLineStart(line); 1399 if (off == 0 || mText.charAt(off - 1) == '\n') 1400 par = true; 1401 1402 1403 if (dir == DIR_RIGHT_TO_LEFT) { 1404 if (mSpannedText) { 1405 Spanned sp = (Spanned) mText; 1406 LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), 1407 getLineEnd(line), 1408 LeadingMarginSpan.class); 1409 1410 for (int i = 0; i < spans.length; i++) { 1411 right -= spans[i].getLeadingMargin(par); 1412 } 1413 } 1414 } 1415 1416 return right; 1417 } 1418 1419 private void drawText(Canvas canvas, 1420 CharSequence text, int start, int end, 1421 int dir, Directions directions, 1422 float x, int top, int y, int bottom, 1423 TextPaint paint, 1424 TextPaint workPaint, 1425 boolean hasTabs, Object[] parspans) { 1426 char[] buf; 1427 if (!hasTabs) { 1428 if (directions == DIRS_ALL_LEFT_TO_RIGHT) { 1429 if (DEBUG) { 1430 Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir); 1431 } 1432 Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false); 1433 return; 1434 } 1435 buf = null; 1436 } else { 1437 buf = TextUtils.obtain(end - start); 1438 TextUtils.getChars(text, start, end, buf, 0); 1439 } 1440 1441 float h = 0; 1442 1443 int lastRunIndex = directions.mDirections.length - 2; 1444 for (int i = 0; i < directions.mDirections.length; i += 2) { 1445 int here = start + directions.mDirections[i]; 1446 int there = here + (directions.mDirections[i+1] & RUN_LENGTH_MASK); 1447 boolean runIsRtl = (directions.mDirections[i+1] & RUN_RTL_FLAG) != 0; 1448 1449 if (there > end) 1450 there = end; 1451 1452 int segstart = here; 1453 for (int j = hasTabs ? here : there; j <= there; j++) { 1454 int pj = j - start; 1455 if (j == there || buf[pj] == '\t') { 1456 h += Styled.drawText(canvas, text, 1457 segstart, j, 1458 dir, runIsRtl, x + h, 1459 top, y, bottom, paint, workPaint, 1460 i != lastRunIndex || j != end); 1461 1462 if (j != there) 1463 h = dir * nextTab(text, start, end, h * dir, parspans); 1464 1465 segstart = j + 1; 1466 } else if (hasTabs && buf[pj] >= 0xD800 && buf[pj] <= 0xDFFF && j + 1 < there) { 1467 int emoji = Character.codePointAt(buf, pj); 1468 1469 if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { 1470 Bitmap bm = EMOJI_FACTORY. 1471 getBitmapFromAndroidPua(emoji); 1472 1473 if (bm != null) { 1474 h += Styled.drawText(canvas, text, 1475 segstart, j, 1476 dir, runIsRtl, x + h, 1477 top, y, bottom, paint, workPaint, 1478 i != lastRunIndex || j != end); 1479 1480 if (mEmojiRect == null) { 1481 mEmojiRect = new RectF(); 1482 } 1483 1484 workPaint.set(paint); 1485 Styled.measureText(paint, workPaint, text, 1486 j, j + 1, 1487 null); 1488 1489 float bitmapHeight = bm.getHeight(); 1490 float textHeight = -workPaint.ascent(); 1491 float scale = textHeight / bitmapHeight; 1492 float width = bm.getWidth() * scale; 1493 1494 mEmojiRect.set(x + h, y - textHeight, 1495 x + h + width, y); 1496 1497 canvas.drawBitmap(bm, null, mEmojiRect, paint); 1498 h += width; 1499 1500 j++; 1501 segstart = j + 1; 1502 } 1503 } 1504 } 1505 } 1506 } 1507 1508 if (hasTabs) 1509 TextUtils.recycle(buf); 1510 } 1511 1512 /** 1513 * Get the distance from the margin to the requested edge of the character 1514 * at offset on the line from start to end. Trailing indicates the edge 1515 * should be the trailing edge of the character at offset-1. 1516 */ 1517 /* package */ static float measureText(TextPaint paint, 1518 TextPaint workPaint, 1519 CharSequence text, 1520 int start, int offset, int end, 1521 int dir, Directions directions, 1522 boolean trailing, boolean hasTabs, 1523 Object[] tabs) { 1524 char[] buf = null; 1525 1526 if (hasTabs) { 1527 buf = TextUtils.obtain(end - start); 1528 TextUtils.getChars(text, start, end, buf, 0); 1529 } 1530 1531 float h = 0; 1532 1533 int target = trailing ? offset - 1 : offset; 1534 if (target < start) { 1535 return 0; 1536 } 1537 1538 int[] runs = directions.mDirections; 1539 for (int i = 0; i < runs.length; i += 2) { 1540 int here = start + runs[i]; 1541 int there = here + (runs[i+1] & RUN_LENGTH_MASK); 1542 if (there > end) { 1543 there = end; 1544 } 1545 boolean runIsRtl = (runs[i+1] & RUN_RTL_FLAG) != 0; 1546 1547 int segstart = here; 1548 for (int j = hasTabs ? here : there; j <= there; j++) { 1549 int codept = 0; 1550 Bitmap bm = null; 1551 1552 if (hasTabs && j < there) { 1553 codept = buf[j - start]; 1554 } 1555 1556 if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) { 1557 codept = Character.codePointAt(buf, j - start); 1558 1559 if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { 1560 bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); 1561 } 1562 } 1563 1564 if (j == there || codept == '\t' || bm != null) { 1565 float segw; 1566 1567 boolean inSegment = target >= segstart && target < j; 1568 1569 if (inSegment) { 1570 if (dir == DIR_LEFT_TO_RIGHT && !runIsRtl) { 1571 h += Styled.measureText(paint, workPaint, text, 1572 segstart, offset, 1573 null); 1574 return h; 1575 } 1576 1577 if (dir == DIR_RIGHT_TO_LEFT && runIsRtl) { 1578 h -= Styled.measureText(paint, workPaint, text, 1579 segstart, offset, 1580 null); 1581 return h; 1582 } 1583 } 1584 1585 // XXX Style.measureText assumes LTR? 1586 segw = Styled.measureText(paint, workPaint, text, 1587 segstart, j, 1588 null); 1589 1590 if (inSegment) { 1591 if (dir == DIR_LEFT_TO_RIGHT) { 1592 h += segw - Styled.measureText(paint, workPaint, 1593 text, 1594 segstart, 1595 offset, null); 1596 return h; 1597 } 1598 1599 if (dir == DIR_RIGHT_TO_LEFT) { 1600 h -= segw - Styled.measureText(paint, workPaint, 1601 text, 1602 segstart, 1603 offset, null); 1604 return h; 1605 } 1606 } 1607 1608 if (dir == DIR_RIGHT_TO_LEFT) 1609 h -= segw; 1610 else 1611 h += segw; 1612 1613 if (j != there && buf[j - start] == '\t') { 1614 if (offset == j) 1615 return h; 1616 1617 h = dir * nextTab(text, start, end, h * dir, tabs); 1618 1619 if (target == j) { 1620 return h; 1621 } 1622 } 1623 1624 if (bm != null) { 1625 workPaint.set(paint); 1626 Styled.measureText(paint, workPaint, text, 1627 j, j + 2, null); 1628 1629 float wid = bm.getWidth() * 1630 -workPaint.ascent() / bm.getHeight(); 1631 1632 if (dir == DIR_RIGHT_TO_LEFT) { 1633 h -= wid; 1634 } else { 1635 h += wid; 1636 } 1637 1638 j++; 1639 } 1640 1641 segstart = j + 1; 1642 } 1643 } 1644 } 1645 1646 if (hasTabs) 1647 TextUtils.recycle(buf); 1648 1649 return h; 1650 } 1651 1652 /** 1653 * Measure width of a run of text on a single line that is known to all be 1654 * in the same direction as the paragraph base direction. Returns the width, 1655 * and the line metrics in fm if fm is not null. 1656 * 1657 * @param paint the paint for the text; will not be modified 1658 * @param workPaint paint available for modification 1659 * @param text text 1660 * @param start start of the line 1661 * @param end limit of the line 1662 * @param fm object to return integer metrics in, can be null 1663 * @param hasTabs true if it is known that the line has tabs 1664 * @param tabs tab position information 1665 * @return the width of the text from start to end 1666 */ 1667 /* package */ static float measureText(TextPaint paint, 1668 TextPaint workPaint, 1669 CharSequence text, 1670 int start, int end, 1671 Paint.FontMetricsInt fm, 1672 boolean hasTabs, Object[] tabs) { 1673 char[] buf = null; 1674 1675 if (hasTabs) { 1676 buf = TextUtils.obtain(end - start); 1677 TextUtils.getChars(text, start, end, buf, 0); 1678 } 1679 1680 int len = end - start; 1681 1682 int lastPos = 0; 1683 float width = 0; 1684 int ascent = 0, descent = 0, top = 0, bottom = 0; 1685 1686 if (fm != null) { 1687 fm.ascent = 0; 1688 fm.descent = 0; 1689 } 1690 1691 for (int pos = hasTabs ? 0 : len; pos <= len; pos++) { 1692 int codept = 0; 1693 Bitmap bm = null; 1694 1695 if (hasTabs && pos < len) { 1696 codept = buf[pos]; 1697 } 1698 1699 if (codept >= 0xD800 && codept <= 0xDFFF && pos < len) { 1700 codept = Character.codePointAt(buf, pos); 1701 1702 if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { 1703 bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); 1704 } 1705 } 1706 1707 if (pos == len || codept == '\t' || bm != null) { 1708 workPaint.baselineShift = 0; 1709 1710 // XXX Styled.measureText assumes the run direction is LTR, 1711 // but it might not be. Check this. 1712 width += Styled.measureText(paint, workPaint, text, 1713 start + lastPos, start + pos, 1714 fm); 1715 1716 if (fm != null) { 1717 if (workPaint.baselineShift < 0) { 1718 fm.ascent += workPaint.baselineShift; 1719 fm.top += workPaint.baselineShift; 1720 } else { 1721 fm.descent += workPaint.baselineShift; 1722 fm.bottom += workPaint.baselineShift; 1723 } 1724 } 1725 1726 if (pos != len) { 1727 if (bm == null) { 1728 // no emoji, must have hit a tab 1729 width = nextTab(text, start, end, width, tabs); 1730 } else { 1731 // This sets up workPaint with the font on the emoji 1732 // text, so that we can extract the ascent and scale. 1733 1734 // We can't use the result of the previous call to 1735 // measureText because the emoji might have its own style. 1736 // We have to initialize workPaint here because if the 1737 // text is unstyled measureText might not use workPaint 1738 // at all. 1739 workPaint.set(paint); 1740 Styled.measureText(paint, workPaint, text, 1741 start + pos, start + pos + 1, null); 1742 1743 width += bm.getWidth() * 1744 -workPaint.ascent() / bm.getHeight(); 1745 1746 // Since we had an emoji, we bump past the second half 1747 // of the surrogate pair. 1748 pos++; 1749 } 1750 } 1751 1752 if (fm != null) { 1753 if (fm.ascent < ascent) { 1754 ascent = fm.ascent; 1755 } 1756 if (fm.descent > descent) { 1757 descent = fm.descent; 1758 } 1759 1760 if (fm.top < top) { 1761 top = fm.top; 1762 } 1763 if (fm.bottom > bottom) { 1764 bottom = fm.bottom; 1765 } 1766 1767 // No need to take bitmap height into account here, 1768 // since it is scaled to match the text height. 1769 } 1770 1771 lastPos = pos + 1; 1772 } 1773 } 1774 1775 if (fm != null) { 1776 fm.ascent = ascent; 1777 fm.descent = descent; 1778 fm.top = top; 1779 fm.bottom = bottom; 1780 } 1781 1782 if (hasTabs) 1783 TextUtils.recycle(buf); 1784 1785 return width; 1786 } 1787 1788 /** 1789 * Returns the position of the next tab stop after h on the line. 1790 * 1791 * @param text the text 1792 * @param start start of the line 1793 * @param end limit of the line 1794 * @param h the current horizontal offset 1795 * @param tabs the tabs, can be null. If it is null, any tabs in effect 1796 * on the line will be used. If there are no tabs, a default offset 1797 * will be used to compute the tab stop. 1798 * @return the offset of the next tab stop. 1799 */ 1800 /* package */ static float nextTab(CharSequence text, int start, int end, 1801 float h, Object[] tabs) { 1802 float nh = Float.MAX_VALUE; 1803 boolean alltabs = false; 1804 1805 if (text instanceof Spanned) { 1806 if (tabs == null) { 1807 tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class); 1808 alltabs = true; 1809 } 1810 1811 for (int i = 0; i < tabs.length; i++) { 1812 if (!alltabs) { 1813 if (!(tabs[i] instanceof TabStopSpan)) 1814 continue; 1815 } 1816 1817 int where = ((TabStopSpan) tabs[i]).getTabStop(); 1818 1819 if (where < nh && where > h) 1820 nh = where; 1821 } 1822 1823 if (nh != Float.MAX_VALUE) 1824 return nh; 1825 } 1826 1827 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; 1828 } 1829 1830 protected final boolean isSpanned() { 1831 return mSpannedText; 1832 } 1833 1834 private void ellipsize(int start, int end, int line, 1835 char[] dest, int destoff) { 1836 int ellipsisCount = getEllipsisCount(line); 1837 1838 if (ellipsisCount == 0) { 1839 return; 1840 } 1841 1842 int ellipsisStart = getEllipsisStart(line); 1843 int linestart = getLineStart(line); 1844 1845 for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { 1846 char c; 1847 1848 if (i == ellipsisStart) { 1849 c = '\u2026'; // ellipsis 1850 } else { 1851 c = '\uFEFF'; // 0-width space 1852 } 1853 1854 int a = i + linestart; 1855 1856 if (a >= start && a < end) { 1857 dest[destoff + a - start] = c; 1858 } 1859 } 1860 } 1861 1862 /** 1863 * Stores information about bidirectional (left-to-right or right-to-left) 1864 * text within the layout of a line. 1865 */ 1866 public static class Directions { 1867 // Directions represents directional runs within a line of text. 1868 // Runs are pairs of ints listed in visual order, starting from the 1869 // leading margin. The first int of each pair is the offset from 1870 // the first character of the line to the start of the run. The 1871 // second int represents both the length and level of the run. 1872 // The length is in the lower bits, accessed by masking with 1873 // DIR_LENGTH_MASK. The level is in the higher bits, accessed 1874 // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. 1875 // To simply test for an RTL direction, test the bit using 1876 // DIR_RTL_FLAG, if set then the direction is rtl. 1877 1878 /* package */ int[] mDirections; 1879 /* package */ Directions(int[] dirs) { 1880 mDirections = dirs; 1881 } 1882 } 1883 1884 /** 1885 * Return the offset of the first character to be ellipsized away, 1886 * relative to the start of the line. (So 0 if the beginning of the 1887 * line is ellipsized, not getLineStart().) 1888 */ 1889 public abstract int getEllipsisStart(int line); 1890 /** 1891 * Returns the number of characters to be ellipsized away, or 0 if 1892 * no ellipsis is to take place. 1893 */ 1894 public abstract int getEllipsisCount(int line); 1895 1896 /* package */ static class Ellipsizer implements CharSequence, GetChars { 1897 /* package */ CharSequence mText; 1898 /* package */ Layout mLayout; 1899 /* package */ int mWidth; 1900 /* package */ TextUtils.TruncateAt mMethod; 1901 1902 public Ellipsizer(CharSequence s) { 1903 mText = s; 1904 } 1905 1906 public char charAt(int off) { 1907 char[] buf = TextUtils.obtain(1); 1908 getChars(off, off + 1, buf, 0); 1909 char ret = buf[0]; 1910 1911 TextUtils.recycle(buf); 1912 return ret; 1913 } 1914 1915 public void getChars(int start, int end, char[] dest, int destoff) { 1916 int line1 = mLayout.getLineForOffset(start); 1917 int line2 = mLayout.getLineForOffset(end); 1918 1919 TextUtils.getChars(mText, start, end, dest, destoff); 1920 1921 for (int i = line1; i <= line2; i++) { 1922 mLayout.ellipsize(start, end, i, dest, destoff); 1923 } 1924 } 1925 1926 public int length() { 1927 return mText.length(); 1928 } 1929 1930 public CharSequence subSequence(int start, int end) { 1931 char[] s = new char[end - start]; 1932 getChars(start, end, s, 0); 1933 return new String(s); 1934 } 1935 1936 public String toString() { 1937 char[] s = new char[length()]; 1938 getChars(0, length(), s, 0); 1939 return new String(s); 1940 } 1941 1942 } 1943 1944 /* package */ static class SpannedEllipsizer 1945 extends Ellipsizer implements Spanned { 1946 private Spanned mSpanned; 1947 1948 public SpannedEllipsizer(CharSequence display) { 1949 super(display); 1950 mSpanned = (Spanned) display; 1951 } 1952 1953 public <T> T[] getSpans(int start, int end, Class<T> type) { 1954 return mSpanned.getSpans(start, end, type); 1955 } 1956 1957 public int getSpanStart(Object tag) { 1958 return mSpanned.getSpanStart(tag); 1959 } 1960 1961 public int getSpanEnd(Object tag) { 1962 return mSpanned.getSpanEnd(tag); 1963 } 1964 1965 public int getSpanFlags(Object tag) { 1966 return mSpanned.getSpanFlags(tag); 1967 } 1968 1969 public int nextSpanTransition(int start, int limit, Class type) { 1970 return mSpanned.nextSpanTransition(start, limit, type); 1971 } 1972 1973 public CharSequence subSequence(int start, int end) { 1974 char[] s = new char[end - start]; 1975 getChars(start, end, s, 0); 1976 1977 SpannableString ss = new SpannableString(new String(s)); 1978 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); 1979 return ss; 1980 } 1981 } 1982 1983 private CharSequence mText; 1984 private TextPaint mPaint; 1985 /* package */ TextPaint mWorkPaint; 1986 private int mWidth; 1987 private Alignment mAlignment = Alignment.ALIGN_NORMAL; 1988 private float mSpacingMult; 1989 private float mSpacingAdd; 1990 private static Rect sTempRect = new Rect(); 1991 private boolean mSpannedText; 1992 1993 public static final int DIR_LEFT_TO_RIGHT = 1; 1994 public static final int DIR_RIGHT_TO_LEFT = -1; 1995 1996 /* package */ static final int DIR_REQUEST_LTR = 1; 1997 /* package */ static final int DIR_REQUEST_RTL = -1; 1998 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; 1999 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; 2000 2001 /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; 2002 /* package */ static final int RUN_LEVEL_SHIFT = 26; 2003 /* package */ static final int RUN_LEVEL_MASK = 0x3f; 2004 /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; 2005 2006 public enum Alignment { 2007 ALIGN_NORMAL, 2008 ALIGN_OPPOSITE, 2009 ALIGN_CENTER, 2010 // XXX ALIGN_LEFT, 2011 // XXX ALIGN_RIGHT, 2012 } 2013 2014 private static final int TAB_INCREMENT = 20; 2015 2016 /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = 2017 new Directions(new int[] { 0, RUN_LENGTH_MASK }); 2018 /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = 2019 new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); 2020} 2021 2022