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