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 */ 66 public static float getDesiredWidth(CharSequence source, 67 TextPaint paint) { 68 return getDesiredWidth(source, 0, source.length(), paint); 69 } 70 71 /** 72 * Return how wide a layout must be in order to display the 73 * specified text slice with one line per paragraph. 74 */ 75 public static float getDesiredWidth(CharSequence source, 76 int start, int end, 77 TextPaint paint) { 78 float need = 0; 79 80 int next; 81 for (int i = start; i <= end; i = next) { 82 next = TextUtils.indexOf(source, '\n', i, end); 83 84 if (next < 0) 85 next = end; 86 87 // note, omits trailing paragraph char 88 float w = measurePara(paint, source, i, next); 89 90 if (w > need) 91 need = w; 92 93 next++; 94 } 95 96 return need; 97 } 98 99 /** 100 * Subclasses of Layout use this constructor to set the display text, 101 * width, and other standard properties. 102 * @param text the text to render 103 * @param paint the default paint for the layout. Styles can override 104 * various attributes of the paint. 105 * @param width the wrapping width for the text. 106 * @param align whether to left, right, or center the text. Styles can 107 * override the alignment. 108 * @param spacingMult factor by which to scale the font size to get the 109 * default line spacing 110 * @param spacingAdd amount to add to the default line spacing 111 */ 112 protected Layout(CharSequence text, TextPaint paint, 113 int width, Alignment align, 114 float spacingMult, float spacingAdd) { 115 this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, 116 spacingMult, spacingAdd); 117 } 118 119 /** 120 * Subclasses of Layout use this constructor to set the display text, 121 * width, and other standard properties. 122 * @param text the text to render 123 * @param paint the default paint for the layout. Styles can override 124 * various attributes of the paint. 125 * @param width the wrapping width for the text. 126 * @param align whether to left, right, or center the text. Styles can 127 * override the alignment. 128 * @param spacingMult factor by which to scale the font size to get the 129 * default line spacing 130 * @param spacingAdd amount to add to the default line spacing 131 * 132 * @hide 133 */ 134 protected Layout(CharSequence text, TextPaint paint, 135 int width, Alignment align, TextDirectionHeuristic textDir, 136 float spacingMult, float spacingAdd) { 137 138 if (width < 0) 139 throw new IllegalArgumentException("Layout: " + width + " < 0"); 140 141 // Ensure paint doesn't have baselineShift set. 142 // While normally we don't modify the paint the user passed in, 143 // we were already doing this in Styled.drawUniformRun with both 144 // baselineShift and bgColor. We probably should reevaluate bgColor. 145 if (paint != null) { 146 paint.bgColor = 0; 147 paint.baselineShift = 0; 148 } 149 150 mText = text; 151 mPaint = paint; 152 mWorkPaint = new TextPaint(); 153 mWidth = width; 154 mAlignment = align; 155 mSpacingMult = spacingMult; 156 mSpacingAdd = spacingAdd; 157 mSpannedText = text instanceof Spanned; 158 mTextDir = textDir; 159 } 160 161 /** 162 * Replace constructor properties of this Layout with new ones. Be careful. 163 */ 164 /* package */ void replaceWith(CharSequence text, TextPaint paint, 165 int width, Alignment align, 166 float spacingmult, float spacingadd) { 167 if (width < 0) { 168 throw new IllegalArgumentException("Layout: " + width + " < 0"); 169 } 170 171 mText = text; 172 mPaint = paint; 173 mWidth = width; 174 mAlignment = align; 175 mSpacingMult = spacingmult; 176 mSpacingAdd = spacingadd; 177 mSpannedText = text instanceof Spanned; 178 } 179 180 /** 181 * Draw this Layout on the specified Canvas. 182 */ 183 public void draw(Canvas c) { 184 draw(c, null, null, 0); 185 } 186 187 /** 188 * Draw this Layout on the specified canvas, with the highlight path drawn 189 * between the background and the text. 190 * 191 * @param canvas the canvas 192 * @param highlight the path of the highlight or cursor; can be null 193 * @param highlightPaint the paint for the highlight 194 * @param cursorOffsetVertical the amount to temporarily translate the 195 * canvas while rendering the highlight 196 */ 197 public void draw(Canvas canvas, Path highlight, Paint highlightPaint, 198 int cursorOffsetVertical) { 199 final long lineRange = getLineRangeForDraw(canvas); 200 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); 201 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); 202 if (lastLine < 0) return; 203 204 drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical, 205 firstLine, lastLine); 206 drawText(canvas, firstLine, lastLine); 207 } 208 209 /** 210 * @hide 211 */ 212 public void drawText(Canvas canvas, int firstLine, int lastLine) { 213 int previousLineBottom = getLineTop(firstLine); 214 int previousLineEnd = getLineStart(firstLine); 215 ParagraphStyle[] spans = NO_PARA_SPANS; 216 int spanEnd = 0; 217 TextPaint paint = mPaint; 218 CharSequence buf = mText; 219 220 Alignment paraAlign = mAlignment; 221 TabStops tabStops = null; 222 boolean tabStopsIsInitialized = false; 223 224 TextLine tl = TextLine.obtain(); 225 226 // Draw the lines, one at a time. 227 // The baseline is the top of the following line minus the current line's descent. 228 for (int i = firstLine; i <= lastLine; i++) { 229 int start = previousLineEnd; 230 previousLineEnd = getLineStart(i + 1); 231 int end = getLineVisibleEnd(i, start, previousLineEnd); 232 233 int ltop = previousLineBottom; 234 int lbottom = getLineTop(i+1); 235 previousLineBottom = lbottom; 236 int lbaseline = lbottom - getLineDescent(i); 237 238 int dir = getParagraphDirection(i); 239 int left = 0; 240 int right = mWidth; 241 242 if (mSpannedText) { 243 Spanned sp = (Spanned) buf; 244 int textLength = buf.length(); 245 boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n'); 246 247 // New batch of paragraph styles, collect into spans array. 248 // Compute the alignment, last alignment style wins. 249 // Reset tabStops, we'll rebuild if we encounter a line with 250 // tabs. 251 // We expect paragraph spans to be relatively infrequent, use 252 // spanEnd so that we can check less frequently. Since 253 // paragraph styles ought to apply to entire paragraphs, we can 254 // just collect the ones present at the start of the paragraph. 255 // If spanEnd is before the end of the paragraph, that's not 256 // our problem. 257 if (start >= spanEnd && (i == firstLine || isFirstParaLine)) { 258 spanEnd = sp.nextSpanTransition(start, textLength, 259 ParagraphStyle.class); 260 spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class); 261 262 paraAlign = mAlignment; 263 for (int n = spans.length - 1; n >= 0; n--) { 264 if (spans[n] instanceof AlignmentSpan) { 265 paraAlign = ((AlignmentSpan) spans[n]).getAlignment(); 266 break; 267 } 268 } 269 270 tabStopsIsInitialized = false; 271 } 272 273 // Draw all leading margin spans. Adjust left or right according 274 // to the paragraph direction of the line. 275 final int length = spans.length; 276 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 */ 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 */ 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 */ 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 */ 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 */ 534 public final TextPaint getPaint() { 535 return mPaint; 536 } 537 538 /** 539 * Return the width of this layout. 540 */ 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 */ 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 */ 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 */ 569 public int getHeight() { 570 return getLineTop(getLineCount()); 571 } 572 573 /** 574 * Return the base alignment of this layout. 575 */ 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 */ 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 */ 590 public final float getSpacingAdd() { 591 return mSpacingAdd; 592 } 593 594 /** 595 * Return the heuristic used to determine paragraph text direction. 596 * @hide 597 */ 598 public final TextDirectionHeuristic getTextDirectionHeuristic() { 599 return mTextDir; 600 } 601 602 /** 603 * Return the number of lines of text in this layout. 604 */ 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 */ 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 */ 631 public abstract int getLineTop(int line); 632 633 /** 634 * Return the descent of the specified line(0…getLineCount() - 1). 635 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 823 public float getSecondaryHorizontal(int offset, boolean clamped) { 824 boolean trailing = primaryIsTrailingPrevious(offset); 825 return getHorizontal(offset, !trailing, clamped); 826 } 827 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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. 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 */ 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 */ 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 */ 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 */ 1138 public int getLineVisibleEnd(int line) { 1139 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 1140 } 1141 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 */ 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 */ 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 */ 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 1189 public int getOffsetToLeftOf(int offset) { 1190 return getOffsetToLeftRightOf(offset, true); 1191 } 1192 1193 public int getOffsetToRightOf(int offset) { 1194 return getOffsetToLeftRightOf(offset, false); 1195 } 1196 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 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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