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