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