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