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