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