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 if (align == Alignment.ALIGN_LEFT) { 479 align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE; 480 } else if (align == Alignment.ALIGN_RIGHT) { 481 align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL; 482 } 483 484 int x; 485 if (align == Alignment.ALIGN_NORMAL) { 486 if (dir == DIR_LEFT_TO_RIGHT) { 487 x = left; 488 } else { 489 x = right; 490 } 491 } else { 492 TabStops tabStops = null; 493 if (mSpannedText && getLineContainsTab(line)) { 494 Spanned spanned = (Spanned) mText; 495 int start = getLineStart(line); 496 int spanEnd = spanned.nextSpanTransition(start, spanned.length(), 497 TabStopSpan.class); 498 TabStopSpan[] tabSpans = getParagraphSpans(spanned, start, spanEnd, 499 TabStopSpan.class); 500 if (tabSpans.length > 0) { 501 tabStops = new TabStops(TAB_INCREMENT, tabSpans); 502 } 503 } 504 int max = (int)getLineExtent(line, tabStops, false); 505 if (align == Alignment.ALIGN_OPPOSITE) { 506 if (dir == DIR_LEFT_TO_RIGHT) { 507 x = right - max; 508 } else { 509 // max is negative here 510 x = left - max; 511 } 512 } else { // Alignment.ALIGN_CENTER 513 max = max & ~1; 514 x = (left + right - max) >> 1; 515 } 516 } 517 return x; 518 } 519 520 /** 521 * Return the text that is displayed by this Layout. 522 */ getText()523 public final CharSequence getText() { 524 return mText; 525 } 526 527 /** 528 * Return the base Paint properties for this layout. 529 * Do NOT change the paint, which may result in funny 530 * drawing for this layout. 531 */ getPaint()532 public final TextPaint getPaint() { 533 return mPaint; 534 } 535 536 /** 537 * Return the width of this layout. 538 */ getWidth()539 public final int getWidth() { 540 return mWidth; 541 } 542 543 /** 544 * Return the width to which this Layout is ellipsizing, or 545 * {@link #getWidth} if it is not doing anything special. 546 */ getEllipsizedWidth()547 public int getEllipsizedWidth() { 548 return mWidth; 549 } 550 551 /** 552 * Increase the width of this layout to the specified width. 553 * Be careful to use this only when you know it is appropriate— 554 * it does not cause the text to reflow to use the full new width. 555 */ increaseWidthTo(int wid)556 public final void increaseWidthTo(int wid) { 557 if (wid < mWidth) { 558 throw new RuntimeException("attempted to reduce Layout width"); 559 } 560 561 mWidth = wid; 562 } 563 564 /** 565 * Return the total height of this layout. 566 */ getHeight()567 public int getHeight() { 568 return getLineTop(getLineCount()); 569 } 570 571 /** 572 * Return the base alignment of this layout. 573 */ getAlignment()574 public final Alignment getAlignment() { 575 return mAlignment; 576 } 577 578 /** 579 * Return what the text height is multiplied by to get the line height. 580 */ getSpacingMultiplier()581 public final float getSpacingMultiplier() { 582 return mSpacingMult; 583 } 584 585 /** 586 * Return the number of units of leading that are added to each line. 587 */ getSpacingAdd()588 public final float getSpacingAdd() { 589 return mSpacingAdd; 590 } 591 592 /** 593 * Return the heuristic used to determine paragraph text direction. 594 * @hide 595 */ getTextDirectionHeuristic()596 public final TextDirectionHeuristic getTextDirectionHeuristic() { 597 return mTextDir; 598 } 599 600 /** 601 * Return the number of lines of text in this layout. 602 */ 603 public abstract int getLineCount(); 604 605 /** 606 * Return the baseline for the specified line (0…getLineCount() - 1) 607 * If bounds is not null, return the top, left, right, bottom extents 608 * of the specified line in it. 609 * @param line which line to examine (0..getLineCount() - 1) 610 * @param bounds Optional. If not null, it returns the extent of the line 611 * @return the Y-coordinate of the baseline 612 */ getLineBounds(int line, Rect bounds)613 public int getLineBounds(int line, Rect bounds) { 614 if (bounds != null) { 615 bounds.left = 0; // ??? 616 bounds.top = getLineTop(line); 617 bounds.right = mWidth; // ??? 618 bounds.bottom = getLineTop(line + 1); 619 } 620 return getLineBaseline(line); 621 } 622 623 /** 624 * Return the vertical position of the top of the specified line 625 * (0…getLineCount()). 626 * If the specified line is equal to the line count, returns the 627 * bottom of the last line. 628 */ 629 public abstract int getLineTop(int line); 630 631 /** 632 * Return the descent of the specified line(0…getLineCount() - 1). 633 */ 634 public abstract int getLineDescent(int line); 635 636 /** 637 * Return the text offset of the beginning of the specified line ( 638 * 0…getLineCount()). If the specified line is equal to the line 639 * count, returns the length of the text. 640 */ 641 public abstract int getLineStart(int line); 642 643 /** 644 * Returns the primary directionality of the paragraph containing the 645 * specified line, either 1 for left-to-right lines, or -1 for right-to-left 646 * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}). 647 */ 648 public abstract int getParagraphDirection(int line); 649 650 /** 651 * Returns whether the specified line contains one or more 652 * characters that need to be handled specially, like tabs 653 * or emoji. 654 */ 655 public abstract boolean getLineContainsTab(int line); 656 657 /** 658 * Returns the directional run information for the specified line. 659 * The array alternates counts of characters in left-to-right 660 * and right-to-left segments of the line. 661 * 662 * <p>NOTE: this is inadequate to support bidirectional text, and will change. 663 */ 664 public abstract Directions getLineDirections(int line); 665 666 /** 667 * Returns the (negative) number of extra pixels of ascent padding in the 668 * top line of the Layout. 669 */ 670 public abstract int getTopPadding(); 671 672 /** 673 * Returns the number of extra pixels of descent padding in the 674 * bottom line of the Layout. 675 */ 676 public abstract int getBottomPadding(); 677 678 679 /** 680 * Returns true if the character at offset and the preceding character 681 * are at different run levels (and thus there's a split caret). 682 * @param offset the offset 683 * @return true if at a level boundary 684 * @hide 685 */ isLevelBoundary(int offset)686 public boolean isLevelBoundary(int offset) { 687 int line = getLineForOffset(offset); 688 Directions dirs = getLineDirections(line); 689 if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) { 690 return false; 691 } 692 693 int[] runs = dirs.mDirections; 694 int lineStart = getLineStart(line); 695 int lineEnd = getLineEnd(line); 696 if (offset == lineStart || offset == lineEnd) { 697 int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1; 698 int runIndex = offset == lineStart ? 0 : runs.length - 2; 699 return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel; 700 } 701 702 offset -= lineStart; 703 for (int i = 0; i < runs.length; i += 2) { 704 if (offset == runs[i]) { 705 return true; 706 } 707 } 708 return false; 709 } 710 711 /** 712 * Returns true if the character at offset is right to left (RTL). 713 * @param offset the offset 714 * @return true if the character is RTL, false if it is LTR 715 */ isRtlCharAt(int offset)716 public boolean isRtlCharAt(int offset) { 717 int line = getLineForOffset(offset); 718 Directions dirs = getLineDirections(line); 719 if (dirs == DIRS_ALL_LEFT_TO_RIGHT) { 720 return false; 721 } 722 if (dirs == DIRS_ALL_RIGHT_TO_LEFT) { 723 return true; 724 } 725 int[] runs = dirs.mDirections; 726 int lineStart = getLineStart(line); 727 for (int i = 0; i < runs.length; i += 2) { 728 int start = lineStart + (runs[i] & RUN_LENGTH_MASK); 729 // No need to test the end as an offset after the last run should return the value 730 // corresponding of the last run 731 if (offset >= start) { 732 int level = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 733 return ((level & 1) != 0); 734 } 735 } 736 // Should happen only if the offset is "out of bounds" 737 return false; 738 } 739 primaryIsTrailingPrevious(int offset)740 private boolean primaryIsTrailingPrevious(int offset) { 741 int line = getLineForOffset(offset); 742 int lineStart = getLineStart(line); 743 int lineEnd = getLineEnd(line); 744 int[] runs = getLineDirections(line).mDirections; 745 746 int levelAt = -1; 747 for (int i = 0; i < runs.length; i += 2) { 748 int start = lineStart + runs[i]; 749 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 750 if (limit > lineEnd) { 751 limit = lineEnd; 752 } 753 if (offset >= start && offset < limit) { 754 if (offset > start) { 755 // Previous character is at same level, so don't use trailing. 756 return false; 757 } 758 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 759 break; 760 } 761 } 762 if (levelAt == -1) { 763 // Offset was limit of line. 764 levelAt = getParagraphDirection(line) == 1 ? 0 : 1; 765 } 766 767 // At level boundary, check previous level. 768 int levelBefore = -1; 769 if (offset == lineStart) { 770 levelBefore = getParagraphDirection(line) == 1 ? 0 : 1; 771 } else { 772 offset -= 1; 773 for (int i = 0; i < runs.length; i += 2) { 774 int start = lineStart + runs[i]; 775 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 776 if (limit > lineEnd) { 777 limit = lineEnd; 778 } 779 if (offset >= start && offset < limit) { 780 levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 781 break; 782 } 783 } 784 } 785 786 return levelBefore < levelAt; 787 } 788 789 /** 790 * Get the primary horizontal position for the specified text offset. 791 * This is the location where a new character would be inserted in 792 * the paragraph's primary direction. 793 */ getPrimaryHorizontal(int offset)794 public float getPrimaryHorizontal(int offset) { 795 return getPrimaryHorizontal(offset, false /* not clamped */); 796 } 797 798 /** 799 * Get the primary horizontal position for the specified text offset, but 800 * optionally clamp it so that it doesn't exceed the width of the layout. 801 * @hide 802 */ getPrimaryHorizontal(int offset, boolean clamped)803 public float getPrimaryHorizontal(int offset, boolean clamped) { 804 boolean trailing = primaryIsTrailingPrevious(offset); 805 return getHorizontal(offset, trailing, clamped); 806 } 807 808 /** 809 * Get the secondary horizontal position for the specified text offset. 810 * This is the location where a new character would be inserted in 811 * the direction other than the paragraph's primary direction. 812 */ getSecondaryHorizontal(int offset)813 public float getSecondaryHorizontal(int offset) { 814 return getSecondaryHorizontal(offset, false /* not clamped */); 815 } 816 817 /** 818 * Get the secondary horizontal position for the specified text offset, but 819 * optionally clamp it so that it doesn't exceed the width of the layout. 820 * @hide 821 */ getSecondaryHorizontal(int offset, boolean clamped)822 public float getSecondaryHorizontal(int offset, boolean clamped) { 823 boolean trailing = primaryIsTrailingPrevious(offset); 824 return getHorizontal(offset, !trailing, clamped); 825 } 826 getHorizontal(int offset, boolean trailing, boolean clamped)827 private float getHorizontal(int offset, boolean trailing, boolean clamped) { 828 int line = getLineForOffset(offset); 829 830 return getHorizontal(offset, trailing, line, clamped); 831 } 832 getHorizontal(int offset, boolean trailing, int line, boolean clamped)833 private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) { 834 int start = getLineStart(line); 835 int end = getLineEnd(line); 836 int dir = getParagraphDirection(line); 837 boolean hasTabOrEmoji = getLineContainsTab(line); 838 Directions directions = getLineDirections(line); 839 840 TabStops tabStops = null; 841 if (hasTabOrEmoji && mText instanceof Spanned) { 842 // Just checking this line should be good enough, tabs should be 843 // consistent across all lines in a paragraph. 844 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 845 if (tabs.length > 0) { 846 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 847 } 848 } 849 850 TextLine tl = TextLine.obtain(); 851 tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops); 852 float wid = tl.measure(offset - start, trailing, null); 853 TextLine.recycle(tl); 854 855 if (clamped && wid > mWidth) { 856 wid = mWidth; 857 } 858 int left = getParagraphLeft(line); 859 int right = getParagraphRight(line); 860 861 return getLineStartPos(line, left, right) + wid; 862 } 863 864 /** 865 * Get the leftmost position that should be exposed for horizontal 866 * scrolling on the specified line. 867 */ getLineLeft(int line)868 public float getLineLeft(int line) { 869 int dir = getParagraphDirection(line); 870 Alignment align = getParagraphAlignment(line); 871 872 if (align == Alignment.ALIGN_LEFT) { 873 return 0; 874 } else if (align == Alignment.ALIGN_NORMAL) { 875 if (dir == DIR_RIGHT_TO_LEFT) 876 return getParagraphRight(line) - getLineMax(line); 877 else 878 return 0; 879 } else if (align == Alignment.ALIGN_RIGHT) { 880 return mWidth - getLineMax(line); 881 } else if (align == Alignment.ALIGN_OPPOSITE) { 882 if (dir == DIR_RIGHT_TO_LEFT) 883 return 0; 884 else 885 return mWidth - getLineMax(line); 886 } else { /* align == Alignment.ALIGN_CENTER */ 887 int left = getParagraphLeft(line); 888 int right = getParagraphRight(line); 889 int max = ((int) getLineMax(line)) & ~1; 890 891 return left + ((right - left) - max) / 2; 892 } 893 } 894 895 /** 896 * Get the rightmost position that should be exposed for horizontal 897 * scrolling on the specified line. 898 */ getLineRight(int line)899 public float getLineRight(int line) { 900 int dir = getParagraphDirection(line); 901 Alignment align = getParagraphAlignment(line); 902 903 if (align == Alignment.ALIGN_LEFT) { 904 return getParagraphLeft(line) + getLineMax(line); 905 } else if (align == Alignment.ALIGN_NORMAL) { 906 if (dir == DIR_RIGHT_TO_LEFT) 907 return mWidth; 908 else 909 return getParagraphLeft(line) + getLineMax(line); 910 } else if (align == Alignment.ALIGN_RIGHT) { 911 return mWidth; 912 } else if (align == Alignment.ALIGN_OPPOSITE) { 913 if (dir == DIR_RIGHT_TO_LEFT) 914 return getLineMax(line); 915 else 916 return mWidth; 917 } else { /* align == Alignment.ALIGN_CENTER */ 918 int left = getParagraphLeft(line); 919 int right = getParagraphRight(line); 920 int max = ((int) getLineMax(line)) & ~1; 921 922 return right - ((right - left) - max) / 2; 923 } 924 } 925 926 /** 927 * Gets the unsigned horizontal extent of the specified line, including 928 * leading margin indent, but excluding trailing whitespace. 929 */ getLineMax(int line)930 public float getLineMax(int line) { 931 float margin = getParagraphLeadingMargin(line); 932 float signedExtent = getLineExtent(line, false); 933 return margin + signedExtent >= 0 ? signedExtent : -signedExtent; 934 } 935 936 /** 937 * Gets the unsigned horizontal extent of the specified line, including 938 * leading margin indent and trailing whitespace. 939 */ getLineWidth(int line)940 public float getLineWidth(int line) { 941 float margin = getParagraphLeadingMargin(line); 942 float signedExtent = getLineExtent(line, true); 943 return margin + signedExtent >= 0 ? signedExtent : -signedExtent; 944 } 945 946 /** 947 * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the 948 * tab stops instead of using the ones passed in. 949 * @param line the index of the line 950 * @param full whether to include trailing whitespace 951 * @return the extent of the line 952 */ getLineExtent(int line, boolean full)953 private float getLineExtent(int line, boolean full) { 954 int start = getLineStart(line); 955 int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 956 957 boolean hasTabsOrEmoji = getLineContainsTab(line); 958 TabStops tabStops = null; 959 if (hasTabsOrEmoji && mText instanceof Spanned) { 960 // Just checking this line should be good enough, tabs should be 961 // consistent across all lines in a paragraph. 962 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 963 if (tabs.length > 0) { 964 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 965 } 966 } 967 Directions directions = getLineDirections(line); 968 // Returned directions can actually be null 969 if (directions == null) { 970 return 0f; 971 } 972 int dir = getParagraphDirection(line); 973 974 TextLine tl = TextLine.obtain(); 975 tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); 976 float width = tl.metrics(null); 977 TextLine.recycle(tl); 978 return width; 979 } 980 981 /** 982 * Returns the signed horizontal extent of the specified line, excluding 983 * leading margin. If full is false, excludes trailing whitespace. 984 * @param line the index of the line 985 * @param tabStops the tab stops, can be null if we know they're not used. 986 * @param full whether to include trailing whitespace 987 * @return the extent of the text on this line 988 */ getLineExtent(int line, TabStops tabStops, boolean full)989 private float getLineExtent(int line, TabStops tabStops, boolean full) { 990 int start = getLineStart(line); 991 int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 992 boolean hasTabsOrEmoji = getLineContainsTab(line); 993 Directions directions = getLineDirections(line); 994 int dir = getParagraphDirection(line); 995 996 TextLine tl = TextLine.obtain(); 997 tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); 998 float width = tl.metrics(null); 999 TextLine.recycle(tl); 1000 return width; 1001 } 1002 1003 /** 1004 * Get the line number corresponding to the specified vertical position. 1005 * If you ask for a position above 0, you get 0; if you ask for a position 1006 * below the bottom of the text, you get the last line. 1007 */ 1008 // FIXME: It may be faster to do a linear search for layouts without many lines. getLineForVertical(int vertical)1009 public int getLineForVertical(int vertical) { 1010 int high = getLineCount(), low = -1, guess; 1011 1012 while (high - low > 1) { 1013 guess = (high + low) / 2; 1014 1015 if (getLineTop(guess) > vertical) 1016 high = guess; 1017 else 1018 low = guess; 1019 } 1020 1021 if (low < 0) 1022 return 0; 1023 else 1024 return low; 1025 } 1026 1027 /** 1028 * Get the line number on which the specified text offset appears. 1029 * If you ask for a position before 0, you get 0; if you ask for a position 1030 * beyond the end of the text, you get the last line. 1031 */ getLineForOffset(int offset)1032 public int getLineForOffset(int offset) { 1033 int high = getLineCount(), low = -1, guess; 1034 1035 while (high - low > 1) { 1036 guess = (high + low) / 2; 1037 1038 if (getLineStart(guess) > offset) 1039 high = guess; 1040 else 1041 low = guess; 1042 } 1043 1044 if (low < 0) 1045 return 0; 1046 else 1047 return low; 1048 } 1049 1050 /** 1051 * Get the character offset on the specified line whose position is 1052 * closest to the specified horizontal position. 1053 */ getOffsetForHorizontal(int line, float horiz)1054 public int getOffsetForHorizontal(int line, float horiz) { 1055 int max = getLineEnd(line) - 1; 1056 int min = getLineStart(line); 1057 Directions dirs = getLineDirections(line); 1058 1059 if (line == getLineCount() - 1) 1060 max++; 1061 1062 int best = min; 1063 float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); 1064 1065 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1066 int here = min + dirs.mDirections[i]; 1067 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1068 int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1; 1069 1070 if (there > max) 1071 there = max; 1072 int high = there - 1 + 1, low = here + 1 - 1, guess; 1073 1074 while (high - low > 1) { 1075 guess = (high + low) / 2; 1076 int adguess = getOffsetAtStartOf(guess); 1077 1078 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap) 1079 high = guess; 1080 else 1081 low = guess; 1082 } 1083 1084 if (low < here + 1) 1085 low = here + 1; 1086 1087 if (low < there) { 1088 low = getOffsetAtStartOf(low); 1089 1090 float dist = Math.abs(getPrimaryHorizontal(low) - horiz); 1091 1092 int aft = TextUtils.getOffsetAfter(mText, low); 1093 if (aft < there) { 1094 float other = Math.abs(getPrimaryHorizontal(aft) - horiz); 1095 1096 if (other < dist) { 1097 dist = other; 1098 low = aft; 1099 } 1100 } 1101 1102 if (dist < bestdist) { 1103 bestdist = dist; 1104 best = low; 1105 } 1106 } 1107 1108 float dist = Math.abs(getPrimaryHorizontal(here) - horiz); 1109 1110 if (dist < bestdist) { 1111 bestdist = dist; 1112 best = here; 1113 } 1114 } 1115 1116 float dist = Math.abs(getPrimaryHorizontal(max) - horiz); 1117 1118 if (dist <= bestdist) { 1119 bestdist = dist; 1120 best = max; 1121 } 1122 1123 return best; 1124 } 1125 1126 /** 1127 * Return the text offset after the last character on the specified line. 1128 */ getLineEnd(int line)1129 public final int getLineEnd(int line) { 1130 return getLineStart(line + 1); 1131 } 1132 1133 /** 1134 * Return the text offset after the last visible character (so whitespace 1135 * is not counted) on the specified line. 1136 */ getLineVisibleEnd(int line)1137 public int getLineVisibleEnd(int line) { 1138 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 1139 } 1140 getLineVisibleEnd(int line, int start, int end)1141 private int getLineVisibleEnd(int line, int start, int end) { 1142 CharSequence text = mText; 1143 char ch; 1144 if (line == getLineCount() - 1) { 1145 return end; 1146 } 1147 1148 for (; end > start; end--) { 1149 ch = text.charAt(end - 1); 1150 1151 if (ch == '\n') { 1152 return end - 1; 1153 } 1154 1155 if (ch != ' ' && ch != '\t') { 1156 break; 1157 } 1158 1159 } 1160 1161 return end; 1162 } 1163 1164 /** 1165 * Return the vertical position of the bottom of the specified line. 1166 */ getLineBottom(int line)1167 public final int getLineBottom(int line) { 1168 return getLineTop(line + 1); 1169 } 1170 1171 /** 1172 * Return the vertical position of the baseline of the specified line. 1173 */ getLineBaseline(int line)1174 public final int getLineBaseline(int line) { 1175 // getLineTop(line+1) == getLineTop(line) 1176 return getLineTop(line+1) - getLineDescent(line); 1177 } 1178 1179 /** 1180 * Get the ascent of the text on the specified line. 1181 * The return value is negative to match the Paint.ascent() convention. 1182 */ getLineAscent(int line)1183 public final int getLineAscent(int line) { 1184 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) 1185 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); 1186 } 1187 getOffsetToLeftOf(int offset)1188 public int getOffsetToLeftOf(int offset) { 1189 return getOffsetToLeftRightOf(offset, true); 1190 } 1191 getOffsetToRightOf(int offset)1192 public int getOffsetToRightOf(int offset) { 1193 return getOffsetToLeftRightOf(offset, false); 1194 } 1195 getOffsetToLeftRightOf(int caret, boolean toLeft)1196 private int getOffsetToLeftRightOf(int caret, boolean toLeft) { 1197 int line = getLineForOffset(caret); 1198 int lineStart = getLineStart(line); 1199 int lineEnd = getLineEnd(line); 1200 int lineDir = getParagraphDirection(line); 1201 1202 boolean lineChanged = false; 1203 boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); 1204 // if walking off line, look at the line we're headed to 1205 if (advance) { 1206 if (caret == lineEnd) { 1207 if (line < getLineCount() - 1) { 1208 lineChanged = true; 1209 ++line; 1210 } else { 1211 return caret; // at very end, don't move 1212 } 1213 } 1214 } else { 1215 if (caret == lineStart) { 1216 if (line > 0) { 1217 lineChanged = true; 1218 --line; 1219 } else { 1220 return caret; // at very start, don't move 1221 } 1222 } 1223 } 1224 1225 if (lineChanged) { 1226 lineStart = getLineStart(line); 1227 lineEnd = getLineEnd(line); 1228 int newDir = getParagraphDirection(line); 1229 if (newDir != lineDir) { 1230 // unusual case. we want to walk onto the line, but it runs 1231 // in a different direction than this one, so we fake movement 1232 // in the opposite direction. 1233 toLeft = !toLeft; 1234 lineDir = newDir; 1235 } 1236 } 1237 1238 Directions directions = getLineDirections(line); 1239 1240 TextLine tl = TextLine.obtain(); 1241 // XXX: we don't care about tabs 1242 tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null); 1243 caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); 1244 tl = TextLine.recycle(tl); 1245 return caret; 1246 } 1247 getOffsetAtStartOf(int offset)1248 private int getOffsetAtStartOf(int offset) { 1249 // XXX this probably should skip local reorderings and 1250 // zero-width characters, look at callers 1251 if (offset == 0) 1252 return 0; 1253 1254 CharSequence text = mText; 1255 char c = text.charAt(offset); 1256 1257 if (c >= '\uDC00' && c <= '\uDFFF') { 1258 char c1 = text.charAt(offset - 1); 1259 1260 if (c1 >= '\uD800' && c1 <= '\uDBFF') 1261 offset -= 1; 1262 } 1263 1264 if (mSpannedText) { 1265 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 1266 ReplacementSpan.class); 1267 1268 for (int i = 0; i < spans.length; i++) { 1269 int start = ((Spanned) text).getSpanStart(spans[i]); 1270 int end = ((Spanned) text).getSpanEnd(spans[i]); 1271 1272 if (start < offset && end > offset) 1273 offset = start; 1274 } 1275 } 1276 1277 return offset; 1278 } 1279 1280 /** 1281 * Determine whether we should clamp cursor position. Currently it's 1282 * only robust for left-aligned displays. 1283 * @hide 1284 */ shouldClampCursor(int line)1285 public boolean shouldClampCursor(int line) { 1286 // Only clamp cursor position in left-aligned displays. 1287 switch (getParagraphAlignment(line)) { 1288 case ALIGN_LEFT: 1289 return true; 1290 case ALIGN_NORMAL: 1291 return getParagraphDirection(line) > 0; 1292 default: 1293 return false; 1294 } 1295 1296 } 1297 /** 1298 * Fills in the specified Path with a representation of a cursor 1299 * at the specified offset. This will often be a vertical line 1300 * but can be multiple discontinuous lines in text with multiple 1301 * directionalities. 1302 */ getCursorPath(int point, Path dest, CharSequence editingBuffer)1303 public void getCursorPath(int point, Path dest, 1304 CharSequence editingBuffer) { 1305 dest.reset(); 1306 1307 int line = getLineForOffset(point); 1308 int top = getLineTop(line); 1309 int bottom = getLineTop(line+1); 1310 1311 boolean clamped = shouldClampCursor(line); 1312 float h1 = getPrimaryHorizontal(point, clamped) - 0.5f; 1313 float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1; 1314 1315 int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | 1316 TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); 1317 int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON); 1318 int dist = 0; 1319 1320 if (caps != 0 || fn != 0) { 1321 dist = (bottom - top) >> 2; 1322 1323 if (fn != 0) 1324 top += dist; 1325 if (caps != 0) 1326 bottom -= dist; 1327 } 1328 1329 if (h1 < 0.5f) 1330 h1 = 0.5f; 1331 if (h2 < 0.5f) 1332 h2 = 0.5f; 1333 1334 if (Float.compare(h1, h2) == 0) { 1335 dest.moveTo(h1, top); 1336 dest.lineTo(h1, bottom); 1337 } else { 1338 dest.moveTo(h1, top); 1339 dest.lineTo(h1, (top + bottom) >> 1); 1340 1341 dest.moveTo(h2, (top + bottom) >> 1); 1342 dest.lineTo(h2, bottom); 1343 } 1344 1345 if (caps == 2) { 1346 dest.moveTo(h2, bottom); 1347 dest.lineTo(h2 - dist, bottom + dist); 1348 dest.lineTo(h2, bottom); 1349 dest.lineTo(h2 + dist, bottom + dist); 1350 } else if (caps == 1) { 1351 dest.moveTo(h2, bottom); 1352 dest.lineTo(h2 - dist, bottom + dist); 1353 1354 dest.moveTo(h2 - dist, bottom + dist - 0.5f); 1355 dest.lineTo(h2 + dist, bottom + dist - 0.5f); 1356 1357 dest.moveTo(h2 + dist, bottom + dist); 1358 dest.lineTo(h2, bottom); 1359 } 1360 1361 if (fn == 2) { 1362 dest.moveTo(h1, top); 1363 dest.lineTo(h1 - dist, top - dist); 1364 dest.lineTo(h1, top); 1365 dest.lineTo(h1 + dist, top - dist); 1366 } else if (fn == 1) { 1367 dest.moveTo(h1, top); 1368 dest.lineTo(h1 - dist, top - dist); 1369 1370 dest.moveTo(h1 - dist, top - dist + 0.5f); 1371 dest.lineTo(h1 + dist, top - dist + 0.5f); 1372 1373 dest.moveTo(h1 + dist, top - dist); 1374 dest.lineTo(h1, top); 1375 } 1376 } 1377 addSelection(int line, int start, int end, int top, int bottom, Path dest)1378 private void addSelection(int line, int start, int end, 1379 int top, int bottom, Path dest) { 1380 int linestart = getLineStart(line); 1381 int lineend = getLineEnd(line); 1382 Directions dirs = getLineDirections(line); 1383 1384 if (lineend > linestart && mText.charAt(lineend - 1) == '\n') 1385 lineend--; 1386 1387 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1388 int here = linestart + dirs.mDirections[i]; 1389 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1390 1391 if (there > lineend) 1392 there = lineend; 1393 1394 if (start <= there && end >= here) { 1395 int st = Math.max(start, here); 1396 int en = Math.min(end, there); 1397 1398 if (st != en) { 1399 float h1 = getHorizontal(st, false, line, false /* not clamped */); 1400 float h2 = getHorizontal(en, true, line, false /* not clamped */); 1401 1402 float left = Math.min(h1, h2); 1403 float right = Math.max(h1, h2); 1404 1405 dest.addRect(left, top, right, bottom, Path.Direction.CW); 1406 } 1407 } 1408 } 1409 } 1410 1411 /** 1412 * Fills in the specified Path with a representation of a highlight 1413 * between the specified offsets. This will often be a rectangle 1414 * or a potentially discontinuous set of rectangles. If the start 1415 * and end are the same, the returned path is empty. 1416 */ getSelectionPath(int start, int end, Path dest)1417 public void getSelectionPath(int start, int end, Path dest) { 1418 dest.reset(); 1419 1420 if (start == end) 1421 return; 1422 1423 if (end < start) { 1424 int temp = end; 1425 end = start; 1426 start = temp; 1427 } 1428 1429 int startline = getLineForOffset(start); 1430 int endline = getLineForOffset(end); 1431 1432 int top = getLineTop(startline); 1433 int bottom = getLineBottom(endline); 1434 1435 if (startline == endline) { 1436 addSelection(startline, start, end, top, bottom, dest); 1437 } else { 1438 final float width = mWidth; 1439 1440 addSelection(startline, start, getLineEnd(startline), 1441 top, getLineBottom(startline), dest); 1442 1443 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) 1444 dest.addRect(getLineLeft(startline), top, 1445 0, getLineBottom(startline), Path.Direction.CW); 1446 else 1447 dest.addRect(getLineRight(startline), top, 1448 width, getLineBottom(startline), Path.Direction.CW); 1449 1450 for (int i = startline + 1; i < endline; i++) { 1451 top = getLineTop(i); 1452 bottom = getLineBottom(i); 1453 dest.addRect(0, top, width, bottom, Path.Direction.CW); 1454 } 1455 1456 top = getLineTop(endline); 1457 bottom = getLineBottom(endline); 1458 1459 addSelection(endline, getLineStart(endline), end, 1460 top, bottom, dest); 1461 1462 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) 1463 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW); 1464 else 1465 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW); 1466 } 1467 } 1468 1469 /** 1470 * Get the alignment of the specified paragraph, taking into account 1471 * markup attached to it. 1472 */ getParagraphAlignment(int line)1473 public final Alignment getParagraphAlignment(int line) { 1474 Alignment align = mAlignment; 1475 1476 if (mSpannedText) { 1477 Spanned sp = (Spanned) mText; 1478 AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line), 1479 getLineEnd(line), 1480 AlignmentSpan.class); 1481 1482 int spanLength = spans.length; 1483 if (spanLength > 0) { 1484 align = spans[spanLength-1].getAlignment(); 1485 } 1486 } 1487 1488 return align; 1489 } 1490 1491 /** 1492 * Get the left edge of the specified paragraph, inset by left margins. 1493 */ getParagraphLeft(int line)1494 public final int getParagraphLeft(int line) { 1495 int left = 0; 1496 int dir = getParagraphDirection(line); 1497 if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) { 1498 return left; // leading margin has no impact, or no styles 1499 } 1500 return getParagraphLeadingMargin(line); 1501 } 1502 1503 /** 1504 * Get the right edge of the specified paragraph, inset by right margins. 1505 */ getParagraphRight(int line)1506 public final int getParagraphRight(int line) { 1507 int right = mWidth; 1508 int dir = getParagraphDirection(line); 1509 if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) { 1510 return right; // leading margin has no impact, or no styles 1511 } 1512 return right - getParagraphLeadingMargin(line); 1513 } 1514 1515 /** 1516 * Returns the effective leading margin (unsigned) for this line, 1517 * taking into account LeadingMarginSpan and LeadingMarginSpan2. 1518 * @param line the line index 1519 * @return the leading margin of this line 1520 */ getParagraphLeadingMargin(int line)1521 private int getParagraphLeadingMargin(int line) { 1522 if (!mSpannedText) { 1523 return 0; 1524 } 1525 Spanned spanned = (Spanned) mText; 1526 1527 int lineStart = getLineStart(line); 1528 int lineEnd = getLineEnd(line); 1529 int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd, 1530 LeadingMarginSpan.class); 1531 LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd, 1532 LeadingMarginSpan.class); 1533 if (spans.length == 0) { 1534 return 0; // no leading margin span; 1535 } 1536 1537 int margin = 0; 1538 1539 boolean isFirstParaLine = lineStart == 0 || 1540 spanned.charAt(lineStart - 1) == '\n'; 1541 1542 for (int i = 0; i < spans.length; i++) { 1543 LeadingMarginSpan span = spans[i]; 1544 boolean useFirstLineMargin = isFirstParaLine; 1545 if (span instanceof LeadingMarginSpan2) { 1546 int spStart = spanned.getSpanStart(span); 1547 int spanLine = getLineForOffset(spStart); 1548 int count = ((LeadingMarginSpan2)span).getLeadingMarginLineCount(); 1549 useFirstLineMargin = line < spanLine + count; 1550 } 1551 margin += span.getLeadingMargin(useFirstLineMargin); 1552 } 1553 1554 return margin; 1555 } 1556 1557 /* package */ 1558 static float measurePara(TextPaint paint, CharSequence text, int start, int end) { 1559 1560 MeasuredText mt = MeasuredText.obtain(); 1561 TextLine tl = TextLine.obtain(); 1562 try { 1563 mt.setPara(text, start, end, TextDirectionHeuristics.LTR); 1564 Directions directions; 1565 int dir; 1566 if (mt.mEasy) { 1567 directions = DIRS_ALL_LEFT_TO_RIGHT; 1568 dir = Layout.DIR_LEFT_TO_RIGHT; 1569 } else { 1570 directions = AndroidBidi.directions(mt.mDir, mt.mLevels, 1571 0, mt.mChars, 0, mt.mLen); 1572 dir = mt.mDir; 1573 } 1574 char[] chars = mt.mChars; 1575 int len = mt.mLen; 1576 boolean hasTabs = false; 1577 TabStops tabStops = null; 1578 for (int i = 0; i < len; ++i) { 1579 if (chars[i] == '\t') { 1580 hasTabs = true; 1581 if (text instanceof Spanned) { 1582 Spanned spanned = (Spanned) text; 1583 int spanEnd = spanned.nextSpanTransition(start, end, 1584 TabStopSpan.class); 1585 TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd, 1586 TabStopSpan.class); 1587 if (spans.length > 0) { 1588 tabStops = new TabStops(TAB_INCREMENT, spans); 1589 } 1590 } 1591 break; 1592 } 1593 } 1594 tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops); 1595 return tl.metrics(null); 1596 } finally { 1597 TextLine.recycle(tl); 1598 MeasuredText.recycle(mt); 1599 } 1600 } 1601 1602 /** 1603 * @hide 1604 */ 1605 /* package */ static class TabStops { 1606 private int[] mStops; 1607 private int mNumStops; 1608 private int mIncrement; 1609 1610 TabStops(int increment, Object[] spans) { 1611 reset(increment, spans); 1612 } 1613 1614 void reset(int increment, Object[] spans) { 1615 this.mIncrement = increment; 1616 1617 int ns = 0; 1618 if (spans != null) { 1619 int[] stops = this.mStops; 1620 for (Object o : spans) { 1621 if (o instanceof TabStopSpan) { 1622 if (stops == null) { 1623 stops = new int[10]; 1624 } else if (ns == stops.length) { 1625 int[] nstops = new int[ns * 2]; 1626 for (int i = 0; i < ns; ++i) { 1627 nstops[i] = stops[i]; 1628 } 1629 stops = nstops; 1630 } 1631 stops[ns++] = ((TabStopSpan) o).getTabStop(); 1632 } 1633 } 1634 if (ns > 1) { 1635 Arrays.sort(stops, 0, ns); 1636 } 1637 if (stops != this.mStops) { 1638 this.mStops = stops; 1639 } 1640 } 1641 this.mNumStops = ns; 1642 } 1643 1644 float nextTab(float h) { 1645 int ns = this.mNumStops; 1646 if (ns > 0) { 1647 int[] stops = this.mStops; 1648 for (int i = 0; i < ns; ++i) { 1649 int stop = stops[i]; 1650 if (stop > h) { 1651 return stop; 1652 } 1653 } 1654 } 1655 return nextDefaultStop(h, mIncrement); 1656 } 1657 nextDefaultStop(float h, int inc)1658 public static float nextDefaultStop(float h, int inc) { 1659 return ((int) ((h + inc) / inc)) * inc; 1660 } 1661 } 1662 1663 /** 1664 * Returns the position of the next tab stop after h on the line. 1665 * 1666 * @param text the text 1667 * @param start start of the line 1668 * @param end limit of the line 1669 * @param h the current horizontal offset 1670 * @param tabs the tabs, can be null. If it is null, any tabs in effect 1671 * on the line will be used. If there are no tabs, a default offset 1672 * will be used to compute the tab stop. 1673 * @return the offset of the next tab stop. 1674 */ nextTab(CharSequence text, int start, int end, float h, Object[] tabs)1675 /* package */ static float nextTab(CharSequence text, int start, int end, 1676 float h, Object[] tabs) { 1677 float nh = Float.MAX_VALUE; 1678 boolean alltabs = false; 1679 1680 if (text instanceof Spanned) { 1681 if (tabs == null) { 1682 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class); 1683 alltabs = true; 1684 } 1685 1686 for (int i = 0; i < tabs.length; i++) { 1687 if (!alltabs) { 1688 if (!(tabs[i] instanceof TabStopSpan)) 1689 continue; 1690 } 1691 1692 int where = ((TabStopSpan) tabs[i]).getTabStop(); 1693 1694 if (where < nh && where > h) 1695 nh = where; 1696 } 1697 1698 if (nh != Float.MAX_VALUE) 1699 return nh; 1700 } 1701 1702 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; 1703 } 1704 isSpanned()1705 protected final boolean isSpanned() { 1706 return mSpannedText; 1707 } 1708 1709 /** 1710 * Returns the same as <code>text.getSpans()</code>, except where 1711 * <code>start</code> and <code>end</code> are the same and are not 1712 * at the very beginning of the text, in which case an empty array 1713 * is returned instead. 1714 * <p> 1715 * This is needed because of the special case that <code>getSpans()</code> 1716 * on an empty range returns the spans adjacent to that range, which is 1717 * primarily for the sake of <code>TextWatchers</code> so they will get 1718 * notifications when text goes from empty to non-empty. But it also 1719 * has the unfortunate side effect that if the text ends with an empty 1720 * paragraph, that paragraph accidentally picks up the styles of the 1721 * preceding paragraph (even though those styles will not be picked up 1722 * by new text that is inserted into the empty paragraph). 1723 * <p> 1724 * The reason it just checks whether <code>start</code> and <code>end</code> 1725 * is the same is that the only time a line can contain 0 characters 1726 * is if it is the final paragraph of the Layout; otherwise any line will 1727 * contain at least one printing or newline character. The reason for the 1728 * additional check if <code>start</code> is greater than 0 is that 1729 * if the empty paragraph is the entire content of the buffer, paragraph 1730 * styles that are already applied to the buffer will apply to text that 1731 * is inserted into it. 1732 */ getParagraphSpans(Spanned text, int start, int end, Class<T> type)1733 /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) { 1734 if (start == end && start > 0) { 1735 return ArrayUtils.emptyArray(type); 1736 } 1737 1738 return text.getSpans(start, end, type); 1739 } 1740 getEllipsisChar(TextUtils.TruncateAt method)1741 private char getEllipsisChar(TextUtils.TruncateAt method) { 1742 return (method == TextUtils.TruncateAt.END_SMALL) ? 1743 ELLIPSIS_TWO_DOTS[0] : 1744 ELLIPSIS_NORMAL[0]; 1745 } 1746 ellipsize(int start, int end, int line, char[] dest, int destoff, TextUtils.TruncateAt method)1747 private void ellipsize(int start, int end, int line, 1748 char[] dest, int destoff, TextUtils.TruncateAt method) { 1749 int ellipsisCount = getEllipsisCount(line); 1750 1751 if (ellipsisCount == 0) { 1752 return; 1753 } 1754 1755 int ellipsisStart = getEllipsisStart(line); 1756 int linestart = getLineStart(line); 1757 1758 for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { 1759 char c; 1760 1761 if (i == ellipsisStart) { 1762 c = getEllipsisChar(method); // ellipsis 1763 } else { 1764 c = '\uFEFF'; // 0-width space 1765 } 1766 1767 int a = i + linestart; 1768 1769 if (a >= start && a < end) { 1770 dest[destoff + a - start] = c; 1771 } 1772 } 1773 } 1774 1775 /** 1776 * Stores information about bidirectional (left-to-right or right-to-left) 1777 * text within the layout of a line. 1778 */ 1779 public static class Directions { 1780 // Directions represents directional runs within a line of text. 1781 // Runs are pairs of ints listed in visual order, starting from the 1782 // leading margin. The first int of each pair is the offset from 1783 // the first character of the line to the start of the run. The 1784 // second int represents both the length and level of the run. 1785 // The length is in the lower bits, accessed by masking with 1786 // DIR_LENGTH_MASK. The level is in the higher bits, accessed 1787 // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. 1788 // To simply test for an RTL direction, test the bit using 1789 // DIR_RTL_FLAG, if set then the direction is rtl. 1790 1791 /* package */ int[] mDirections; Directions(int[] dirs)1792 /* package */ Directions(int[] dirs) { 1793 mDirections = dirs; 1794 } 1795 } 1796 1797 /** 1798 * Return the offset of the first character to be ellipsized away, 1799 * relative to the start of the line. (So 0 if the beginning of the 1800 * line is ellipsized, not getLineStart().) 1801 */ 1802 public abstract int getEllipsisStart(int line); 1803 1804 /** 1805 * Returns the number of characters to be ellipsized away, or 0 if 1806 * no ellipsis is to take place. 1807 */ 1808 public abstract int getEllipsisCount(int line); 1809 1810 /* package */ static class Ellipsizer implements CharSequence, GetChars { 1811 /* package */ CharSequence mText; 1812 /* package */ Layout mLayout; 1813 /* package */ int mWidth; 1814 /* package */ TextUtils.TruncateAt mMethod; 1815 Ellipsizer(CharSequence s)1816 public Ellipsizer(CharSequence s) { 1817 mText = s; 1818 } 1819 charAt(int off)1820 public char charAt(int off) { 1821 char[] buf = TextUtils.obtain(1); 1822 getChars(off, off + 1, buf, 0); 1823 char ret = buf[0]; 1824 1825 TextUtils.recycle(buf); 1826 return ret; 1827 } 1828 getChars(int start, int end, char[] dest, int destoff)1829 public void getChars(int start, int end, char[] dest, int destoff) { 1830 int line1 = mLayout.getLineForOffset(start); 1831 int line2 = mLayout.getLineForOffset(end); 1832 1833 TextUtils.getChars(mText, start, end, dest, destoff); 1834 1835 for (int i = line1; i <= line2; i++) { 1836 mLayout.ellipsize(start, end, i, dest, destoff, mMethod); 1837 } 1838 } 1839 length()1840 public int length() { 1841 return mText.length(); 1842 } 1843 subSequence(int start, int end)1844 public CharSequence subSequence(int start, int end) { 1845 char[] s = new char[end - start]; 1846 getChars(start, end, s, 0); 1847 return new String(s); 1848 } 1849 1850 @Override toString()1851 public String toString() { 1852 char[] s = new char[length()]; 1853 getChars(0, length(), s, 0); 1854 return new String(s); 1855 } 1856 1857 } 1858 1859 /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned { 1860 private Spanned mSpanned; 1861 SpannedEllipsizer(CharSequence display)1862 public SpannedEllipsizer(CharSequence display) { 1863 super(display); 1864 mSpanned = (Spanned) display; 1865 } 1866 getSpans(int start, int end, Class<T> type)1867 public <T> T[] getSpans(int start, int end, Class<T> type) { 1868 return mSpanned.getSpans(start, end, type); 1869 } 1870 getSpanStart(Object tag)1871 public int getSpanStart(Object tag) { 1872 return mSpanned.getSpanStart(tag); 1873 } 1874 getSpanEnd(Object tag)1875 public int getSpanEnd(Object tag) { 1876 return mSpanned.getSpanEnd(tag); 1877 } 1878 getSpanFlags(Object tag)1879 public int getSpanFlags(Object tag) { 1880 return mSpanned.getSpanFlags(tag); 1881 } 1882 1883 @SuppressWarnings("rawtypes") nextSpanTransition(int start, int limit, Class type)1884 public int nextSpanTransition(int start, int limit, Class type) { 1885 return mSpanned.nextSpanTransition(start, limit, type); 1886 } 1887 1888 @Override subSequence(int start, int end)1889 public CharSequence subSequence(int start, int end) { 1890 char[] s = new char[end - start]; 1891 getChars(start, end, s, 0); 1892 1893 SpannableString ss = new SpannableString(new String(s)); 1894 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); 1895 return ss; 1896 } 1897 } 1898 1899 private CharSequence mText; 1900 private TextPaint mPaint; 1901 /* package */ TextPaint mWorkPaint; 1902 private int mWidth; 1903 private Alignment mAlignment = Alignment.ALIGN_NORMAL; 1904 private float mSpacingMult; 1905 private float mSpacingAdd; 1906 private static final Rect sTempRect = new Rect(); 1907 private boolean mSpannedText; 1908 private TextDirectionHeuristic mTextDir; 1909 private SpanSet<LineBackgroundSpan> mLineBackgroundSpans; 1910 1911 public static final int DIR_LEFT_TO_RIGHT = 1; 1912 public static final int DIR_RIGHT_TO_LEFT = -1; 1913 1914 /* package */ static final int DIR_REQUEST_LTR = 1; 1915 /* package */ static final int DIR_REQUEST_RTL = -1; 1916 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; 1917 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; 1918 1919 /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; 1920 /* package */ static final int RUN_LEVEL_SHIFT = 26; 1921 /* package */ static final int RUN_LEVEL_MASK = 0x3f; 1922 /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; 1923 1924 public enum Alignment { 1925 ALIGN_NORMAL, 1926 ALIGN_OPPOSITE, 1927 ALIGN_CENTER, 1928 /** @hide */ 1929 ALIGN_LEFT, 1930 /** @hide */ 1931 ALIGN_RIGHT, 1932 } 1933 1934 private static final int TAB_INCREMENT = 20; 1935 1936 /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = 1937 new Directions(new int[] { 0, RUN_LENGTH_MASK }); 1938 /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = 1939 new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); 1940 1941 /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..." 1942 /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".." 1943 } 1944