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 */ getDesiredWidth(CharSequence source, TextPaint paint)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 */ getDesiredWidth(CharSequence source, int start, int end, TextPaint paint)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 */ Layout(CharSequence text, TextPaint paint, int width, Alignment align, float spacingMult, float spacingAdd)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 */ Layout(CharSequence text, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingMult, float spacingAdd)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 */ replaceWith(CharSequence text, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd)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 */ draw(Canvas c)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 */ draw(Canvas canvas, Path highlight, Paint highlightPaint, int cursorOffsetVertical)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 */ drawText(Canvas canvas, int firstLine, int lastLine)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 */ drawBackground(Canvas canvas, Path highlight, Paint highlightPaint, int cursorOffsetVertical, int firstLine, int lastLine)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 */ getLineRangeForDraw(Canvas canvas)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 */ getLineStartPos(int line, int left, int right)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 */ getText()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 */ getPaint()593 public final TextPaint getPaint() { 594 return mPaint; 595 } 596 597 /** 598 * Return the width of this layout. 599 */ getWidth()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 */ getEllipsizedWidth()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 */ increaseWidthTo(int wid)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 */ getHeight()628 public int getHeight() { 629 return getLineTop(getLineCount()); 630 } 631 632 /** 633 * Return the base alignment of this layout. 634 */ getAlignment()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 */ getSpacingMultiplier()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 */ getSpacingAdd()649 public final float getSpacingAdd() { 650 return mSpacingAdd; 651 } 652 653 /** 654 * Return the heuristic used to determine paragraph text direction. 655 * @hide 656 */ getTextDirectionHeuristic()657 public final TextDirectionHeuristic getTextDirectionHeuristic() { 658 return mTextDir; 659 } 660 661 /** 662 * Return the number of lines of text in this layout. 663 */ getLineCount()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 */ getLineBounds(int line, Rect bounds)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 */ getLineTop(int line)690 public abstract int getLineTop(int line); 691 692 /** 693 * Return the descent of the specified line(0…getLineCount() - 1). 694 */ getLineDescent(int line)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 */ getLineStart(int line)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 */ getParagraphDirection(int line)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 */ getLineContainsTab(int line)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 */ getLineDirections(int line)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 */ getTopPadding()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 */ getBottomPadding()737 public abstract int getBottomPadding(); 738 739 /** 740 * Returns the hyphen edit for a line. 741 * 742 * @hide 743 */ getHyphen(int line)744 public int getHyphen(int line) { 745 return 0; 746 } 747 748 /** 749 * Returns the left indent for a line. 750 * 751 * @hide 752 */ getIndentAdjust(int line, Alignment alignment)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 */ isLevelBoundary(int offset)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 */ isRtlCharAt(int offset)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 primaryIsTrailingPrevious(int offset)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 */ getPrimaryHorizontal(int offset)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 */ getPrimaryHorizontal(int offset, boolean clamped)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 */ getSecondaryHorizontal(int offset)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 */ getSecondaryHorizontal(int offset, boolean clamped)899 public float getSecondaryHorizontal(int offset, boolean clamped) { 900 boolean trailing = primaryIsTrailingPrevious(offset); 901 return getHorizontal(offset, !trailing, clamped); 902 } 903 getHorizontal(int offset, boolean trailing, boolean clamped)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 getHorizontal(int offset, boolean trailing, int line, boolean clamped)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 */ getLineLeft(int line)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 */ getLineRight(int line)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 */ getLineMax(int line)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 */ getLineWidth(int line)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 */ getLineExtent(int line, boolean full)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 */ getLineExtent(int line, TabStops tabStops, boolean full)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. getLineForVertical(int vertical)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 */ getLineForOffset(int offset)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 */ getOffsetForHorizontal(int line, float horiz)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 */ getLineEnd(int line)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 */ getLineVisibleEnd(int line)1215 public int getLineVisibleEnd(int line) { 1216 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 1217 } 1218 getLineVisibleEnd(int line, int start, int end)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 */ getLineBottom(int line)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 */ getLineBaseline(int line)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 */ getLineAscent(int line)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 getOffsetToLeftOf(int offset)1269 public int getOffsetToLeftOf(int offset) { 1270 return getOffsetToLeftRightOf(offset, true); 1271 } 1272 getOffsetToRightOf(int offset)1273 public int getOffsetToRightOf(int offset) { 1274 return getOffsetToLeftRightOf(offset, false); 1275 } 1276 getOffsetToLeftRightOf(int caret, boolean toLeft)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 getOffsetAtStartOf(int offset)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 */ shouldClampCursor(int line)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 */ getCursorPath(int point, Path dest, CharSequence editingBuffer)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 addSelection(int line, int start, int end, int top, int bottom, Path dest)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 */ getSelectionPath(int start, int end, Path dest)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 */ getParagraphAlignment(int line)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 */ getParagraphLeft(int line)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 */ getParagraphRight(int line)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 */ getParagraphLeadingMargin(int line)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