1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.text; 18 19import android.annotation.IntDef; 20import android.graphics.Canvas; 21import android.graphics.Paint; 22import android.graphics.Path; 23import android.graphics.Rect; 24import android.text.method.TextKeyListener; 25import android.text.style.AlignmentSpan; 26import android.text.style.LeadingMarginSpan; 27import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; 28import android.text.style.LineBackgroundSpan; 29import android.text.style.ParagraphStyle; 30import android.text.style.ReplacementSpan; 31import android.text.style.TabStopSpan; 32 33import com.android.internal.util.ArrayUtils; 34import com.android.internal.util.GrowingArrayUtils; 35 36import java.lang.annotation.Retention; 37import java.lang.annotation.RetentionPolicy; 38import 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 */ 47public 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 579 public final TextPaint getPaint() { 580 return mPaint; 581 } 582 583 /** 584 * Return the width of this layout. 585 */ 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 */ 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 */ 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 */ 614 public int getHeight() { 615 return getLineTop(getLineCount()); 616 } 617 618 /** 619 * Return the base alignment of this layout. 620 */ 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 */ 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 */ 635 public final float getSpacingAdd() { 636 return mSpacingAdd; 637 } 638 639 /** 640 * Return the heuristic used to determine paragraph text direction. 641 * @hide 642 */ 643 public final TextDirectionHeuristic getTextDirectionHeuristic() { 644 return mTextDir; 645 } 646 647 /** 648 * Return the number of lines of text in this layout. 649 */ 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 */ 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 */ 676 public abstract int getLineTop(int line); 677 678 /** 679 * Return the descent of the specified line(0…getLineCount() - 1). 680 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 722 public abstract int getBottomPadding(); 723 724 /** 725 * Returns the hyphen edit for a line. 726 * 727 * @hide 728 */ 729 public int getHyphen(int line) { 730 return 0; 731 } 732 733 /** 734 * Returns the left indent for a line. 735 * 736 * @hide 737 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 909 public float getSecondaryHorizontal(int offset, boolean clamped) { 910 boolean trailing = primaryIsTrailingPrevious(offset); 911 return getHorizontal(offset, !trailing, clamped); 912 } 913 914 private float getHorizontal(int offset, boolean primary) { 915 return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset); 916 } 917 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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. 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 */ 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 */ 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 */ 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 */ 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 */ 1255 public int getLineVisibleEnd(int line) { 1256 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 1257 } 1258 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 */ 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 */ 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 */ 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 1309 public int getOffsetToLeftOf(int offset) { 1310 return getOffsetToLeftRightOf(offset, true); 1311 } 1312 1313 public int getOffsetToRightOf(int offset) { 1314 return getOffsetToLeftRightOf(offset, false); 1315 } 1316 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 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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