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