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