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