Layout.java revision fa05ba0b0d39fae1d2cb3d98fbee0aef6a9fed88
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 private boolean primaryIsTrailingPrevious(int offset) { 803 int line = getLineForOffset(offset); 804 int lineStart = getLineStart(line); 805 int lineEnd = getLineEnd(line); 806 int[] runs = getLineDirections(line).mDirections; 807 808 int levelAt = -1; 809 for (int i = 0; i < runs.length; i += 2) { 810 int start = lineStart + runs[i]; 811 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 812 if (limit > lineEnd) { 813 limit = lineEnd; 814 } 815 if (offset >= start && offset < limit) { 816 if (offset > start) { 817 // Previous character is at same level, so don't use trailing. 818 return false; 819 } 820 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 821 break; 822 } 823 } 824 if (levelAt == -1) { 825 // Offset was limit of line. 826 levelAt = getParagraphDirection(line) == 1 ? 0 : 1; 827 } 828 829 // At level boundary, check previous level. 830 int levelBefore = -1; 831 if (offset == lineStart) { 832 levelBefore = getParagraphDirection(line) == 1 ? 0 : 1; 833 } else { 834 offset -= 1; 835 for (int i = 0; i < runs.length; i += 2) { 836 int start = lineStart + runs[i]; 837 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 838 if (limit > lineEnd) { 839 limit = lineEnd; 840 } 841 if (offset >= start && offset < limit) { 842 levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 843 break; 844 } 845 } 846 } 847 848 return levelBefore < levelAt; 849 } 850 851 /** 852 * Get the primary horizontal position for the specified text offset. 853 * This is the location where a new character would be inserted in 854 * the paragraph's primary direction. 855 */ 856 public float getPrimaryHorizontal(int offset) { 857 return getPrimaryHorizontal(offset, false /* not clamped */); 858 } 859 860 /** 861 * Get the primary horizontal position for the specified text offset, but 862 * optionally clamp it so that it doesn't exceed the width of the layout. 863 * @hide 864 */ 865 public float getPrimaryHorizontal(int offset, boolean clamped) { 866 boolean trailing = primaryIsTrailingPrevious(offset); 867 return getHorizontal(offset, trailing, clamped); 868 } 869 870 /** 871 * Get the secondary horizontal position for the specified text offset. 872 * This is the location where a new character would be inserted in 873 * the direction other than the paragraph's primary direction. 874 */ 875 public float getSecondaryHorizontal(int offset) { 876 return getSecondaryHorizontal(offset, false /* not clamped */); 877 } 878 879 /** 880 * Get the secondary horizontal position for the specified text offset, but 881 * optionally clamp it so that it doesn't exceed the width of the layout. 882 * @hide 883 */ 884 public float getSecondaryHorizontal(int offset, boolean clamped) { 885 boolean trailing = primaryIsTrailingPrevious(offset); 886 return getHorizontal(offset, !trailing, clamped); 887 } 888 889 private float getHorizontal(int offset, boolean trailing, boolean clamped) { 890 int line = getLineForOffset(offset); 891 892 return getHorizontal(offset, trailing, line, clamped); 893 } 894 895 private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) { 896 int start = getLineStart(line); 897 int end = getLineEnd(line); 898 int dir = getParagraphDirection(line); 899 boolean hasTab = getLineContainsTab(line); 900 Directions directions = getLineDirections(line); 901 902 TabStops tabStops = null; 903 if (hasTab && mText instanceof Spanned) { 904 // Just checking this line should be good enough, tabs should be 905 // consistent across all lines in a paragraph. 906 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 907 if (tabs.length > 0) { 908 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 909 } 910 } 911 912 TextLine tl = TextLine.obtain(); 913 tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops); 914 float wid = tl.measure(offset - start, trailing, null); 915 TextLine.recycle(tl); 916 917 if (clamped && wid > mWidth) { 918 wid = mWidth; 919 } 920 int left = getParagraphLeft(line); 921 int right = getParagraphRight(line); 922 923 return getLineStartPos(line, left, right) + wid; 924 } 925 926 /** 927 * Get the leftmost position that should be exposed for horizontal 928 * scrolling on the specified line. 929 */ 930 public float getLineLeft(int line) { 931 int dir = getParagraphDirection(line); 932 Alignment align = getParagraphAlignment(line); 933 934 if (align == Alignment.ALIGN_LEFT) { 935 return 0; 936 } else if (align == Alignment.ALIGN_NORMAL) { 937 if (dir == DIR_RIGHT_TO_LEFT) 938 return getParagraphRight(line) - getLineMax(line); 939 else 940 return 0; 941 } else if (align == Alignment.ALIGN_RIGHT) { 942 return mWidth - getLineMax(line); 943 } else if (align == Alignment.ALIGN_OPPOSITE) { 944 if (dir == DIR_RIGHT_TO_LEFT) 945 return 0; 946 else 947 return mWidth - getLineMax(line); 948 } else { /* align == Alignment.ALIGN_CENTER */ 949 int left = getParagraphLeft(line); 950 int right = getParagraphRight(line); 951 int max = ((int) getLineMax(line)) & ~1; 952 953 return left + ((right - left) - max) / 2; 954 } 955 } 956 957 /** 958 * Get the rightmost position that should be exposed for horizontal 959 * scrolling on the specified line. 960 */ 961 public float getLineRight(int line) { 962 int dir = getParagraphDirection(line); 963 Alignment align = getParagraphAlignment(line); 964 965 if (align == Alignment.ALIGN_LEFT) { 966 return getParagraphLeft(line) + getLineMax(line); 967 } else if (align == Alignment.ALIGN_NORMAL) { 968 if (dir == DIR_RIGHT_TO_LEFT) 969 return mWidth; 970 else 971 return getParagraphLeft(line) + getLineMax(line); 972 } else if (align == Alignment.ALIGN_RIGHT) { 973 return mWidth; 974 } else if (align == Alignment.ALIGN_OPPOSITE) { 975 if (dir == DIR_RIGHT_TO_LEFT) 976 return getLineMax(line); 977 else 978 return mWidth; 979 } else { /* align == Alignment.ALIGN_CENTER */ 980 int left = getParagraphLeft(line); 981 int right = getParagraphRight(line); 982 int max = ((int) getLineMax(line)) & ~1; 983 984 return right - ((right - left) - max) / 2; 985 } 986 } 987 988 /** 989 * Gets the unsigned horizontal extent of the specified line, including 990 * leading margin indent, but excluding trailing whitespace. 991 */ 992 public float getLineMax(int line) { 993 float margin = getParagraphLeadingMargin(line); 994 float signedExtent = getLineExtent(line, false); 995 return margin + (signedExtent >= 0 ? signedExtent : -signedExtent); 996 } 997 998 /** 999 * Gets the unsigned horizontal extent of the specified line, including 1000 * leading margin indent and trailing whitespace. 1001 */ 1002 public float getLineWidth(int line) { 1003 float margin = getParagraphLeadingMargin(line); 1004 float signedExtent = getLineExtent(line, true); 1005 return margin + (signedExtent >= 0 ? signedExtent : -signedExtent); 1006 } 1007 1008 /** 1009 * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the 1010 * tab stops instead of using the ones passed in. 1011 * @param line the index of the line 1012 * @param full whether to include trailing whitespace 1013 * @return the extent of the line 1014 */ 1015 private float getLineExtent(int line, boolean full) { 1016 int start = getLineStart(line); 1017 int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 1018 1019 boolean hasTabs = getLineContainsTab(line); 1020 TabStops tabStops = null; 1021 if (hasTabs && mText instanceof Spanned) { 1022 // Just checking this line should be good enough, tabs should be 1023 // consistent across all lines in a paragraph. 1024 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 1025 if (tabs.length > 0) { 1026 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1027 } 1028 } 1029 Directions directions = getLineDirections(line); 1030 // Returned directions can actually be null 1031 if (directions == null) { 1032 return 0f; 1033 } 1034 int dir = getParagraphDirection(line); 1035 1036 TextLine tl = TextLine.obtain(); 1037 tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops); 1038 float width = tl.metrics(null); 1039 TextLine.recycle(tl); 1040 return width; 1041 } 1042 1043 /** 1044 * Returns the signed horizontal extent of the specified line, excluding 1045 * leading margin. If full is false, excludes trailing whitespace. 1046 * @param line the index of the line 1047 * @param tabStops the tab stops, can be null if we know they're not used. 1048 * @param full whether to include trailing whitespace 1049 * @return the extent of the text on this line 1050 */ 1051 private float getLineExtent(int line, TabStops tabStops, boolean full) { 1052 int start = getLineStart(line); 1053 int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 1054 boolean hasTabs = getLineContainsTab(line); 1055 Directions directions = getLineDirections(line); 1056 int dir = getParagraphDirection(line); 1057 1058 TextLine tl = TextLine.obtain(); 1059 tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops); 1060 float width = tl.metrics(null); 1061 TextLine.recycle(tl); 1062 return width; 1063 } 1064 1065 /** 1066 * Get the line number corresponding to the specified vertical position. 1067 * If you ask for a position above 0, you get 0; if you ask for a position 1068 * below the bottom of the text, you get the last line. 1069 */ 1070 // FIXME: It may be faster to do a linear search for layouts without many lines. 1071 public int getLineForVertical(int vertical) { 1072 int high = getLineCount(), low = -1, guess; 1073 1074 while (high - low > 1) { 1075 guess = (high + low) / 2; 1076 1077 if (getLineTop(guess) > vertical) 1078 high = guess; 1079 else 1080 low = guess; 1081 } 1082 1083 if (low < 0) 1084 return 0; 1085 else 1086 return low; 1087 } 1088 1089 /** 1090 * Get the line number on which the specified text offset appears. 1091 * If you ask for a position before 0, you get 0; if you ask for a position 1092 * beyond the end of the text, you get the last line. 1093 */ 1094 public int getLineForOffset(int offset) { 1095 int high = getLineCount(), low = -1, guess; 1096 1097 while (high - low > 1) { 1098 guess = (high + low) / 2; 1099 1100 if (getLineStart(guess) > offset) 1101 high = guess; 1102 else 1103 low = guess; 1104 } 1105 1106 if (low < 0) 1107 return 0; 1108 else 1109 return low; 1110 } 1111 1112 /** 1113 * Get the character offset on the specified line whose position is 1114 * closest to the specified horizontal position. 1115 */ 1116 public int getOffsetForHorizontal(int line, float horiz) { 1117 // TODO: use Paint.getOffsetForAdvance to avoid binary search 1118 final int lineEndOffset = getLineEnd(line); 1119 final int lineStartOffset = getLineStart(line); 1120 1121 Directions dirs = getLineDirections(line); 1122 1123 TextLine tl = TextLine.obtain(); 1124 // XXX: we don't care about tabs as we just use TextLine#getOffsetToLeftRightOf here. 1125 tl.set(mPaint, mText, lineStartOffset, lineEndOffset, getParagraphDirection(line), dirs, 1126 false, null); 1127 1128 final int max; 1129 if (line == getLineCount() - 1) { 1130 max = lineEndOffset; 1131 } else { 1132 max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset, 1133 !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset; 1134 } 1135 int best = lineStartOffset; 1136 float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); 1137 1138 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1139 int here = lineStartOffset + dirs.mDirections[i]; 1140 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1141 boolean isRtl = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0; 1142 int swap = isRtl ? -1 : 1; 1143 1144 if (there > max) 1145 there = max; 1146 int high = there - 1 + 1, low = here + 1 - 1, guess; 1147 1148 while (high - low > 1) { 1149 guess = (high + low) / 2; 1150 int adguess = getOffsetAtStartOf(guess); 1151 1152 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap) 1153 high = guess; 1154 else 1155 low = guess; 1156 } 1157 1158 if (low < here + 1) 1159 low = here + 1; 1160 1161 if (low < there) { 1162 int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset; 1163 low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset; 1164 if (low >= here && low < there) { 1165 float dist = Math.abs(getPrimaryHorizontal(low) - horiz); 1166 if (aft < there) { 1167 float other = Math.abs(getPrimaryHorizontal(aft) - horiz); 1168 1169 if (other < dist) { 1170 dist = other; 1171 low = aft; 1172 } 1173 } 1174 1175 if (dist < bestdist) { 1176 bestdist = dist; 1177 best = low; 1178 } 1179 } 1180 } 1181 1182 float dist = Math.abs(getPrimaryHorizontal(here) - horiz); 1183 1184 if (dist < bestdist) { 1185 bestdist = dist; 1186 best = here; 1187 } 1188 } 1189 1190 float dist = Math.abs(getPrimaryHorizontal(max) - horiz); 1191 1192 if (dist <= bestdist) { 1193 bestdist = dist; 1194 best = max; 1195 } 1196 1197 TextLine.recycle(tl); 1198 return best; 1199 } 1200 1201 /** 1202 * Return the text offset after the last character on the specified line. 1203 */ 1204 public final int getLineEnd(int line) { 1205 return getLineStart(line + 1); 1206 } 1207 1208 /** 1209 * Return the text offset after the last visible character (so whitespace 1210 * is not counted) on the specified line. 1211 */ 1212 public int getLineVisibleEnd(int line) { 1213 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 1214 } 1215 1216 private int getLineVisibleEnd(int line, int start, int end) { 1217 CharSequence text = mText; 1218 char ch; 1219 if (line == getLineCount() - 1) { 1220 return end; 1221 } 1222 1223 for (; end > start; end--) { 1224 ch = text.charAt(end - 1); 1225 1226 if (ch == '\n') { 1227 return end - 1; 1228 } 1229 1230 // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace() 1231 if (!(ch == ' ' || ch == '\t' || ch == 0x1680 || 1232 (0x2000 <= ch && ch <= 0x200A && ch != 0x2007) || 1233 ch == 0x205F || ch == 0x3000)) { 1234 break; 1235 } 1236 1237 } 1238 1239 return end; 1240 } 1241 1242 /** 1243 * Return the vertical position of the bottom of the specified line. 1244 */ 1245 public final int getLineBottom(int line) { 1246 return getLineTop(line + 1); 1247 } 1248 1249 /** 1250 * Return the vertical position of the baseline of the specified line. 1251 */ 1252 public final int getLineBaseline(int line) { 1253 // getLineTop(line+1) == getLineTop(line) 1254 return getLineTop(line+1) - getLineDescent(line); 1255 } 1256 1257 /** 1258 * Get the ascent of the text on the specified line. 1259 * The return value is negative to match the Paint.ascent() convention. 1260 */ 1261 public final int getLineAscent(int line) { 1262 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) 1263 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); 1264 } 1265 1266 public int getOffsetToLeftOf(int offset) { 1267 return getOffsetToLeftRightOf(offset, true); 1268 } 1269 1270 public int getOffsetToRightOf(int offset) { 1271 return getOffsetToLeftRightOf(offset, false); 1272 } 1273 1274 private int getOffsetToLeftRightOf(int caret, boolean toLeft) { 1275 int line = getLineForOffset(caret); 1276 int lineStart = getLineStart(line); 1277 int lineEnd = getLineEnd(line); 1278 int lineDir = getParagraphDirection(line); 1279 1280 boolean lineChanged = false; 1281 boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); 1282 // if walking off line, look at the line we're headed to 1283 if (advance) { 1284 if (caret == lineEnd) { 1285 if (line < getLineCount() - 1) { 1286 lineChanged = true; 1287 ++line; 1288 } else { 1289 return caret; // at very end, don't move 1290 } 1291 } 1292 } else { 1293 if (caret == lineStart) { 1294 if (line > 0) { 1295 lineChanged = true; 1296 --line; 1297 } else { 1298 return caret; // at very start, don't move 1299 } 1300 } 1301 } 1302 1303 if (lineChanged) { 1304 lineStart = getLineStart(line); 1305 lineEnd = getLineEnd(line); 1306 int newDir = getParagraphDirection(line); 1307 if (newDir != lineDir) { 1308 // unusual case. we want to walk onto the line, but it runs 1309 // in a different direction than this one, so we fake movement 1310 // in the opposite direction. 1311 toLeft = !toLeft; 1312 lineDir = newDir; 1313 } 1314 } 1315 1316 Directions directions = getLineDirections(line); 1317 1318 TextLine tl = TextLine.obtain(); 1319 // XXX: we don't care about tabs 1320 tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null); 1321 caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); 1322 tl = TextLine.recycle(tl); 1323 return caret; 1324 } 1325 1326 private int getOffsetAtStartOf(int offset) { 1327 // XXX this probably should skip local reorderings and 1328 // zero-width characters, look at callers 1329 if (offset == 0) 1330 return 0; 1331 1332 CharSequence text = mText; 1333 char c = text.charAt(offset); 1334 1335 if (c >= '\uDC00' && c <= '\uDFFF') { 1336 char c1 = text.charAt(offset - 1); 1337 1338 if (c1 >= '\uD800' && c1 <= '\uDBFF') 1339 offset -= 1; 1340 } 1341 1342 if (mSpannedText) { 1343 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 1344 ReplacementSpan.class); 1345 1346 for (int i = 0; i < spans.length; i++) { 1347 int start = ((Spanned) text).getSpanStart(spans[i]); 1348 int end = ((Spanned) text).getSpanEnd(spans[i]); 1349 1350 if (start < offset && end > offset) 1351 offset = start; 1352 } 1353 } 1354 1355 return offset; 1356 } 1357 1358 /** 1359 * Determine whether we should clamp cursor position. Currently it's 1360 * only robust for left-aligned displays. 1361 * @hide 1362 */ 1363 public boolean shouldClampCursor(int line) { 1364 // Only clamp cursor position in left-aligned displays. 1365 switch (getParagraphAlignment(line)) { 1366 case ALIGN_LEFT: 1367 return true; 1368 case ALIGN_NORMAL: 1369 return getParagraphDirection(line) > 0; 1370 default: 1371 return false; 1372 } 1373 1374 } 1375 /** 1376 * Fills in the specified Path with a representation of a cursor 1377 * at the specified offset. This will often be a vertical line 1378 * but can be multiple discontinuous lines in text with multiple 1379 * directionalities. 1380 */ 1381 public void getCursorPath(int point, Path dest, 1382 CharSequence editingBuffer) { 1383 dest.reset(); 1384 1385 int line = getLineForOffset(point); 1386 int top = getLineTop(line); 1387 int bottom = getLineTop(line+1); 1388 1389 boolean clamped = shouldClampCursor(line); 1390 float h1 = getPrimaryHorizontal(point, clamped) - 0.5f; 1391 float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1; 1392 1393 int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | 1394 TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); 1395 int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON); 1396 int dist = 0; 1397 1398 if (caps != 0 || fn != 0) { 1399 dist = (bottom - top) >> 2; 1400 1401 if (fn != 0) 1402 top += dist; 1403 if (caps != 0) 1404 bottom -= dist; 1405 } 1406 1407 if (h1 < 0.5f) 1408 h1 = 0.5f; 1409 if (h2 < 0.5f) 1410 h2 = 0.5f; 1411 1412 if (Float.compare(h1, h2) == 0) { 1413 dest.moveTo(h1, top); 1414 dest.lineTo(h1, bottom); 1415 } else { 1416 dest.moveTo(h1, top); 1417 dest.lineTo(h1, (top + bottom) >> 1); 1418 1419 dest.moveTo(h2, (top + bottom) >> 1); 1420 dest.lineTo(h2, bottom); 1421 } 1422 1423 if (caps == 2) { 1424 dest.moveTo(h2, bottom); 1425 dest.lineTo(h2 - dist, bottom + dist); 1426 dest.lineTo(h2, bottom); 1427 dest.lineTo(h2 + dist, bottom + dist); 1428 } else if (caps == 1) { 1429 dest.moveTo(h2, bottom); 1430 dest.lineTo(h2 - dist, bottom + dist); 1431 1432 dest.moveTo(h2 - dist, bottom + dist - 0.5f); 1433 dest.lineTo(h2 + dist, bottom + dist - 0.5f); 1434 1435 dest.moveTo(h2 + dist, bottom + dist); 1436 dest.lineTo(h2, bottom); 1437 } 1438 1439 if (fn == 2) { 1440 dest.moveTo(h1, top); 1441 dest.lineTo(h1 - dist, top - dist); 1442 dest.lineTo(h1, top); 1443 dest.lineTo(h1 + dist, top - dist); 1444 } else if (fn == 1) { 1445 dest.moveTo(h1, top); 1446 dest.lineTo(h1 - dist, top - dist); 1447 1448 dest.moveTo(h1 - dist, top - dist + 0.5f); 1449 dest.lineTo(h1 + dist, top - dist + 0.5f); 1450 1451 dest.moveTo(h1 + dist, top - dist); 1452 dest.lineTo(h1, top); 1453 } 1454 } 1455 1456 private void addSelection(int line, int start, int end, 1457 int top, int bottom, Path dest) { 1458 int linestart = getLineStart(line); 1459 int lineend = getLineEnd(line); 1460 Directions dirs = getLineDirections(line); 1461 1462 if (lineend > linestart && mText.charAt(lineend - 1) == '\n') 1463 lineend--; 1464 1465 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1466 int here = linestart + dirs.mDirections[i]; 1467 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1468 1469 if (there > lineend) 1470 there = lineend; 1471 1472 if (start <= there && end >= here) { 1473 int st = Math.max(start, here); 1474 int en = Math.min(end, there); 1475 1476 if (st != en) { 1477 float h1 = getHorizontal(st, false, line, false /* not clamped */); 1478 float h2 = getHorizontal(en, true, line, false /* not clamped */); 1479 1480 float left = Math.min(h1, h2); 1481 float right = Math.max(h1, h2); 1482 1483 dest.addRect(left, top, right, bottom, Path.Direction.CW); 1484 } 1485 } 1486 } 1487 } 1488 1489 /** 1490 * Fills in the specified Path with a representation of a highlight 1491 * between the specified offsets. This will often be a rectangle 1492 * or a potentially discontinuous set of rectangles. If the start 1493 * and end are the same, the returned path is empty. 1494 */ 1495 public void getSelectionPath(int start, int end, Path dest) { 1496 dest.reset(); 1497 1498 if (start == end) 1499 return; 1500 1501 if (end < start) { 1502 int temp = end; 1503 end = start; 1504 start = temp; 1505 } 1506 1507 int startline = getLineForOffset(start); 1508 int endline = getLineForOffset(end); 1509 1510 int top = getLineTop(startline); 1511 int bottom = getLineBottom(endline); 1512 1513 if (startline == endline) { 1514 addSelection(startline, start, end, top, bottom, dest); 1515 } else { 1516 final float width = mWidth; 1517 1518 addSelection(startline, start, getLineEnd(startline), 1519 top, getLineBottom(startline), dest); 1520 1521 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) 1522 dest.addRect(getLineLeft(startline), top, 1523 0, getLineBottom(startline), Path.Direction.CW); 1524 else 1525 dest.addRect(getLineRight(startline), top, 1526 width, getLineBottom(startline), Path.Direction.CW); 1527 1528 for (int i = startline + 1; i < endline; i++) { 1529 top = getLineTop(i); 1530 bottom = getLineBottom(i); 1531 dest.addRect(0, top, width, bottom, Path.Direction.CW); 1532 } 1533 1534 top = getLineTop(endline); 1535 bottom = getLineBottom(endline); 1536 1537 addSelection(endline, getLineStart(endline), end, 1538 top, bottom, dest); 1539 1540 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) 1541 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW); 1542 else 1543 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW); 1544 } 1545 } 1546 1547 /** 1548 * Get the alignment of the specified paragraph, taking into account 1549 * markup attached to it. 1550 */ 1551 public final Alignment getParagraphAlignment(int line) { 1552 Alignment align = mAlignment; 1553 1554 if (mSpannedText) { 1555 Spanned sp = (Spanned) mText; 1556 AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line), 1557 getLineEnd(line), 1558 AlignmentSpan.class); 1559 1560 int spanLength = spans.length; 1561 if (spanLength > 0) { 1562 align = spans[spanLength-1].getAlignment(); 1563 } 1564 } 1565 1566 return align; 1567 } 1568 1569 /** 1570 * Get the left edge of the specified paragraph, inset by left margins. 1571 */ 1572 public final int getParagraphLeft(int line) { 1573 int left = 0; 1574 int dir = getParagraphDirection(line); 1575 if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) { 1576 return left; // leading margin has no impact, or no styles 1577 } 1578 return getParagraphLeadingMargin(line); 1579 } 1580 1581 /** 1582 * Get the right edge of the specified paragraph, inset by right margins. 1583 */ 1584 public final int getParagraphRight(int line) { 1585 int right = mWidth; 1586 int dir = getParagraphDirection(line); 1587 if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) { 1588 return right; // leading margin has no impact, or no styles 1589 } 1590 return right - getParagraphLeadingMargin(line); 1591 } 1592 1593 /** 1594 * Returns the effective leading margin (unsigned) for this line, 1595 * taking into account LeadingMarginSpan and LeadingMarginSpan2. 1596 * @param line the line index 1597 * @return the leading margin of this line 1598 */ 1599 private int getParagraphLeadingMargin(int line) { 1600 if (!mSpannedText) { 1601 return 0; 1602 } 1603 Spanned spanned = (Spanned) mText; 1604 1605 int lineStart = getLineStart(line); 1606 int lineEnd = getLineEnd(line); 1607 int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd, 1608 LeadingMarginSpan.class); 1609 LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd, 1610 LeadingMarginSpan.class); 1611 if (spans.length == 0) { 1612 return 0; // no leading margin span; 1613 } 1614 1615 int margin = 0; 1616 1617 boolean isFirstParaLine = lineStart == 0 || 1618 spanned.charAt(lineStart - 1) == '\n'; 1619 1620 boolean useFirstLineMargin = isFirstParaLine; 1621 for (int i = 0; i < spans.length; i++) { 1622 if (spans[i] instanceof LeadingMarginSpan2) { 1623 int spStart = spanned.getSpanStart(spans[i]); 1624 int spanLine = getLineForOffset(spStart); 1625 int count = ((LeadingMarginSpan2) spans[i]).getLeadingMarginLineCount(); 1626 // if there is more than one LeadingMarginSpan2, use the count that is greatest 1627 useFirstLineMargin |= line < spanLine + count; 1628 } 1629 } 1630 for (int i = 0; i < spans.length; i++) { 1631 LeadingMarginSpan span = spans[i]; 1632 margin += span.getLeadingMargin(useFirstLineMargin); 1633 } 1634 1635 return margin; 1636 } 1637 1638 /* package */ 1639 static float measurePara(TextPaint paint, CharSequence text, int start, int end) { 1640 1641 MeasuredText mt = MeasuredText.obtain(); 1642 TextLine tl = TextLine.obtain(); 1643 try { 1644 mt.setPara(text, start, end, TextDirectionHeuristics.LTR, null); 1645 Directions directions; 1646 int dir; 1647 if (mt.mEasy) { 1648 directions = DIRS_ALL_LEFT_TO_RIGHT; 1649 dir = Layout.DIR_LEFT_TO_RIGHT; 1650 } else { 1651 directions = AndroidBidi.directions(mt.mDir, mt.mLevels, 1652 0, mt.mChars, 0, mt.mLen); 1653 dir = mt.mDir; 1654 } 1655 char[] chars = mt.mChars; 1656 int len = mt.mLen; 1657 boolean hasTabs = false; 1658 TabStops tabStops = null; 1659 // leading margins should be taken into account when measuring a paragraph 1660 int margin = 0; 1661 if (text instanceof Spanned) { 1662 Spanned spanned = (Spanned) text; 1663 LeadingMarginSpan[] spans = getParagraphSpans(spanned, start, end, 1664 LeadingMarginSpan.class); 1665 for (LeadingMarginSpan lms : spans) { 1666 margin += lms.getLeadingMargin(true); 1667 } 1668 } 1669 for (int i = 0; i < len; ++i) { 1670 if (chars[i] == '\t') { 1671 hasTabs = true; 1672 if (text instanceof Spanned) { 1673 Spanned spanned = (Spanned) text; 1674 int spanEnd = spanned.nextSpanTransition(start, end, 1675 TabStopSpan.class); 1676 TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd, 1677 TabStopSpan.class); 1678 if (spans.length > 0) { 1679 tabStops = new TabStops(TAB_INCREMENT, spans); 1680 } 1681 } 1682 break; 1683 } 1684 } 1685 tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops); 1686 return margin + tl.metrics(null); 1687 } finally { 1688 TextLine.recycle(tl); 1689 MeasuredText.recycle(mt); 1690 } 1691 } 1692 1693 /** 1694 * @hide 1695 */ 1696 /* package */ static class TabStops { 1697 private int[] mStops; 1698 private int mNumStops; 1699 private int mIncrement; 1700 1701 TabStops(int increment, Object[] spans) { 1702 reset(increment, spans); 1703 } 1704 1705 void reset(int increment, Object[] spans) { 1706 this.mIncrement = increment; 1707 1708 int ns = 0; 1709 if (spans != null) { 1710 int[] stops = this.mStops; 1711 for (Object o : spans) { 1712 if (o instanceof TabStopSpan) { 1713 if (stops == null) { 1714 stops = new int[10]; 1715 } else if (ns == stops.length) { 1716 int[] nstops = new int[ns * 2]; 1717 for (int i = 0; i < ns; ++i) { 1718 nstops[i] = stops[i]; 1719 } 1720 stops = nstops; 1721 } 1722 stops[ns++] = ((TabStopSpan) o).getTabStop(); 1723 } 1724 } 1725 if (ns > 1) { 1726 Arrays.sort(stops, 0, ns); 1727 } 1728 if (stops != this.mStops) { 1729 this.mStops = stops; 1730 } 1731 } 1732 this.mNumStops = ns; 1733 } 1734 1735 float nextTab(float h) { 1736 int ns = this.mNumStops; 1737 if (ns > 0) { 1738 int[] stops = this.mStops; 1739 for (int i = 0; i < ns; ++i) { 1740 int stop = stops[i]; 1741 if (stop > h) { 1742 return stop; 1743 } 1744 } 1745 } 1746 return nextDefaultStop(h, mIncrement); 1747 } 1748 1749 public static float nextDefaultStop(float h, int inc) { 1750 return ((int) ((h + inc) / inc)) * inc; 1751 } 1752 } 1753 1754 /** 1755 * Returns the position of the next tab stop after h on the line. 1756 * 1757 * @param text the text 1758 * @param start start of the line 1759 * @param end limit of the line 1760 * @param h the current horizontal offset 1761 * @param tabs the tabs, can be null. If it is null, any tabs in effect 1762 * on the line will be used. If there are no tabs, a default offset 1763 * will be used to compute the tab stop. 1764 * @return the offset of the next tab stop. 1765 */ 1766 /* package */ static float nextTab(CharSequence text, int start, int end, 1767 float h, Object[] tabs) { 1768 float nh = Float.MAX_VALUE; 1769 boolean alltabs = false; 1770 1771 if (text instanceof Spanned) { 1772 if (tabs == null) { 1773 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class); 1774 alltabs = true; 1775 } 1776 1777 for (int i = 0; i < tabs.length; i++) { 1778 if (!alltabs) { 1779 if (!(tabs[i] instanceof TabStopSpan)) 1780 continue; 1781 } 1782 1783 int where = ((TabStopSpan) tabs[i]).getTabStop(); 1784 1785 if (where < nh && where > h) 1786 nh = where; 1787 } 1788 1789 if (nh != Float.MAX_VALUE) 1790 return nh; 1791 } 1792 1793 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; 1794 } 1795 1796 protected final boolean isSpanned() { 1797 return mSpannedText; 1798 } 1799 1800 /** 1801 * Returns the same as <code>text.getSpans()</code>, except where 1802 * <code>start</code> and <code>end</code> are the same and are not 1803 * at the very beginning of the text, in which case an empty array 1804 * is returned instead. 1805 * <p> 1806 * This is needed because of the special case that <code>getSpans()</code> 1807 * on an empty range returns the spans adjacent to that range, which is 1808 * primarily for the sake of <code>TextWatchers</code> so they will get 1809 * notifications when text goes from empty to non-empty. But it also 1810 * has the unfortunate side effect that if the text ends with an empty 1811 * paragraph, that paragraph accidentally picks up the styles of the 1812 * preceding paragraph (even though those styles will not be picked up 1813 * by new text that is inserted into the empty paragraph). 1814 * <p> 1815 * The reason it just checks whether <code>start</code> and <code>end</code> 1816 * is the same is that the only time a line can contain 0 characters 1817 * is if it is the final paragraph of the Layout; otherwise any line will 1818 * contain at least one printing or newline character. The reason for the 1819 * additional check if <code>start</code> is greater than 0 is that 1820 * if the empty paragraph is the entire content of the buffer, paragraph 1821 * styles that are already applied to the buffer will apply to text that 1822 * is inserted into it. 1823 */ 1824 /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) { 1825 if (start == end && start > 0) { 1826 return ArrayUtils.emptyArray(type); 1827 } 1828 1829 if(text instanceof SpannableStringBuilder) { 1830 return ((SpannableStringBuilder) text).getSpans(start, end, type, false); 1831 } else { 1832 return text.getSpans(start, end, type); 1833 } 1834 } 1835 1836 private char getEllipsisChar(TextUtils.TruncateAt method) { 1837 return (method == TextUtils.TruncateAt.END_SMALL) ? 1838 TextUtils.ELLIPSIS_TWO_DOTS[0] : 1839 TextUtils.ELLIPSIS_NORMAL[0]; 1840 } 1841 1842 private void ellipsize(int start, int end, int line, 1843 char[] dest, int destoff, TextUtils.TruncateAt method) { 1844 int ellipsisCount = getEllipsisCount(line); 1845 1846 if (ellipsisCount == 0) { 1847 return; 1848 } 1849 1850 int ellipsisStart = getEllipsisStart(line); 1851 int linestart = getLineStart(line); 1852 1853 for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { 1854 char c; 1855 1856 if (i == ellipsisStart) { 1857 c = getEllipsisChar(method); // ellipsis 1858 } else { 1859 c = '\uFEFF'; // 0-width space 1860 } 1861 1862 int a = i + linestart; 1863 1864 if (a >= start && a < end) { 1865 dest[destoff + a - start] = c; 1866 } 1867 } 1868 } 1869 1870 /** 1871 * Stores information about bidirectional (left-to-right or right-to-left) 1872 * text within the layout of a line. 1873 */ 1874 public static class Directions { 1875 // Directions represents directional runs within a line of text. 1876 // Runs are pairs of ints listed in visual order, starting from the 1877 // leading margin. The first int of each pair is the offset from 1878 // the first character of the line to the start of the run. The 1879 // second int represents both the length and level of the run. 1880 // The length is in the lower bits, accessed by masking with 1881 // DIR_LENGTH_MASK. The level is in the higher bits, accessed 1882 // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. 1883 // To simply test for an RTL direction, test the bit using 1884 // DIR_RTL_FLAG, if set then the direction is rtl. 1885 1886 /* package */ int[] mDirections; 1887 /* package */ Directions(int[] dirs) { 1888 mDirections = dirs; 1889 } 1890 } 1891 1892 /** 1893 * Return the offset of the first character to be ellipsized away, 1894 * relative to the start of the line. (So 0 if the beginning of the 1895 * line is ellipsized, not getLineStart().) 1896 */ 1897 public abstract int getEllipsisStart(int line); 1898 1899 /** 1900 * Returns the number of characters to be ellipsized away, or 0 if 1901 * no ellipsis is to take place. 1902 */ 1903 public abstract int getEllipsisCount(int line); 1904 1905 /* package */ static class Ellipsizer implements CharSequence, GetChars { 1906 /* package */ CharSequence mText; 1907 /* package */ Layout mLayout; 1908 /* package */ int mWidth; 1909 /* package */ TextUtils.TruncateAt mMethod; 1910 1911 public Ellipsizer(CharSequence s) { 1912 mText = s; 1913 } 1914 1915 public char charAt(int off) { 1916 char[] buf = TextUtils.obtain(1); 1917 getChars(off, off + 1, buf, 0); 1918 char ret = buf[0]; 1919 1920 TextUtils.recycle(buf); 1921 return ret; 1922 } 1923 1924 public void getChars(int start, int end, char[] dest, int destoff) { 1925 int line1 = mLayout.getLineForOffset(start); 1926 int line2 = mLayout.getLineForOffset(end); 1927 1928 TextUtils.getChars(mText, start, end, dest, destoff); 1929 1930 for (int i = line1; i <= line2; i++) { 1931 mLayout.ellipsize(start, end, i, dest, destoff, mMethod); 1932 } 1933 } 1934 1935 public int length() { 1936 return mText.length(); 1937 } 1938 1939 public CharSequence subSequence(int start, int end) { 1940 char[] s = new char[end - start]; 1941 getChars(start, end, s, 0); 1942 return new String(s); 1943 } 1944 1945 @Override 1946 public String toString() { 1947 char[] s = new char[length()]; 1948 getChars(0, length(), s, 0); 1949 return new String(s); 1950 } 1951 1952 } 1953 1954 /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned { 1955 private Spanned mSpanned; 1956 1957 public SpannedEllipsizer(CharSequence display) { 1958 super(display); 1959 mSpanned = (Spanned) display; 1960 } 1961 1962 public <T> T[] getSpans(int start, int end, Class<T> type) { 1963 return mSpanned.getSpans(start, end, type); 1964 } 1965 1966 public int getSpanStart(Object tag) { 1967 return mSpanned.getSpanStart(tag); 1968 } 1969 1970 public int getSpanEnd(Object tag) { 1971 return mSpanned.getSpanEnd(tag); 1972 } 1973 1974 public int getSpanFlags(Object tag) { 1975 return mSpanned.getSpanFlags(tag); 1976 } 1977 1978 @SuppressWarnings("rawtypes") 1979 public int nextSpanTransition(int start, int limit, Class type) { 1980 return mSpanned.nextSpanTransition(start, limit, type); 1981 } 1982 1983 @Override 1984 public CharSequence subSequence(int start, int end) { 1985 char[] s = new char[end - start]; 1986 getChars(start, end, s, 0); 1987 1988 SpannableString ss = new SpannableString(new String(s)); 1989 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); 1990 return ss; 1991 } 1992 } 1993 1994 private CharSequence mText; 1995 private TextPaint mPaint; 1996 private int mWidth; 1997 private Alignment mAlignment = Alignment.ALIGN_NORMAL; 1998 private float mSpacingMult; 1999 private float mSpacingAdd; 2000 private static final Rect sTempRect = new Rect(); 2001 private boolean mSpannedText; 2002 private TextDirectionHeuristic mTextDir; 2003 private SpanSet<LineBackgroundSpan> mLineBackgroundSpans; 2004 2005 public static final int DIR_LEFT_TO_RIGHT = 1; 2006 public static final int DIR_RIGHT_TO_LEFT = -1; 2007 2008 /* package */ static final int DIR_REQUEST_LTR = 1; 2009 /* package */ static final int DIR_REQUEST_RTL = -1; 2010 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; 2011 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; 2012 2013 /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; 2014 /* package */ static final int RUN_LEVEL_SHIFT = 26; 2015 /* package */ static final int RUN_LEVEL_MASK = 0x3f; 2016 /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; 2017 2018 public enum Alignment { 2019 ALIGN_NORMAL, 2020 ALIGN_OPPOSITE, 2021 ALIGN_CENTER, 2022 /** @hide */ 2023 ALIGN_LEFT, 2024 /** @hide */ 2025 ALIGN_RIGHT, 2026 } 2027 2028 private static final int TAB_INCREMENT = 20; 2029 2030 /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = 2031 new Directions(new int[] { 0, RUN_LENGTH_MASK }); 2032 /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = 2033 new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); 2034 2035} 2036