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 boolean trailing = primaryIsTrailingPrevious(offset); 796 return getHorizontal(offset, trailing); 797 } 798 799 /** 800 * Get the secondary horizontal position for the specified text offset. 801 * This is the location where a new character would be inserted in 802 * the direction other than the paragraph's primary direction. 803 */ getSecondaryHorizontal(int offset)804 public float getSecondaryHorizontal(int offset) { 805 boolean trailing = primaryIsTrailingPrevious(offset); 806 return getHorizontal(offset, !trailing); 807 } 808 getHorizontal(int offset, boolean trailing)809 private float getHorizontal(int offset, boolean trailing) { 810 int line = getLineForOffset(offset); 811 812 return getHorizontal(offset, trailing, line); 813 } 814 getHorizontal(int offset, boolean trailing, int line)815 private float getHorizontal(int offset, boolean trailing, int line) { 816 int start = getLineStart(line); 817 int end = getLineEnd(line); 818 int dir = getParagraphDirection(line); 819 boolean hasTabOrEmoji = getLineContainsTab(line); 820 Directions directions = getLineDirections(line); 821 822 TabStops tabStops = null; 823 if (hasTabOrEmoji && mText instanceof Spanned) { 824 // Just checking this line should be good enough, tabs should be 825 // consistent across all lines in a paragraph. 826 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 827 if (tabs.length > 0) { 828 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 829 } 830 } 831 832 TextLine tl = TextLine.obtain(); 833 tl.set(mPaint, mText, start, end, dir, directions, hasTabOrEmoji, tabStops); 834 float wid = tl.measure(offset - start, trailing, null); 835 TextLine.recycle(tl); 836 837 int left = getParagraphLeft(line); 838 int right = getParagraphRight(line); 839 840 return getLineStartPos(line, left, right) + wid; 841 } 842 843 /** 844 * Get the leftmost position that should be exposed for horizontal 845 * scrolling on the specified line. 846 */ getLineLeft(int line)847 public float getLineLeft(int line) { 848 int dir = getParagraphDirection(line); 849 Alignment align = getParagraphAlignment(line); 850 851 if (align == Alignment.ALIGN_LEFT) { 852 return 0; 853 } else if (align == Alignment.ALIGN_NORMAL) { 854 if (dir == DIR_RIGHT_TO_LEFT) 855 return getParagraphRight(line) - getLineMax(line); 856 else 857 return 0; 858 } else if (align == Alignment.ALIGN_RIGHT) { 859 return mWidth - getLineMax(line); 860 } else if (align == Alignment.ALIGN_OPPOSITE) { 861 if (dir == DIR_RIGHT_TO_LEFT) 862 return 0; 863 else 864 return mWidth - getLineMax(line); 865 } else { /* align == Alignment.ALIGN_CENTER */ 866 int left = getParagraphLeft(line); 867 int right = getParagraphRight(line); 868 int max = ((int) getLineMax(line)) & ~1; 869 870 return left + ((right - left) - max) / 2; 871 } 872 } 873 874 /** 875 * Get the rightmost position that should be exposed for horizontal 876 * scrolling on the specified line. 877 */ getLineRight(int line)878 public float getLineRight(int line) { 879 int dir = getParagraphDirection(line); 880 Alignment align = getParagraphAlignment(line); 881 882 if (align == Alignment.ALIGN_LEFT) { 883 return getParagraphLeft(line) + getLineMax(line); 884 } else if (align == Alignment.ALIGN_NORMAL) { 885 if (dir == DIR_RIGHT_TO_LEFT) 886 return mWidth; 887 else 888 return getParagraphLeft(line) + getLineMax(line); 889 } else if (align == Alignment.ALIGN_RIGHT) { 890 return mWidth; 891 } else if (align == Alignment.ALIGN_OPPOSITE) { 892 if (dir == DIR_RIGHT_TO_LEFT) 893 return getLineMax(line); 894 else 895 return mWidth; 896 } else { /* align == Alignment.ALIGN_CENTER */ 897 int left = getParagraphLeft(line); 898 int right = getParagraphRight(line); 899 int max = ((int) getLineMax(line)) & ~1; 900 901 return right - ((right - left) - max) / 2; 902 } 903 } 904 905 /** 906 * Gets the unsigned horizontal extent of the specified line, including 907 * leading margin indent, but excluding trailing whitespace. 908 */ getLineMax(int line)909 public float getLineMax(int line) { 910 float margin = getParagraphLeadingMargin(line); 911 float signedExtent = getLineExtent(line, false); 912 return margin + signedExtent >= 0 ? signedExtent : -signedExtent; 913 } 914 915 /** 916 * Gets the unsigned horizontal extent of the specified line, including 917 * leading margin indent and trailing whitespace. 918 */ getLineWidth(int line)919 public float getLineWidth(int line) { 920 float margin = getParagraphLeadingMargin(line); 921 float signedExtent = getLineExtent(line, true); 922 return margin + signedExtent >= 0 ? signedExtent : -signedExtent; 923 } 924 925 /** 926 * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the 927 * tab stops instead of using the ones passed in. 928 * @param line the index of the line 929 * @param full whether to include trailing whitespace 930 * @return the extent of the line 931 */ getLineExtent(int line, boolean full)932 private float getLineExtent(int line, boolean full) { 933 int start = getLineStart(line); 934 int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 935 936 boolean hasTabsOrEmoji = getLineContainsTab(line); 937 TabStops tabStops = null; 938 if (hasTabsOrEmoji && mText instanceof Spanned) { 939 // Just checking this line should be good enough, tabs should be 940 // consistent across all lines in a paragraph. 941 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 942 if (tabs.length > 0) { 943 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 944 } 945 } 946 Directions directions = getLineDirections(line); 947 // Returned directions can actually be null 948 if (directions == null) { 949 return 0f; 950 } 951 int dir = getParagraphDirection(line); 952 953 TextLine tl = TextLine.obtain(); 954 tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); 955 float width = tl.metrics(null); 956 TextLine.recycle(tl); 957 return width; 958 } 959 960 /** 961 * Returns the signed horizontal extent of the specified line, excluding 962 * leading margin. If full is false, excludes trailing whitespace. 963 * @param line the index of the line 964 * @param tabStops the tab stops, can be null if we know they're not used. 965 * @param full whether to include trailing whitespace 966 * @return the extent of the text on this line 967 */ getLineExtent(int line, TabStops tabStops, boolean full)968 private float getLineExtent(int line, TabStops tabStops, boolean full) { 969 int start = getLineStart(line); 970 int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 971 boolean hasTabsOrEmoji = getLineContainsTab(line); 972 Directions directions = getLineDirections(line); 973 int dir = getParagraphDirection(line); 974 975 TextLine tl = TextLine.obtain(); 976 tl.set(mPaint, mText, start, end, dir, directions, hasTabsOrEmoji, tabStops); 977 float width = tl.metrics(null); 978 TextLine.recycle(tl); 979 return width; 980 } 981 982 /** 983 * Get the line number corresponding to the specified vertical position. 984 * If you ask for a position above 0, you get 0; if you ask for a position 985 * below the bottom of the text, you get the last line. 986 */ 987 // FIXME: It may be faster to do a linear search for layouts without many lines. getLineForVertical(int vertical)988 public int getLineForVertical(int vertical) { 989 int high = getLineCount(), low = -1, guess; 990 991 while (high - low > 1) { 992 guess = (high + low) / 2; 993 994 if (getLineTop(guess) > vertical) 995 high = guess; 996 else 997 low = guess; 998 } 999 1000 if (low < 0) 1001 return 0; 1002 else 1003 return low; 1004 } 1005 1006 /** 1007 * Get the line number on which the specified text offset appears. 1008 * If you ask for a position before 0, you get 0; if you ask for a position 1009 * beyond the end of the text, you get the last line. 1010 */ getLineForOffset(int offset)1011 public int getLineForOffset(int offset) { 1012 int high = getLineCount(), low = -1, guess; 1013 1014 while (high - low > 1) { 1015 guess = (high + low) / 2; 1016 1017 if (getLineStart(guess) > offset) 1018 high = guess; 1019 else 1020 low = guess; 1021 } 1022 1023 if (low < 0) 1024 return 0; 1025 else 1026 return low; 1027 } 1028 1029 /** 1030 * Get the character offset on the specified line whose position is 1031 * closest to the specified horizontal position. 1032 */ getOffsetForHorizontal(int line, float horiz)1033 public int getOffsetForHorizontal(int line, float horiz) { 1034 int max = getLineEnd(line) - 1; 1035 int min = getLineStart(line); 1036 Directions dirs = getLineDirections(line); 1037 1038 if (line == getLineCount() - 1) 1039 max++; 1040 1041 int best = min; 1042 float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); 1043 1044 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1045 int here = min + dirs.mDirections[i]; 1046 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1047 int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1; 1048 1049 if (there > max) 1050 there = max; 1051 int high = there - 1 + 1, low = here + 1 - 1, guess; 1052 1053 while (high - low > 1) { 1054 guess = (high + low) / 2; 1055 int adguess = getOffsetAtStartOf(guess); 1056 1057 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap) 1058 high = guess; 1059 else 1060 low = guess; 1061 } 1062 1063 if (low < here + 1) 1064 low = here + 1; 1065 1066 if (low < there) { 1067 low = getOffsetAtStartOf(low); 1068 1069 float dist = Math.abs(getPrimaryHorizontal(low) - horiz); 1070 1071 int aft = TextUtils.getOffsetAfter(mText, low); 1072 if (aft < there) { 1073 float other = Math.abs(getPrimaryHorizontal(aft) - horiz); 1074 1075 if (other < dist) { 1076 dist = other; 1077 low = aft; 1078 } 1079 } 1080 1081 if (dist < bestdist) { 1082 bestdist = dist; 1083 best = low; 1084 } 1085 } 1086 1087 float dist = Math.abs(getPrimaryHorizontal(here) - horiz); 1088 1089 if (dist < bestdist) { 1090 bestdist = dist; 1091 best = here; 1092 } 1093 } 1094 1095 float dist = Math.abs(getPrimaryHorizontal(max) - horiz); 1096 1097 if (dist < bestdist) { 1098 bestdist = dist; 1099 best = max; 1100 } 1101 1102 return best; 1103 } 1104 1105 /** 1106 * Return the text offset after the last character on the specified line. 1107 */ getLineEnd(int line)1108 public final int getLineEnd(int line) { 1109 return getLineStart(line + 1); 1110 } 1111 1112 /** 1113 * Return the text offset after the last visible character (so whitespace 1114 * is not counted) on the specified line. 1115 */ getLineVisibleEnd(int line)1116 public int getLineVisibleEnd(int line) { 1117 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 1118 } 1119 getLineVisibleEnd(int line, int start, int end)1120 private int getLineVisibleEnd(int line, int start, int end) { 1121 CharSequence text = mText; 1122 char ch; 1123 if (line == getLineCount() - 1) { 1124 return end; 1125 } 1126 1127 for (; end > start; end--) { 1128 ch = text.charAt(end - 1); 1129 1130 if (ch == '\n') { 1131 return end - 1; 1132 } 1133 1134 if (ch != ' ' && ch != '\t') { 1135 break; 1136 } 1137 1138 } 1139 1140 return end; 1141 } 1142 1143 /** 1144 * Return the vertical position of the bottom of the specified line. 1145 */ getLineBottom(int line)1146 public final int getLineBottom(int line) { 1147 return getLineTop(line + 1); 1148 } 1149 1150 /** 1151 * Return the vertical position of the baseline of the specified line. 1152 */ getLineBaseline(int line)1153 public final int getLineBaseline(int line) { 1154 // getLineTop(line+1) == getLineTop(line) 1155 return getLineTop(line+1) - getLineDescent(line); 1156 } 1157 1158 /** 1159 * Get the ascent of the text on the specified line. 1160 * The return value is negative to match the Paint.ascent() convention. 1161 */ getLineAscent(int line)1162 public final int getLineAscent(int line) { 1163 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) 1164 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); 1165 } 1166 getOffsetToLeftOf(int offset)1167 public int getOffsetToLeftOf(int offset) { 1168 return getOffsetToLeftRightOf(offset, true); 1169 } 1170 getOffsetToRightOf(int offset)1171 public int getOffsetToRightOf(int offset) { 1172 return getOffsetToLeftRightOf(offset, false); 1173 } 1174 getOffsetToLeftRightOf(int caret, boolean toLeft)1175 private int getOffsetToLeftRightOf(int caret, boolean toLeft) { 1176 int line = getLineForOffset(caret); 1177 int lineStart = getLineStart(line); 1178 int lineEnd = getLineEnd(line); 1179 int lineDir = getParagraphDirection(line); 1180 1181 boolean lineChanged = false; 1182 boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); 1183 // if walking off line, look at the line we're headed to 1184 if (advance) { 1185 if (caret == lineEnd) { 1186 if (line < getLineCount() - 1) { 1187 lineChanged = true; 1188 ++line; 1189 } else { 1190 return caret; // at very end, don't move 1191 } 1192 } 1193 } else { 1194 if (caret == lineStart) { 1195 if (line > 0) { 1196 lineChanged = true; 1197 --line; 1198 } else { 1199 return caret; // at very start, don't move 1200 } 1201 } 1202 } 1203 1204 if (lineChanged) { 1205 lineStart = getLineStart(line); 1206 lineEnd = getLineEnd(line); 1207 int newDir = getParagraphDirection(line); 1208 if (newDir != lineDir) { 1209 // unusual case. we want to walk onto the line, but it runs 1210 // in a different direction than this one, so we fake movement 1211 // in the opposite direction. 1212 toLeft = !toLeft; 1213 lineDir = newDir; 1214 } 1215 } 1216 1217 Directions directions = getLineDirections(line); 1218 1219 TextLine tl = TextLine.obtain(); 1220 // XXX: we don't care about tabs 1221 tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null); 1222 caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); 1223 tl = TextLine.recycle(tl); 1224 return caret; 1225 } 1226 getOffsetAtStartOf(int offset)1227 private int getOffsetAtStartOf(int offset) { 1228 // XXX this probably should skip local reorderings and 1229 // zero-width characters, look at callers 1230 if (offset == 0) 1231 return 0; 1232 1233 CharSequence text = mText; 1234 char c = text.charAt(offset); 1235 1236 if (c >= '\uDC00' && c <= '\uDFFF') { 1237 char c1 = text.charAt(offset - 1); 1238 1239 if (c1 >= '\uD800' && c1 <= '\uDBFF') 1240 offset -= 1; 1241 } 1242 1243 if (mSpannedText) { 1244 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 1245 ReplacementSpan.class); 1246 1247 for (int i = 0; i < spans.length; i++) { 1248 int start = ((Spanned) text).getSpanStart(spans[i]); 1249 int end = ((Spanned) text).getSpanEnd(spans[i]); 1250 1251 if (start < offset && end > offset) 1252 offset = start; 1253 } 1254 } 1255 1256 return offset; 1257 } 1258 1259 /** 1260 * Fills in the specified Path with a representation of a cursor 1261 * at the specified offset. This will often be a vertical line 1262 * but can be multiple discontinuous lines in text with multiple 1263 * directionalities. 1264 */ getCursorPath(int point, Path dest, CharSequence editingBuffer)1265 public void getCursorPath(int point, Path dest, 1266 CharSequence editingBuffer) { 1267 dest.reset(); 1268 1269 int line = getLineForOffset(point); 1270 int top = getLineTop(line); 1271 int bottom = getLineTop(line+1); 1272 1273 float h1 = getPrimaryHorizontal(point) - 0.5f; 1274 float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point) - 0.5f : h1; 1275 1276 int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | 1277 TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); 1278 int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON); 1279 int dist = 0; 1280 1281 if (caps != 0 || fn != 0) { 1282 dist = (bottom - top) >> 2; 1283 1284 if (fn != 0) 1285 top += dist; 1286 if (caps != 0) 1287 bottom -= dist; 1288 } 1289 1290 if (h1 < 0.5f) 1291 h1 = 0.5f; 1292 if (h2 < 0.5f) 1293 h2 = 0.5f; 1294 1295 if (Float.compare(h1, h2) == 0) { 1296 dest.moveTo(h1, top); 1297 dest.lineTo(h1, bottom); 1298 } else { 1299 dest.moveTo(h1, top); 1300 dest.lineTo(h1, (top + bottom) >> 1); 1301 1302 dest.moveTo(h2, (top + bottom) >> 1); 1303 dest.lineTo(h2, bottom); 1304 } 1305 1306 if (caps == 2) { 1307 dest.moveTo(h2, bottom); 1308 dest.lineTo(h2 - dist, bottom + dist); 1309 dest.lineTo(h2, bottom); 1310 dest.lineTo(h2 + dist, bottom + dist); 1311 } else if (caps == 1) { 1312 dest.moveTo(h2, bottom); 1313 dest.lineTo(h2 - dist, bottom + dist); 1314 1315 dest.moveTo(h2 - dist, bottom + dist - 0.5f); 1316 dest.lineTo(h2 + dist, bottom + dist - 0.5f); 1317 1318 dest.moveTo(h2 + dist, bottom + dist); 1319 dest.lineTo(h2, bottom); 1320 } 1321 1322 if (fn == 2) { 1323 dest.moveTo(h1, top); 1324 dest.lineTo(h1 - dist, top - dist); 1325 dest.lineTo(h1, top); 1326 dest.lineTo(h1 + dist, top - dist); 1327 } else if (fn == 1) { 1328 dest.moveTo(h1, top); 1329 dest.lineTo(h1 - dist, top - dist); 1330 1331 dest.moveTo(h1 - dist, top - dist + 0.5f); 1332 dest.lineTo(h1 + dist, top - dist + 0.5f); 1333 1334 dest.moveTo(h1 + dist, top - dist); 1335 dest.lineTo(h1, top); 1336 } 1337 } 1338 addSelection(int line, int start, int end, int top, int bottom, Path dest)1339 private void addSelection(int line, int start, int end, 1340 int top, int bottom, Path dest) { 1341 int linestart = getLineStart(line); 1342 int lineend = getLineEnd(line); 1343 Directions dirs = getLineDirections(line); 1344 1345 if (lineend > linestart && mText.charAt(lineend - 1) == '\n') 1346 lineend--; 1347 1348 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1349 int here = linestart + dirs.mDirections[i]; 1350 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1351 1352 if (there > lineend) 1353 there = lineend; 1354 1355 if (start <= there && end >= here) { 1356 int st = Math.max(start, here); 1357 int en = Math.min(end, there); 1358 1359 if (st != en) { 1360 float h1 = getHorizontal(st, false, line); 1361 float h2 = getHorizontal(en, true, line); 1362 1363 float left = Math.min(h1, h2); 1364 float right = Math.max(h1, h2); 1365 1366 dest.addRect(left, top, right, bottom, Path.Direction.CW); 1367 } 1368 } 1369 } 1370 } 1371 1372 /** 1373 * Fills in the specified Path with a representation of a highlight 1374 * between the specified offsets. This will often be a rectangle 1375 * or a potentially discontinuous set of rectangles. If the start 1376 * and end are the same, the returned path is empty. 1377 */ getSelectionPath(int start, int end, Path dest)1378 public void getSelectionPath(int start, int end, Path dest) { 1379 dest.reset(); 1380 1381 if (start == end) 1382 return; 1383 1384 if (end < start) { 1385 int temp = end; 1386 end = start; 1387 start = temp; 1388 } 1389 1390 int startline = getLineForOffset(start); 1391 int endline = getLineForOffset(end); 1392 1393 int top = getLineTop(startline); 1394 int bottom = getLineBottom(endline); 1395 1396 if (startline == endline) { 1397 addSelection(startline, start, end, top, bottom, dest); 1398 } else { 1399 final float width = mWidth; 1400 1401 addSelection(startline, start, getLineEnd(startline), 1402 top, getLineBottom(startline), dest); 1403 1404 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) 1405 dest.addRect(getLineLeft(startline), top, 1406 0, getLineBottom(startline), Path.Direction.CW); 1407 else 1408 dest.addRect(getLineRight(startline), top, 1409 width, getLineBottom(startline), Path.Direction.CW); 1410 1411 for (int i = startline + 1; i < endline; i++) { 1412 top = getLineTop(i); 1413 bottom = getLineBottom(i); 1414 dest.addRect(0, top, width, bottom, Path.Direction.CW); 1415 } 1416 1417 top = getLineTop(endline); 1418 bottom = getLineBottom(endline); 1419 1420 addSelection(endline, getLineStart(endline), end, 1421 top, bottom, dest); 1422 1423 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) 1424 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW); 1425 else 1426 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW); 1427 } 1428 } 1429 1430 /** 1431 * Get the alignment of the specified paragraph, taking into account 1432 * markup attached to it. 1433 */ getParagraphAlignment(int line)1434 public final Alignment getParagraphAlignment(int line) { 1435 Alignment align = mAlignment; 1436 1437 if (mSpannedText) { 1438 Spanned sp = (Spanned) mText; 1439 AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line), 1440 getLineEnd(line), 1441 AlignmentSpan.class); 1442 1443 int spanLength = spans.length; 1444 if (spanLength > 0) { 1445 align = spans[spanLength-1].getAlignment(); 1446 } 1447 } 1448 1449 return align; 1450 } 1451 1452 /** 1453 * Get the left edge of the specified paragraph, inset by left margins. 1454 */ getParagraphLeft(int line)1455 public final int getParagraphLeft(int line) { 1456 int left = 0; 1457 int dir = getParagraphDirection(line); 1458 if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) { 1459 return left; // leading margin has no impact, or no styles 1460 } 1461 return getParagraphLeadingMargin(line); 1462 } 1463 1464 /** 1465 * Get the right edge of the specified paragraph, inset by right margins. 1466 */ getParagraphRight(int line)1467 public final int getParagraphRight(int line) { 1468 int right = mWidth; 1469 int dir = getParagraphDirection(line); 1470 if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) { 1471 return right; // leading margin has no impact, or no styles 1472 } 1473 return right - getParagraphLeadingMargin(line); 1474 } 1475 1476 /** 1477 * Returns the effective leading margin (unsigned) for this line, 1478 * taking into account LeadingMarginSpan and LeadingMarginSpan2. 1479 * @param line the line index 1480 * @return the leading margin of this line 1481 */ getParagraphLeadingMargin(int line)1482 private int getParagraphLeadingMargin(int line) { 1483 if (!mSpannedText) { 1484 return 0; 1485 } 1486 Spanned spanned = (Spanned) mText; 1487 1488 int lineStart = getLineStart(line); 1489 int lineEnd = getLineEnd(line); 1490 int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd, 1491 LeadingMarginSpan.class); 1492 LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd, 1493 LeadingMarginSpan.class); 1494 if (spans.length == 0) { 1495 return 0; // no leading margin span; 1496 } 1497 1498 int margin = 0; 1499 1500 boolean isFirstParaLine = lineStart == 0 || 1501 spanned.charAt(lineStart - 1) == '\n'; 1502 1503 for (int i = 0; i < spans.length; i++) { 1504 LeadingMarginSpan span = spans[i]; 1505 boolean useFirstLineMargin = isFirstParaLine; 1506 if (span instanceof LeadingMarginSpan2) { 1507 int spStart = spanned.getSpanStart(span); 1508 int spanLine = getLineForOffset(spStart); 1509 int count = ((LeadingMarginSpan2)span).getLeadingMarginLineCount(); 1510 useFirstLineMargin = line < spanLine + count; 1511 } 1512 margin += span.getLeadingMargin(useFirstLineMargin); 1513 } 1514 1515 return margin; 1516 } 1517 1518 /* package */ 1519 static float measurePara(TextPaint paint, CharSequence text, int start, int end) { 1520 1521 MeasuredText mt = MeasuredText.obtain(); 1522 TextLine tl = TextLine.obtain(); 1523 try { 1524 mt.setPara(text, start, end, TextDirectionHeuristics.LTR); 1525 Directions directions; 1526 int dir; 1527 if (mt.mEasy) { 1528 directions = DIRS_ALL_LEFT_TO_RIGHT; 1529 dir = Layout.DIR_LEFT_TO_RIGHT; 1530 } else { 1531 directions = AndroidBidi.directions(mt.mDir, mt.mLevels, 1532 0, mt.mChars, 0, mt.mLen); 1533 dir = mt.mDir; 1534 } 1535 char[] chars = mt.mChars; 1536 int len = mt.mLen; 1537 boolean hasTabs = false; 1538 TabStops tabStops = null; 1539 for (int i = 0; i < len; ++i) { 1540 if (chars[i] == '\t') { 1541 hasTabs = true; 1542 if (text instanceof Spanned) { 1543 Spanned spanned = (Spanned) text; 1544 int spanEnd = spanned.nextSpanTransition(start, end, 1545 TabStopSpan.class); 1546 TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd, 1547 TabStopSpan.class); 1548 if (spans.length > 0) { 1549 tabStops = new TabStops(TAB_INCREMENT, spans); 1550 } 1551 } 1552 break; 1553 } 1554 } 1555 tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops); 1556 return tl.metrics(null); 1557 } finally { 1558 TextLine.recycle(tl); 1559 MeasuredText.recycle(mt); 1560 } 1561 } 1562 1563 /** 1564 * @hide 1565 */ 1566 /* package */ static class TabStops { 1567 private int[] mStops; 1568 private int mNumStops; 1569 private int mIncrement; 1570 1571 TabStops(int increment, Object[] spans) { 1572 reset(increment, spans); 1573 } 1574 1575 void reset(int increment, Object[] spans) { 1576 this.mIncrement = increment; 1577 1578 int ns = 0; 1579 if (spans != null) { 1580 int[] stops = this.mStops; 1581 for (Object o : spans) { 1582 if (o instanceof TabStopSpan) { 1583 if (stops == null) { 1584 stops = new int[10]; 1585 } else if (ns == stops.length) { 1586 int[] nstops = new int[ns * 2]; 1587 for (int i = 0; i < ns; ++i) { 1588 nstops[i] = stops[i]; 1589 } 1590 stops = nstops; 1591 } 1592 stops[ns++] = ((TabStopSpan) o).getTabStop(); 1593 } 1594 } 1595 if (ns > 1) { 1596 Arrays.sort(stops, 0, ns); 1597 } 1598 if (stops != this.mStops) { 1599 this.mStops = stops; 1600 } 1601 } 1602 this.mNumStops = ns; 1603 } 1604 1605 float nextTab(float h) { 1606 int ns = this.mNumStops; 1607 if (ns > 0) { 1608 int[] stops = this.mStops; 1609 for (int i = 0; i < ns; ++i) { 1610 int stop = stops[i]; 1611 if (stop > h) { 1612 return stop; 1613 } 1614 } 1615 } 1616 return nextDefaultStop(h, mIncrement); 1617 } 1618 nextDefaultStop(float h, int inc)1619 public static float nextDefaultStop(float h, int inc) { 1620 return ((int) ((h + inc) / inc)) * inc; 1621 } 1622 } 1623 1624 /** 1625 * Returns the position of the next tab stop after h on the line. 1626 * 1627 * @param text the text 1628 * @param start start of the line 1629 * @param end limit of the line 1630 * @param h the current horizontal offset 1631 * @param tabs the tabs, can be null. If it is null, any tabs in effect 1632 * on the line will be used. If there are no tabs, a default offset 1633 * will be used to compute the tab stop. 1634 * @return the offset of the next tab stop. 1635 */ nextTab(CharSequence text, int start, int end, float h, Object[] tabs)1636 /* package */ static float nextTab(CharSequence text, int start, int end, 1637 float h, Object[] tabs) { 1638 float nh = Float.MAX_VALUE; 1639 boolean alltabs = false; 1640 1641 if (text instanceof Spanned) { 1642 if (tabs == null) { 1643 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class); 1644 alltabs = true; 1645 } 1646 1647 for (int i = 0; i < tabs.length; i++) { 1648 if (!alltabs) { 1649 if (!(tabs[i] instanceof TabStopSpan)) 1650 continue; 1651 } 1652 1653 int where = ((TabStopSpan) tabs[i]).getTabStop(); 1654 1655 if (where < nh && where > h) 1656 nh = where; 1657 } 1658 1659 if (nh != Float.MAX_VALUE) 1660 return nh; 1661 } 1662 1663 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; 1664 } 1665 isSpanned()1666 protected final boolean isSpanned() { 1667 return mSpannedText; 1668 } 1669 1670 /** 1671 * Returns the same as <code>text.getSpans()</code>, except where 1672 * <code>start</code> and <code>end</code> are the same and are not 1673 * at the very beginning of the text, in which case an empty array 1674 * is returned instead. 1675 * <p> 1676 * This is needed because of the special case that <code>getSpans()</code> 1677 * on an empty range returns the spans adjacent to that range, which is 1678 * primarily for the sake of <code>TextWatchers</code> so they will get 1679 * notifications when text goes from empty to non-empty. But it also 1680 * has the unfortunate side effect that if the text ends with an empty 1681 * paragraph, that paragraph accidentally picks up the styles of the 1682 * preceding paragraph (even though those styles will not be picked up 1683 * by new text that is inserted into the empty paragraph). 1684 * <p> 1685 * The reason it just checks whether <code>start</code> and <code>end</code> 1686 * is the same is that the only time a line can contain 0 characters 1687 * is if it is the final paragraph of the Layout; otherwise any line will 1688 * contain at least one printing or newline character. The reason for the 1689 * additional check if <code>start</code> is greater than 0 is that 1690 * if the empty paragraph is the entire content of the buffer, paragraph 1691 * styles that are already applied to the buffer will apply to text that 1692 * is inserted into it. 1693 */ getParagraphSpans(Spanned text, int start, int end, Class<T> type)1694 /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) { 1695 if (start == end && start > 0) { 1696 return ArrayUtils.emptyArray(type); 1697 } 1698 1699 return text.getSpans(start, end, type); 1700 } 1701 getEllipsisChar(TextUtils.TruncateAt method)1702 private char getEllipsisChar(TextUtils.TruncateAt method) { 1703 return (method == TextUtils.TruncateAt.END_SMALL) ? 1704 ELLIPSIS_TWO_DOTS[0] : 1705 ELLIPSIS_NORMAL[0]; 1706 } 1707 ellipsize(int start, int end, int line, char[] dest, int destoff, TextUtils.TruncateAt method)1708 private void ellipsize(int start, int end, int line, 1709 char[] dest, int destoff, TextUtils.TruncateAt method) { 1710 int ellipsisCount = getEllipsisCount(line); 1711 1712 if (ellipsisCount == 0) { 1713 return; 1714 } 1715 1716 int ellipsisStart = getEllipsisStart(line); 1717 int linestart = getLineStart(line); 1718 1719 for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { 1720 char c; 1721 1722 if (i == ellipsisStart) { 1723 c = getEllipsisChar(method); // ellipsis 1724 } else { 1725 c = '\uFEFF'; // 0-width space 1726 } 1727 1728 int a = i + linestart; 1729 1730 if (a >= start && a < end) { 1731 dest[destoff + a - start] = c; 1732 } 1733 } 1734 } 1735 1736 /** 1737 * Stores information about bidirectional (left-to-right or right-to-left) 1738 * text within the layout of a line. 1739 */ 1740 public static class Directions { 1741 // Directions represents directional runs within a line of text. 1742 // Runs are pairs of ints listed in visual order, starting from the 1743 // leading margin. The first int of each pair is the offset from 1744 // the first character of the line to the start of the run. The 1745 // second int represents both the length and level of the run. 1746 // The length is in the lower bits, accessed by masking with 1747 // DIR_LENGTH_MASK. The level is in the higher bits, accessed 1748 // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. 1749 // To simply test for an RTL direction, test the bit using 1750 // DIR_RTL_FLAG, if set then the direction is rtl. 1751 1752 /* package */ int[] mDirections; Directions(int[] dirs)1753 /* package */ Directions(int[] dirs) { 1754 mDirections = dirs; 1755 } 1756 } 1757 1758 /** 1759 * Return the offset of the first character to be ellipsized away, 1760 * relative to the start of the line. (So 0 if the beginning of the 1761 * line is ellipsized, not getLineStart().) 1762 */ 1763 public abstract int getEllipsisStart(int line); 1764 1765 /** 1766 * Returns the number of characters to be ellipsized away, or 0 if 1767 * no ellipsis is to take place. 1768 */ 1769 public abstract int getEllipsisCount(int line); 1770 1771 /* package */ static class Ellipsizer implements CharSequence, GetChars { 1772 /* package */ CharSequence mText; 1773 /* package */ Layout mLayout; 1774 /* package */ int mWidth; 1775 /* package */ TextUtils.TruncateAt mMethod; 1776 Ellipsizer(CharSequence s)1777 public Ellipsizer(CharSequence s) { 1778 mText = s; 1779 } 1780 charAt(int off)1781 public char charAt(int off) { 1782 char[] buf = TextUtils.obtain(1); 1783 getChars(off, off + 1, buf, 0); 1784 char ret = buf[0]; 1785 1786 TextUtils.recycle(buf); 1787 return ret; 1788 } 1789 getChars(int start, int end, char[] dest, int destoff)1790 public void getChars(int start, int end, char[] dest, int destoff) { 1791 int line1 = mLayout.getLineForOffset(start); 1792 int line2 = mLayout.getLineForOffset(end); 1793 1794 TextUtils.getChars(mText, start, end, dest, destoff); 1795 1796 for (int i = line1; i <= line2; i++) { 1797 mLayout.ellipsize(start, end, i, dest, destoff, mMethod); 1798 } 1799 } 1800 length()1801 public int length() { 1802 return mText.length(); 1803 } 1804 subSequence(int start, int end)1805 public CharSequence subSequence(int start, int end) { 1806 char[] s = new char[end - start]; 1807 getChars(start, end, s, 0); 1808 return new String(s); 1809 } 1810 1811 @Override toString()1812 public String toString() { 1813 char[] s = new char[length()]; 1814 getChars(0, length(), s, 0); 1815 return new String(s); 1816 } 1817 1818 } 1819 1820 /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned { 1821 private Spanned mSpanned; 1822 SpannedEllipsizer(CharSequence display)1823 public SpannedEllipsizer(CharSequence display) { 1824 super(display); 1825 mSpanned = (Spanned) display; 1826 } 1827 getSpans(int start, int end, Class<T> type)1828 public <T> T[] getSpans(int start, int end, Class<T> type) { 1829 return mSpanned.getSpans(start, end, type); 1830 } 1831 getSpanStart(Object tag)1832 public int getSpanStart(Object tag) { 1833 return mSpanned.getSpanStart(tag); 1834 } 1835 getSpanEnd(Object tag)1836 public int getSpanEnd(Object tag) { 1837 return mSpanned.getSpanEnd(tag); 1838 } 1839 getSpanFlags(Object tag)1840 public int getSpanFlags(Object tag) { 1841 return mSpanned.getSpanFlags(tag); 1842 } 1843 1844 @SuppressWarnings("rawtypes") nextSpanTransition(int start, int limit, Class type)1845 public int nextSpanTransition(int start, int limit, Class type) { 1846 return mSpanned.nextSpanTransition(start, limit, type); 1847 } 1848 1849 @Override subSequence(int start, int end)1850 public CharSequence subSequence(int start, int end) { 1851 char[] s = new char[end - start]; 1852 getChars(start, end, s, 0); 1853 1854 SpannableString ss = new SpannableString(new String(s)); 1855 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); 1856 return ss; 1857 } 1858 } 1859 1860 private CharSequence mText; 1861 private TextPaint mPaint; 1862 /* package */ TextPaint mWorkPaint; 1863 private int mWidth; 1864 private Alignment mAlignment = Alignment.ALIGN_NORMAL; 1865 private float mSpacingMult; 1866 private float mSpacingAdd; 1867 private static final Rect sTempRect = new Rect(); 1868 private boolean mSpannedText; 1869 private TextDirectionHeuristic mTextDir; 1870 private SpanSet<LineBackgroundSpan> mLineBackgroundSpans; 1871 1872 public static final int DIR_LEFT_TO_RIGHT = 1; 1873 public static final int DIR_RIGHT_TO_LEFT = -1; 1874 1875 /* package */ static final int DIR_REQUEST_LTR = 1; 1876 /* package */ static final int DIR_REQUEST_RTL = -1; 1877 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; 1878 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; 1879 1880 /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; 1881 /* package */ static final int RUN_LEVEL_SHIFT = 26; 1882 /* package */ static final int RUN_LEVEL_MASK = 0x3f; 1883 /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; 1884 1885 public enum Alignment { 1886 ALIGN_NORMAL, 1887 ALIGN_OPPOSITE, 1888 ALIGN_CENTER, 1889 /** @hide */ 1890 ALIGN_LEFT, 1891 /** @hide */ 1892 ALIGN_RIGHT, 1893 } 1894 1895 private static final int TAB_INCREMENT = 20; 1896 1897 /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = 1898 new Directions(new int[] { 0, RUN_LENGTH_MASK }); 1899 /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = 1900 new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); 1901 1902 /* package */ static final char[] ELLIPSIS_NORMAL = { '\u2026' }; // this is "..." 1903 /* package */ static final char[] ELLIPSIS_TWO_DOTS = { '\u2025' }; // this is ".." 1904 } 1905