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