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 */ getDesiredWidth(CharSequence source, TextPaint paint)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 */ getDesiredWidth(CharSequence source, int start, int end, TextPaint paint)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 */ getDesiredWidth(CharSequence source, int start, int end, TextPaint paint, TextDirectionHeuristic textDir)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 */ Layout(CharSequence text, TextPaint paint, int width, Alignment align, float spacingMult, float spacingAdd)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 */ Layout(CharSequence text, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingMult, float spacingAdd)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 */ setJustificationMode(@ustificationMode int justificationMode)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 */ replaceWith(CharSequence text, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd)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 */ draw(Canvas c)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 */ draw(Canvas canvas, Path highlight, Paint highlightPaint, int cursorOffsetVertical)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 isJustificationRequired(int lineNum)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 getJustifyWidth(int lineNum)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 */ drawText(Canvas canvas, int firstLine, int lastLine)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 */ drawBackground(Canvas canvas, Path highlight, Paint highlightPaint, int cursorOffsetVertical, int firstLine, int lastLine)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 */ getLineRangeForDraw(Canvas canvas)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 */ getLineStartPos(int line, int left, int right)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 */ getText()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 */ getPaint()719 public final TextPaint getPaint() { 720 return mPaint; 721 } 722 723 /** 724 * Return the width of this layout. 725 */ getWidth()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 */ getEllipsizedWidth()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 */ increaseWidthTo(int wid)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 */ getHeight()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 */ getHeight(boolean cap)765 public int getHeight(boolean cap) { 766 return getHeight(); 767 } 768 769 /** 770 * Return the base alignment of this layout. 771 */ getAlignment()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 */ getSpacingMultiplier()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 */ getSpacingAdd()786 public final float getSpacingAdd() { 787 return mSpacingAdd; 788 } 789 790 /** 791 * Return the heuristic used to determine paragraph text direction. 792 * @hide 793 */ getTextDirectionHeuristic()794 public final TextDirectionHeuristic getTextDirectionHeuristic() { 795 return mTextDir; 796 } 797 798 /** 799 * Return the number of lines of text in this layout. 800 */ getLineCount()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 */ getLineBounds(int line, Rect bounds)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 */ getLineTop(int line)827 public abstract int getLineTop(int line); 828 829 /** 830 * Return the descent of the specified line(0…getLineCount() - 1). 831 */ getLineDescent(int line)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 */ getLineStart(int line)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 */ getParagraphDirection(int line)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 */ getLineContainsTab(int line)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 */ getLineDirections(int line)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 */ getTopPadding()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 */ getBottomPadding()873 public abstract int getBottomPadding(); 874 875 /** 876 * Returns the hyphen edit for a line. 877 * 878 * @hide 879 */ getHyphen(int line)880 public int getHyphen(int line) { 881 return 0; 882 } 883 884 /** 885 * Returns the left indent for a line. 886 * 887 * @hide 888 */ getIndentAdjust(int line, Alignment alignment)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 */ isLevelBoundary(int offset)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 */ isRtlCharAt(int offset)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 */ getRunRange(int offset)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 /** 979 * Checks if the trailing BiDi level should be used for an offset 980 * 981 * This method is useful when the offset is at the BiDi level transition point and determine 982 * which run need to be used. For example, let's think about following input: (L* denotes 983 * Left-to-Right characters, R* denotes Right-to-Left characters.) 984 * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6 985 * Input (Display Order): L1 L2 L3 R3 R2 R1 L4 L5 L6 986 * 987 * Then, think about selecting the range (3, 6). The offset=3 and offset=6 are ambiguous here 988 * since they are at the BiDi transition point. In Android, the offset is considered to be 989 * associated with the trailing run if the BiDi level of the trailing run is higher than of the 990 * previous run. In this case, the BiDi level of the input text is as follows: 991 * 992 * Input (Logical Order): L1 L2 L3 R1 R2 R3 L4 L5 L6 993 * BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ] 994 * BiDi Level: 0 0 0 1 1 1 0 0 0 995 * 996 * Thus, offset = 3 is part of Run 1 and this method returns true for offset = 3, since the BiDi 997 * level of Run 1 is higher than the level of Run 0. Similarly, the offset = 6 is a part of Run 998 * 1 and this method returns false for the offset = 6 since the BiDi level of Run 1 is higher 999 * than the level of Run 2. 1000 * 1001 * @returns true if offset is at the BiDi level transition point and trailing BiDi level is 1002 * higher than previous BiDi level. See above for the detail. 1003 */ primaryIsTrailingPrevious(int offset)1004 private boolean primaryIsTrailingPrevious(int offset) { 1005 int line = getLineForOffset(offset); 1006 int lineStart = getLineStart(line); 1007 int lineEnd = getLineEnd(line); 1008 int[] runs = getLineDirections(line).mDirections; 1009 1010 int levelAt = -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 if (offset > start) { 1019 // Previous character is at same level, so don't use trailing. 1020 return false; 1021 } 1022 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 1023 break; 1024 } 1025 } 1026 if (levelAt == -1) { 1027 // Offset was limit of line. 1028 levelAt = getParagraphDirection(line) == 1 ? 0 : 1; 1029 } 1030 1031 // At level boundary, check previous level. 1032 int levelBefore = -1; 1033 if (offset == lineStart) { 1034 levelBefore = getParagraphDirection(line) == 1 ? 0 : 1; 1035 } else { 1036 offset -= 1; 1037 for (int i = 0; i < runs.length; i += 2) { 1038 int start = lineStart + runs[i]; 1039 int limit = start + (runs[i+1] & RUN_LENGTH_MASK); 1040 if (limit > lineEnd) { 1041 limit = lineEnd; 1042 } 1043 if (offset >= start && offset < limit) { 1044 levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK; 1045 break; 1046 } 1047 } 1048 } 1049 1050 return levelBefore < levelAt; 1051 } 1052 1053 /** 1054 * Computes in linear time the results of calling 1055 * #primaryIsTrailingPrevious for all offsets on a line. 1056 * @param line The line giving the offsets we compute the information for 1057 * @return The array of results, indexed from 0, where 0 corresponds to the line start offset 1058 */ primaryIsTrailingPreviousAllLineOffsets(int line)1059 private boolean[] primaryIsTrailingPreviousAllLineOffsets(int line) { 1060 int lineStart = getLineStart(line); 1061 int lineEnd = getLineEnd(line); 1062 int[] runs = getLineDirections(line).mDirections; 1063 1064 boolean[] trailing = new boolean[lineEnd - lineStart + 1]; 1065 1066 byte[] level = new byte[lineEnd - lineStart + 1]; 1067 for (int i = 0; i < runs.length; i += 2) { 1068 int start = lineStart + runs[i]; 1069 int limit = start + (runs[i + 1] & RUN_LENGTH_MASK); 1070 if (limit > lineEnd) { 1071 limit = lineEnd; 1072 } 1073 if (limit == start) { 1074 continue; 1075 } 1076 level[limit - lineStart - 1] = 1077 (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK); 1078 } 1079 1080 for (int i = 0; i < runs.length; i += 2) { 1081 int start = lineStart + runs[i]; 1082 byte currentLevel = (byte) ((runs[i + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK); 1083 trailing[start - lineStart] = currentLevel > (start == lineStart 1084 ? (getParagraphDirection(line) == 1 ? 0 : 1) 1085 : level[start - lineStart - 1]); 1086 } 1087 1088 return trailing; 1089 } 1090 1091 /** 1092 * Get the primary horizontal position for the specified text offset. 1093 * This is the location where a new character would be inserted in 1094 * the paragraph's primary direction. 1095 */ getPrimaryHorizontal(int offset)1096 public float getPrimaryHorizontal(int offset) { 1097 return getPrimaryHorizontal(offset, false /* not clamped */); 1098 } 1099 1100 /** 1101 * Get the primary horizontal position for the specified text offset, but 1102 * optionally clamp it so that it doesn't exceed the width of the layout. 1103 * @hide 1104 */ getPrimaryHorizontal(int offset, boolean clamped)1105 public float getPrimaryHorizontal(int offset, boolean clamped) { 1106 boolean trailing = primaryIsTrailingPrevious(offset); 1107 return getHorizontal(offset, trailing, clamped); 1108 } 1109 1110 /** 1111 * Get the secondary horizontal position for the specified text offset. 1112 * This is the location where a new character would be inserted in 1113 * the direction other than the paragraph's primary direction. 1114 */ getSecondaryHorizontal(int offset)1115 public float getSecondaryHorizontal(int offset) { 1116 return getSecondaryHorizontal(offset, false /* not clamped */); 1117 } 1118 1119 /** 1120 * Get the secondary horizontal position for the specified text offset, but 1121 * optionally clamp it so that it doesn't exceed the width of the layout. 1122 * @hide 1123 */ getSecondaryHorizontal(int offset, boolean clamped)1124 public float getSecondaryHorizontal(int offset, boolean clamped) { 1125 boolean trailing = primaryIsTrailingPrevious(offset); 1126 return getHorizontal(offset, !trailing, clamped); 1127 } 1128 getHorizontal(int offset, boolean primary)1129 private float getHorizontal(int offset, boolean primary) { 1130 return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset); 1131 } 1132 getHorizontal(int offset, boolean trailing, boolean clamped)1133 private float getHorizontal(int offset, boolean trailing, boolean clamped) { 1134 int line = getLineForOffset(offset); 1135 1136 return getHorizontal(offset, trailing, line, clamped); 1137 } 1138 getHorizontal(int offset, boolean trailing, int line, boolean clamped)1139 private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) { 1140 int start = getLineStart(line); 1141 int end = getLineEnd(line); 1142 int dir = getParagraphDirection(line); 1143 boolean hasTab = getLineContainsTab(line); 1144 Directions directions = getLineDirections(line); 1145 1146 TabStops tabStops = null; 1147 if (hasTab && mText instanceof Spanned) { 1148 // Just checking this line should be good enough, tabs should be 1149 // consistent across all lines in a paragraph. 1150 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 1151 if (tabs.length > 0) { 1152 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1153 } 1154 } 1155 1156 TextLine tl = TextLine.obtain(); 1157 tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops); 1158 float wid = tl.measure(offset - start, trailing, null); 1159 TextLine.recycle(tl); 1160 1161 if (clamped && wid > mWidth) { 1162 wid = mWidth; 1163 } 1164 int left = getParagraphLeft(line); 1165 int right = getParagraphRight(line); 1166 1167 return getLineStartPos(line, left, right) + wid; 1168 } 1169 1170 /** 1171 * Computes in linear time the results of calling #getHorizontal for all offsets on a line. 1172 * 1173 * @param line The line giving the offsets we compute information for 1174 * @param clamped Whether to clamp the results to the width of the layout 1175 * @param primary Whether the results should be the primary or the secondary horizontal 1176 * @return The array of results, indexed from 0, where 0 corresponds to the line start offset 1177 */ getLineHorizontals(int line, boolean clamped, boolean primary)1178 private float[] getLineHorizontals(int line, boolean clamped, boolean primary) { 1179 int start = getLineStart(line); 1180 int end = getLineEnd(line); 1181 int dir = getParagraphDirection(line); 1182 boolean hasTab = getLineContainsTab(line); 1183 Directions directions = getLineDirections(line); 1184 1185 TabStops tabStops = null; 1186 if (hasTab && mText instanceof Spanned) { 1187 // Just checking this line should be good enough, tabs should be 1188 // consistent across all lines in a paragraph. 1189 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 1190 if (tabs.length > 0) { 1191 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1192 } 1193 } 1194 1195 TextLine tl = TextLine.obtain(); 1196 tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops); 1197 boolean[] trailings = primaryIsTrailingPreviousAllLineOffsets(line); 1198 if (!primary) { 1199 for (int offset = 0; offset < trailings.length; ++offset) { 1200 trailings[offset] = !trailings[offset]; 1201 } 1202 } 1203 float[] wid = tl.measureAllOffsets(trailings, null); 1204 TextLine.recycle(tl); 1205 1206 if (clamped) { 1207 for (int offset = 0; offset < wid.length; ++offset) { 1208 if (wid[offset] > mWidth) { 1209 wid[offset] = mWidth; 1210 } 1211 } 1212 } 1213 int left = getParagraphLeft(line); 1214 int right = getParagraphRight(line); 1215 1216 int lineStartPos = getLineStartPos(line, left, right); 1217 float[] horizontal = new float[end - start + 1]; 1218 for (int offset = 0; offset < horizontal.length; ++offset) { 1219 horizontal[offset] = lineStartPos + wid[offset]; 1220 } 1221 return horizontal; 1222 } 1223 1224 /** 1225 * Get the leftmost position that should be exposed for horizontal 1226 * scrolling on the specified line. 1227 */ getLineLeft(int line)1228 public float getLineLeft(int line) { 1229 int dir = getParagraphDirection(line); 1230 Alignment align = getParagraphAlignment(line); 1231 1232 if (align == Alignment.ALIGN_LEFT) { 1233 return 0; 1234 } else if (align == Alignment.ALIGN_NORMAL) { 1235 if (dir == DIR_RIGHT_TO_LEFT) 1236 return getParagraphRight(line) - getLineMax(line); 1237 else 1238 return 0; 1239 } else if (align == Alignment.ALIGN_RIGHT) { 1240 return mWidth - getLineMax(line); 1241 } else if (align == Alignment.ALIGN_OPPOSITE) { 1242 if (dir == DIR_RIGHT_TO_LEFT) 1243 return 0; 1244 else 1245 return mWidth - getLineMax(line); 1246 } else { /* align == Alignment.ALIGN_CENTER */ 1247 int left = getParagraphLeft(line); 1248 int right = getParagraphRight(line); 1249 int max = ((int) getLineMax(line)) & ~1; 1250 1251 return left + ((right - left) - max) / 2; 1252 } 1253 } 1254 1255 /** 1256 * Get the rightmost position that should be exposed for horizontal 1257 * scrolling on the specified line. 1258 */ getLineRight(int line)1259 public float getLineRight(int line) { 1260 int dir = getParagraphDirection(line); 1261 Alignment align = getParagraphAlignment(line); 1262 1263 if (align == Alignment.ALIGN_LEFT) { 1264 return getParagraphLeft(line) + getLineMax(line); 1265 } else if (align == Alignment.ALIGN_NORMAL) { 1266 if (dir == DIR_RIGHT_TO_LEFT) 1267 return mWidth; 1268 else 1269 return getParagraphLeft(line) + getLineMax(line); 1270 } else if (align == Alignment.ALIGN_RIGHT) { 1271 return mWidth; 1272 } else if (align == Alignment.ALIGN_OPPOSITE) { 1273 if (dir == DIR_RIGHT_TO_LEFT) 1274 return getLineMax(line); 1275 else 1276 return mWidth; 1277 } else { /* align == Alignment.ALIGN_CENTER */ 1278 int left = getParagraphLeft(line); 1279 int right = getParagraphRight(line); 1280 int max = ((int) getLineMax(line)) & ~1; 1281 1282 return right - ((right - left) - max) / 2; 1283 } 1284 } 1285 1286 /** 1287 * Gets the unsigned horizontal extent of the specified line, including 1288 * leading margin indent, but excluding trailing whitespace. 1289 */ getLineMax(int line)1290 public float getLineMax(int line) { 1291 float margin = getParagraphLeadingMargin(line); 1292 float signedExtent = getLineExtent(line, false); 1293 return margin + (signedExtent >= 0 ? signedExtent : -signedExtent); 1294 } 1295 1296 /** 1297 * Gets the unsigned horizontal extent of the specified line, including 1298 * leading margin indent and trailing whitespace. 1299 */ getLineWidth(int line)1300 public float getLineWidth(int line) { 1301 float margin = getParagraphLeadingMargin(line); 1302 float signedExtent = getLineExtent(line, true); 1303 return margin + (signedExtent >= 0 ? signedExtent : -signedExtent); 1304 } 1305 1306 /** 1307 * Like {@link #getLineExtent(int,TabStops,boolean)} but determines the 1308 * tab stops instead of using the ones passed in. 1309 * @param line the index of the line 1310 * @param full whether to include trailing whitespace 1311 * @return the extent of the line 1312 */ getLineExtent(int line, boolean full)1313 private float getLineExtent(int line, boolean full) { 1314 int start = getLineStart(line); 1315 int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 1316 1317 boolean hasTabs = getLineContainsTab(line); 1318 TabStops tabStops = null; 1319 if (hasTabs && mText instanceof Spanned) { 1320 // Just checking this line should be good enough, tabs should be 1321 // consistent across all lines in a paragraph. 1322 TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end, TabStopSpan.class); 1323 if (tabs.length > 0) { 1324 tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse 1325 } 1326 } 1327 Directions directions = getLineDirections(line); 1328 // Returned directions can actually be null 1329 if (directions == null) { 1330 return 0f; 1331 } 1332 int dir = getParagraphDirection(line); 1333 1334 TextLine tl = TextLine.obtain(); 1335 mPaint.setHyphenEdit(getHyphen(line)); 1336 tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops); 1337 if (isJustificationRequired(line)) { 1338 tl.justify(getJustifyWidth(line)); 1339 } 1340 float width = tl.metrics(null); 1341 mPaint.setHyphenEdit(0); 1342 TextLine.recycle(tl); 1343 return width; 1344 } 1345 1346 /** 1347 * Returns the signed horizontal extent of the specified line, excluding 1348 * leading margin. If full is false, excludes trailing whitespace. 1349 * @param line the index of the line 1350 * @param tabStops the tab stops, can be null if we know they're not used. 1351 * @param full whether to include trailing whitespace 1352 * @return the extent of the text on this line 1353 */ getLineExtent(int line, TabStops tabStops, boolean full)1354 private float getLineExtent(int line, TabStops tabStops, boolean full) { 1355 int start = getLineStart(line); 1356 int end = full ? getLineEnd(line) : getLineVisibleEnd(line); 1357 boolean hasTabs = getLineContainsTab(line); 1358 Directions directions = getLineDirections(line); 1359 int dir = getParagraphDirection(line); 1360 1361 TextLine tl = TextLine.obtain(); 1362 mPaint.setHyphenEdit(getHyphen(line)); 1363 tl.set(mPaint, mText, start, end, dir, directions, hasTabs, tabStops); 1364 if (isJustificationRequired(line)) { 1365 tl.justify(getJustifyWidth(line)); 1366 } 1367 float width = tl.metrics(null); 1368 mPaint.setHyphenEdit(0); 1369 TextLine.recycle(tl); 1370 return width; 1371 } 1372 1373 /** 1374 * Get the line number corresponding to the specified vertical position. 1375 * If you ask for a position above 0, you get 0; if you ask for a position 1376 * below the bottom of the text, you get the last line. 1377 */ 1378 // FIXME: It may be faster to do a linear search for layouts without many lines. getLineForVertical(int vertical)1379 public int getLineForVertical(int vertical) { 1380 int high = getLineCount(), low = -1, guess; 1381 1382 while (high - low > 1) { 1383 guess = (high + low) / 2; 1384 1385 if (getLineTop(guess) > vertical) 1386 high = guess; 1387 else 1388 low = guess; 1389 } 1390 1391 if (low < 0) 1392 return 0; 1393 else 1394 return low; 1395 } 1396 1397 /** 1398 * Get the line number on which the specified text offset appears. 1399 * If you ask for a position before 0, you get 0; if you ask for a position 1400 * beyond the end of the text, you get the last line. 1401 */ getLineForOffset(int offset)1402 public int getLineForOffset(int offset) { 1403 int high = getLineCount(), low = -1, guess; 1404 1405 while (high - low > 1) { 1406 guess = (high + low) / 2; 1407 1408 if (getLineStart(guess) > offset) 1409 high = guess; 1410 else 1411 low = guess; 1412 } 1413 1414 if (low < 0) { 1415 return 0; 1416 } else { 1417 return low; 1418 } 1419 } 1420 1421 /** 1422 * Get the character offset on the specified line whose position is 1423 * closest to the specified horizontal position. 1424 */ getOffsetForHorizontal(int line, float horiz)1425 public int getOffsetForHorizontal(int line, float horiz) { 1426 return getOffsetForHorizontal(line, horiz, true); 1427 } 1428 1429 /** 1430 * Get the character offset on the specified line whose position is 1431 * closest to the specified horizontal position. 1432 * 1433 * @param line the line used to find the closest offset 1434 * @param horiz the horizontal position used to find the closest offset 1435 * @param primary whether to use the primary position or secondary position to find the offset 1436 * 1437 * @hide 1438 */ getOffsetForHorizontal(int line, float horiz, boolean primary)1439 public int getOffsetForHorizontal(int line, float horiz, boolean primary) { 1440 // TODO: use Paint.getOffsetForAdvance to avoid binary search 1441 final int lineEndOffset = getLineEnd(line); 1442 final int lineStartOffset = getLineStart(line); 1443 1444 Directions dirs = getLineDirections(line); 1445 1446 TextLine tl = TextLine.obtain(); 1447 // XXX: we don't care about tabs as we just use TextLine#getOffsetToLeftRightOf here. 1448 tl.set(mPaint, mText, lineStartOffset, lineEndOffset, getParagraphDirection(line), dirs, 1449 false, null); 1450 final HorizontalMeasurementProvider horizontal = 1451 new HorizontalMeasurementProvider(line, primary); 1452 1453 final int max; 1454 if (line == getLineCount() - 1) { 1455 max = lineEndOffset; 1456 } else { 1457 max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset, 1458 !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset; 1459 } 1460 int best = lineStartOffset; 1461 float bestdist = Math.abs(horizontal.get(lineStartOffset) - horiz); 1462 1463 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1464 int here = lineStartOffset + dirs.mDirections[i]; 1465 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1466 boolean isRtl = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0; 1467 int swap = isRtl ? -1 : 1; 1468 1469 if (there > max) 1470 there = max; 1471 int high = there - 1 + 1, low = here + 1 - 1, guess; 1472 1473 while (high - low > 1) { 1474 guess = (high + low) / 2; 1475 int adguess = getOffsetAtStartOf(guess); 1476 1477 if (horizontal.get(adguess) * swap >= horiz * swap) { 1478 high = guess; 1479 } else { 1480 low = guess; 1481 } 1482 } 1483 1484 if (low < here + 1) 1485 low = here + 1; 1486 1487 if (low < there) { 1488 int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset; 1489 low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset; 1490 if (low >= here && low < there) { 1491 float dist = Math.abs(horizontal.get(low) - horiz); 1492 if (aft < there) { 1493 float other = Math.abs(horizontal.get(aft) - horiz); 1494 1495 if (other < dist) { 1496 dist = other; 1497 low = aft; 1498 } 1499 } 1500 1501 if (dist < bestdist) { 1502 bestdist = dist; 1503 best = low; 1504 } 1505 } 1506 } 1507 1508 float dist = Math.abs(horizontal.get(here) - horiz); 1509 1510 if (dist < bestdist) { 1511 bestdist = dist; 1512 best = here; 1513 } 1514 } 1515 1516 float dist = Math.abs(horizontal.get(max) - horiz); 1517 1518 if (dist <= bestdist) { 1519 bestdist = dist; 1520 best = max; 1521 } 1522 1523 TextLine.recycle(tl); 1524 return best; 1525 } 1526 1527 /** 1528 * Responds to #getHorizontal queries, by selecting the better strategy between: 1529 * - calling #getHorizontal explicitly for each query 1530 * - precomputing all #getHorizontal measurements, and responding to any query in constant time 1531 * The first strategy is used for LTR-only text, while the second is used for all other cases. 1532 * The class is currently only used in #getOffsetForHorizontal, so reuse with care in other 1533 * contexts. 1534 */ 1535 private class HorizontalMeasurementProvider { 1536 private final int mLine; 1537 private final boolean mPrimary; 1538 1539 private float[] mHorizontals; 1540 private int mLineStartOffset; 1541 HorizontalMeasurementProvider(final int line, final boolean primary)1542 HorizontalMeasurementProvider(final int line, final boolean primary) { 1543 mLine = line; 1544 mPrimary = primary; 1545 init(); 1546 } 1547 init()1548 private void init() { 1549 final Directions dirs = getLineDirections(mLine); 1550 if (dirs == DIRS_ALL_LEFT_TO_RIGHT) { 1551 return; 1552 } 1553 1554 mHorizontals = getLineHorizontals(mLine, false, mPrimary); 1555 mLineStartOffset = getLineStart(mLine); 1556 } 1557 get(final int offset)1558 float get(final int offset) { 1559 if (mHorizontals == null || offset < mLineStartOffset 1560 || offset >= mLineStartOffset + mHorizontals.length) { 1561 return getHorizontal(offset, mPrimary); 1562 } else { 1563 return mHorizontals[offset - mLineStartOffset]; 1564 } 1565 } 1566 } 1567 1568 /** 1569 * Return the text offset after the last character on the specified line. 1570 */ getLineEnd(int line)1571 public final int getLineEnd(int line) { 1572 return getLineStart(line + 1); 1573 } 1574 1575 /** 1576 * Return the text offset after the last visible character (so whitespace 1577 * is not counted) on the specified line. 1578 */ getLineVisibleEnd(int line)1579 public int getLineVisibleEnd(int line) { 1580 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); 1581 } 1582 getLineVisibleEnd(int line, int start, int end)1583 private int getLineVisibleEnd(int line, int start, int end) { 1584 CharSequence text = mText; 1585 char ch; 1586 if (line == getLineCount() - 1) { 1587 return end; 1588 } 1589 1590 for (; end > start; end--) { 1591 ch = text.charAt(end - 1); 1592 1593 if (ch == '\n') { 1594 return end - 1; 1595 } 1596 1597 if (!TextLine.isLineEndSpace(ch)) { 1598 break; 1599 } 1600 1601 } 1602 1603 return end; 1604 } 1605 1606 /** 1607 * Return the vertical position of the bottom of the specified line. 1608 */ getLineBottom(int line)1609 public final int getLineBottom(int line) { 1610 return getLineTop(line + 1); 1611 } 1612 1613 /** 1614 * Return the vertical position of the baseline of the specified line. 1615 */ getLineBaseline(int line)1616 public final int getLineBaseline(int line) { 1617 // getLineTop(line+1) == getLineTop(line) 1618 return getLineTop(line+1) - getLineDescent(line); 1619 } 1620 1621 /** 1622 * Get the ascent of the text on the specified line. 1623 * The return value is negative to match the Paint.ascent() convention. 1624 */ getLineAscent(int line)1625 public final int getLineAscent(int line) { 1626 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) 1627 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); 1628 } 1629 getOffsetToLeftOf(int offset)1630 public int getOffsetToLeftOf(int offset) { 1631 return getOffsetToLeftRightOf(offset, true); 1632 } 1633 getOffsetToRightOf(int offset)1634 public int getOffsetToRightOf(int offset) { 1635 return getOffsetToLeftRightOf(offset, false); 1636 } 1637 getOffsetToLeftRightOf(int caret, boolean toLeft)1638 private int getOffsetToLeftRightOf(int caret, boolean toLeft) { 1639 int line = getLineForOffset(caret); 1640 int lineStart = getLineStart(line); 1641 int lineEnd = getLineEnd(line); 1642 int lineDir = getParagraphDirection(line); 1643 1644 boolean lineChanged = false; 1645 boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT); 1646 // if walking off line, look at the line we're headed to 1647 if (advance) { 1648 if (caret == lineEnd) { 1649 if (line < getLineCount() - 1) { 1650 lineChanged = true; 1651 ++line; 1652 } else { 1653 return caret; // at very end, don't move 1654 } 1655 } 1656 } else { 1657 if (caret == lineStart) { 1658 if (line > 0) { 1659 lineChanged = true; 1660 --line; 1661 } else { 1662 return caret; // at very start, don't move 1663 } 1664 } 1665 } 1666 1667 if (lineChanged) { 1668 lineStart = getLineStart(line); 1669 lineEnd = getLineEnd(line); 1670 int newDir = getParagraphDirection(line); 1671 if (newDir != lineDir) { 1672 // unusual case. we want to walk onto the line, but it runs 1673 // in a different direction than this one, so we fake movement 1674 // in the opposite direction. 1675 toLeft = !toLeft; 1676 lineDir = newDir; 1677 } 1678 } 1679 1680 Directions directions = getLineDirections(line); 1681 1682 TextLine tl = TextLine.obtain(); 1683 // XXX: we don't care about tabs 1684 tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null); 1685 caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft); 1686 tl = TextLine.recycle(tl); 1687 return caret; 1688 } 1689 getOffsetAtStartOf(int offset)1690 private int getOffsetAtStartOf(int offset) { 1691 // XXX this probably should skip local reorderings and 1692 // zero-width characters, look at callers 1693 if (offset == 0) 1694 return 0; 1695 1696 CharSequence text = mText; 1697 char c = text.charAt(offset); 1698 1699 if (c >= '\uDC00' && c <= '\uDFFF') { 1700 char c1 = text.charAt(offset - 1); 1701 1702 if (c1 >= '\uD800' && c1 <= '\uDBFF') 1703 offset -= 1; 1704 } 1705 1706 if (mSpannedText) { 1707 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, 1708 ReplacementSpan.class); 1709 1710 for (int i = 0; i < spans.length; i++) { 1711 int start = ((Spanned) text).getSpanStart(spans[i]); 1712 int end = ((Spanned) text).getSpanEnd(spans[i]); 1713 1714 if (start < offset && end > offset) 1715 offset = start; 1716 } 1717 } 1718 1719 return offset; 1720 } 1721 1722 /** 1723 * Determine whether we should clamp cursor position. Currently it's 1724 * only robust for left-aligned displays. 1725 * @hide 1726 */ shouldClampCursor(int line)1727 public boolean shouldClampCursor(int line) { 1728 // Only clamp cursor position in left-aligned displays. 1729 switch (getParagraphAlignment(line)) { 1730 case ALIGN_LEFT: 1731 return true; 1732 case ALIGN_NORMAL: 1733 return getParagraphDirection(line) > 0; 1734 default: 1735 return false; 1736 } 1737 1738 } 1739 /** 1740 * Fills in the specified Path with a representation of a cursor 1741 * at the specified offset. This will often be a vertical line 1742 * but can be multiple discontinuous lines in text with multiple 1743 * directionalities. 1744 */ getCursorPath(int point, Path dest, CharSequence editingBuffer)1745 public void getCursorPath(int point, Path dest, 1746 CharSequence editingBuffer) { 1747 dest.reset(); 1748 1749 int line = getLineForOffset(point); 1750 int top = getLineTop(line); 1751 int bottom = getLineTop(line+1); 1752 1753 boolean clamped = shouldClampCursor(line); 1754 float h1 = getPrimaryHorizontal(point, clamped) - 0.5f; 1755 float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1; 1756 1757 int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | 1758 TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); 1759 int fn = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_ALT_ON); 1760 int dist = 0; 1761 1762 if (caps != 0 || fn != 0) { 1763 dist = (bottom - top) >> 2; 1764 1765 if (fn != 0) 1766 top += dist; 1767 if (caps != 0) 1768 bottom -= dist; 1769 } 1770 1771 if (h1 < 0.5f) 1772 h1 = 0.5f; 1773 if (h2 < 0.5f) 1774 h2 = 0.5f; 1775 1776 if (Float.compare(h1, h2) == 0) { 1777 dest.moveTo(h1, top); 1778 dest.lineTo(h1, bottom); 1779 } else { 1780 dest.moveTo(h1, top); 1781 dest.lineTo(h1, (top + bottom) >> 1); 1782 1783 dest.moveTo(h2, (top + bottom) >> 1); 1784 dest.lineTo(h2, bottom); 1785 } 1786 1787 if (caps == 2) { 1788 dest.moveTo(h2, bottom); 1789 dest.lineTo(h2 - dist, bottom + dist); 1790 dest.lineTo(h2, bottom); 1791 dest.lineTo(h2 + dist, bottom + dist); 1792 } else if (caps == 1) { 1793 dest.moveTo(h2, bottom); 1794 dest.lineTo(h2 - dist, bottom + dist); 1795 1796 dest.moveTo(h2 - dist, bottom + dist - 0.5f); 1797 dest.lineTo(h2 + dist, bottom + dist - 0.5f); 1798 1799 dest.moveTo(h2 + dist, bottom + dist); 1800 dest.lineTo(h2, bottom); 1801 } 1802 1803 if (fn == 2) { 1804 dest.moveTo(h1, top); 1805 dest.lineTo(h1 - dist, top - dist); 1806 dest.lineTo(h1, top); 1807 dest.lineTo(h1 + dist, top - dist); 1808 } else if (fn == 1) { 1809 dest.moveTo(h1, top); 1810 dest.lineTo(h1 - dist, top - dist); 1811 1812 dest.moveTo(h1 - dist, top - dist + 0.5f); 1813 dest.lineTo(h1 + dist, top - dist + 0.5f); 1814 1815 dest.moveTo(h1 + dist, top - dist); 1816 dest.lineTo(h1, top); 1817 } 1818 } 1819 addSelection(int line, int start, int end, int top, int bottom, Path dest)1820 private void addSelection(int line, int start, int end, 1821 int top, int bottom, Path dest) { 1822 int linestart = getLineStart(line); 1823 int lineend = getLineEnd(line); 1824 Directions dirs = getLineDirections(line); 1825 1826 if (lineend > linestart && mText.charAt(lineend - 1) == '\n') 1827 lineend--; 1828 1829 for (int i = 0; i < dirs.mDirections.length; i += 2) { 1830 int here = linestart + dirs.mDirections[i]; 1831 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK); 1832 1833 if (there > lineend) 1834 there = lineend; 1835 1836 if (start <= there && end >= here) { 1837 int st = Math.max(start, here); 1838 int en = Math.min(end, there); 1839 1840 if (st != en) { 1841 float h1 = getHorizontal(st, false, line, false /* not clamped */); 1842 float h2 = getHorizontal(en, true, line, false /* not clamped */); 1843 1844 float left = Math.min(h1, h2); 1845 float right = Math.max(h1, h2); 1846 1847 dest.addRect(left, top, right, bottom, Path.Direction.CW); 1848 } 1849 } 1850 } 1851 } 1852 1853 /** 1854 * Fills in the specified Path with a representation of a highlight 1855 * between the specified offsets. This will often be a rectangle 1856 * or a potentially discontinuous set of rectangles. If the start 1857 * and end are the same, the returned path is empty. 1858 */ getSelectionPath(int start, int end, Path dest)1859 public void getSelectionPath(int start, int end, Path dest) { 1860 dest.reset(); 1861 1862 if (start == end) 1863 return; 1864 1865 if (end < start) { 1866 int temp = end; 1867 end = start; 1868 start = temp; 1869 } 1870 1871 int startline = getLineForOffset(start); 1872 int endline = getLineForOffset(end); 1873 1874 int top = getLineTop(startline); 1875 int bottom = getLineBottom(endline); 1876 1877 if (startline == endline) { 1878 addSelection(startline, start, end, top, bottom, dest); 1879 } else { 1880 final float width = mWidth; 1881 1882 addSelection(startline, start, getLineEnd(startline), 1883 top, getLineBottom(startline), dest); 1884 1885 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) 1886 dest.addRect(getLineLeft(startline), top, 1887 0, getLineBottom(startline), Path.Direction.CW); 1888 else 1889 dest.addRect(getLineRight(startline), top, 1890 width, getLineBottom(startline), Path.Direction.CW); 1891 1892 for (int i = startline + 1; i < endline; i++) { 1893 top = getLineTop(i); 1894 bottom = getLineBottom(i); 1895 dest.addRect(0, top, width, bottom, Path.Direction.CW); 1896 } 1897 1898 top = getLineTop(endline); 1899 bottom = getLineBottom(endline); 1900 1901 addSelection(endline, getLineStart(endline), end, 1902 top, bottom, dest); 1903 1904 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) 1905 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW); 1906 else 1907 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW); 1908 } 1909 } 1910 1911 /** 1912 * Get the alignment of the specified paragraph, taking into account 1913 * markup attached to it. 1914 */ getParagraphAlignment(int line)1915 public final Alignment getParagraphAlignment(int line) { 1916 Alignment align = mAlignment; 1917 1918 if (mSpannedText) { 1919 Spanned sp = (Spanned) mText; 1920 AlignmentSpan[] spans = getParagraphSpans(sp, getLineStart(line), 1921 getLineEnd(line), 1922 AlignmentSpan.class); 1923 1924 int spanLength = spans.length; 1925 if (spanLength > 0) { 1926 align = spans[spanLength-1].getAlignment(); 1927 } 1928 } 1929 1930 return align; 1931 } 1932 1933 /** 1934 * Get the left edge of the specified paragraph, inset by left margins. 1935 */ getParagraphLeft(int line)1936 public final int getParagraphLeft(int line) { 1937 int left = 0; 1938 int dir = getParagraphDirection(line); 1939 if (dir == DIR_RIGHT_TO_LEFT || !mSpannedText) { 1940 return left; // leading margin has no impact, or no styles 1941 } 1942 return getParagraphLeadingMargin(line); 1943 } 1944 1945 /** 1946 * Get the right edge of the specified paragraph, inset by right margins. 1947 */ getParagraphRight(int line)1948 public final int getParagraphRight(int line) { 1949 int right = mWidth; 1950 int dir = getParagraphDirection(line); 1951 if (dir == DIR_LEFT_TO_RIGHT || !mSpannedText) { 1952 return right; // leading margin has no impact, or no styles 1953 } 1954 return right - getParagraphLeadingMargin(line); 1955 } 1956 1957 /** 1958 * Returns the effective leading margin (unsigned) for this line, 1959 * taking into account LeadingMarginSpan and LeadingMarginSpan2. 1960 * @param line the line index 1961 * @return the leading margin of this line 1962 */ getParagraphLeadingMargin(int line)1963 private int getParagraphLeadingMargin(int line) { 1964 if (!mSpannedText) { 1965 return 0; 1966 } 1967 Spanned spanned = (Spanned) mText; 1968 1969 int lineStart = getLineStart(line); 1970 int lineEnd = getLineEnd(line); 1971 int spanEnd = spanned.nextSpanTransition(lineStart, lineEnd, 1972 LeadingMarginSpan.class); 1973 LeadingMarginSpan[] spans = getParagraphSpans(spanned, lineStart, spanEnd, 1974 LeadingMarginSpan.class); 1975 if (spans.length == 0) { 1976 return 0; // no leading margin span; 1977 } 1978 1979 int margin = 0; 1980 1981 boolean isFirstParaLine = lineStart == 0 || 1982 spanned.charAt(lineStart - 1) == '\n'; 1983 1984 boolean useFirstLineMargin = isFirstParaLine; 1985 for (int i = 0; i < spans.length; i++) { 1986 if (spans[i] instanceof LeadingMarginSpan2) { 1987 int spStart = spanned.getSpanStart(spans[i]); 1988 int spanLine = getLineForOffset(spStart); 1989 int count = ((LeadingMarginSpan2) spans[i]).getLeadingMarginLineCount(); 1990 // if there is more than one LeadingMarginSpan2, use the count that is greatest 1991 useFirstLineMargin |= line < spanLine + count; 1992 } 1993 } 1994 for (int i = 0; i < spans.length; i++) { 1995 LeadingMarginSpan span = spans[i]; 1996 margin += span.getLeadingMargin(useFirstLineMargin); 1997 } 1998 1999 return margin; 2000 } 2001 2002 /* package */ 2003 static float measurePara(TextPaint paint, CharSequence text, int start, int end, 2004 TextDirectionHeuristic textDir) { 2005 MeasuredText mt = MeasuredText.obtain(); 2006 TextLine tl = TextLine.obtain(); 2007 try { 2008 mt.setPara(text, start, end, textDir, null); 2009 Directions directions; 2010 int dir; 2011 if (mt.mEasy) { 2012 directions = DIRS_ALL_LEFT_TO_RIGHT; 2013 dir = Layout.DIR_LEFT_TO_RIGHT; 2014 } else { 2015 directions = AndroidBidi.directions(mt.mDir, mt.mLevels, 2016 0, mt.mChars, 0, mt.mLen); 2017 dir = mt.mDir; 2018 } 2019 char[] chars = mt.mChars; 2020 int len = mt.mLen; 2021 boolean hasTabs = false; 2022 TabStops tabStops = null; 2023 // leading margins should be taken into account when measuring a paragraph 2024 int margin = 0; 2025 if (text instanceof Spanned) { 2026 Spanned spanned = (Spanned) text; 2027 LeadingMarginSpan[] spans = getParagraphSpans(spanned, start, end, 2028 LeadingMarginSpan.class); 2029 for (LeadingMarginSpan lms : spans) { 2030 margin += lms.getLeadingMargin(true); 2031 } 2032 } 2033 for (int i = 0; i < len; ++i) { 2034 if (chars[i] == '\t') { 2035 hasTabs = true; 2036 if (text instanceof Spanned) { 2037 Spanned spanned = (Spanned) text; 2038 int spanEnd = spanned.nextSpanTransition(start, end, 2039 TabStopSpan.class); 2040 TabStopSpan[] spans = getParagraphSpans(spanned, start, spanEnd, 2041 TabStopSpan.class); 2042 if (spans.length > 0) { 2043 tabStops = new TabStops(TAB_INCREMENT, spans); 2044 } 2045 } 2046 break; 2047 } 2048 } 2049 tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops); 2050 return margin + Math.abs(tl.metrics(null)); 2051 } finally { 2052 TextLine.recycle(tl); 2053 MeasuredText.recycle(mt); 2054 } 2055 } 2056 2057 /** 2058 * @hide 2059 */ 2060 /* package */ static class TabStops { 2061 private int[] mStops; 2062 private int mNumStops; 2063 private int mIncrement; 2064 2065 TabStops(int increment, Object[] spans) { 2066 reset(increment, spans); 2067 } 2068 2069 void reset(int increment, Object[] spans) { 2070 this.mIncrement = increment; 2071 2072 int ns = 0; 2073 if (spans != null) { 2074 int[] stops = this.mStops; 2075 for (Object o : spans) { 2076 if (o instanceof TabStopSpan) { 2077 if (stops == null) { 2078 stops = new int[10]; 2079 } else if (ns == stops.length) { 2080 int[] nstops = new int[ns * 2]; 2081 for (int i = 0; i < ns; ++i) { 2082 nstops[i] = stops[i]; 2083 } 2084 stops = nstops; 2085 } 2086 stops[ns++] = ((TabStopSpan) o).getTabStop(); 2087 } 2088 } 2089 if (ns > 1) { 2090 Arrays.sort(stops, 0, ns); 2091 } 2092 if (stops != this.mStops) { 2093 this.mStops = stops; 2094 } 2095 } 2096 this.mNumStops = ns; 2097 } 2098 2099 float nextTab(float h) { 2100 int ns = this.mNumStops; 2101 if (ns > 0) { 2102 int[] stops = this.mStops; 2103 for (int i = 0; i < ns; ++i) { 2104 int stop = stops[i]; 2105 if (stop > h) { 2106 return stop; 2107 } 2108 } 2109 } 2110 return nextDefaultStop(h, mIncrement); 2111 } 2112 2113 public static float nextDefaultStop(float h, int inc) { 2114 return ((int) ((h + inc) / inc)) * inc; 2115 } 2116 } 2117 2118 /** 2119 * Returns the position of the next tab stop after h on the line. 2120 * 2121 * @param text the text 2122 * @param start start of the line 2123 * @param end limit of the line 2124 * @param h the current horizontal offset 2125 * @param tabs the tabs, can be null. If it is null, any tabs in effect 2126 * on the line will be used. If there are no tabs, a default offset 2127 * will be used to compute the tab stop. 2128 * @return the offset of the next tab stop. 2129 */ 2130 /* package */ static float nextTab(CharSequence text, int start, int end, 2131 float h, Object[] tabs) { 2132 float nh = Float.MAX_VALUE; 2133 boolean alltabs = false; 2134 2135 if (text instanceof Spanned) { 2136 if (tabs == null) { 2137 tabs = getParagraphSpans((Spanned) text, start, end, TabStopSpan.class); 2138 alltabs = true; 2139 } 2140 2141 for (int i = 0; i < tabs.length; i++) { 2142 if (!alltabs) { 2143 if (!(tabs[i] instanceof TabStopSpan)) 2144 continue; 2145 } 2146 2147 int where = ((TabStopSpan) tabs[i]).getTabStop(); 2148 2149 if (where < nh && where > h) 2150 nh = where; 2151 } 2152 2153 if (nh != Float.MAX_VALUE) 2154 return nh; 2155 } 2156 2157 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; 2158 } 2159 2160 protected final boolean isSpanned() { 2161 return mSpannedText; 2162 } 2163 2164 /** 2165 * Returns the same as <code>text.getSpans()</code>, except where 2166 * <code>start</code> and <code>end</code> are the same and are not 2167 * at the very beginning of the text, in which case an empty array 2168 * is returned instead. 2169 * <p> 2170 * This is needed because of the special case that <code>getSpans()</code> 2171 * on an empty range returns the spans adjacent to that range, which is 2172 * primarily for the sake of <code>TextWatchers</code> so they will get 2173 * notifications when text goes from empty to non-empty. But it also 2174 * has the unfortunate side effect that if the text ends with an empty 2175 * paragraph, that paragraph accidentally picks up the styles of the 2176 * preceding paragraph (even though those styles will not be picked up 2177 * by new text that is inserted into the empty paragraph). 2178 * <p> 2179 * The reason it just checks whether <code>start</code> and <code>end</code> 2180 * is the same is that the only time a line can contain 0 characters 2181 * is if it is the final paragraph of the Layout; otherwise any line will 2182 * contain at least one printing or newline character. The reason for the 2183 * additional check if <code>start</code> is greater than 0 is that 2184 * if the empty paragraph is the entire content of the buffer, paragraph 2185 * styles that are already applied to the buffer will apply to text that 2186 * is inserted into it. 2187 */ 2188 /* package */static <T> T[] getParagraphSpans(Spanned text, int start, int end, Class<T> type) { 2189 if (start == end && start > 0) { 2190 return ArrayUtils.emptyArray(type); 2191 } 2192 2193 if(text instanceof SpannableStringBuilder) { 2194 return ((SpannableStringBuilder) text).getSpans(start, end, type, false); 2195 } else { 2196 return text.getSpans(start, end, type); 2197 } 2198 } 2199 2200 private char getEllipsisChar(TextUtils.TruncateAt method) { 2201 return (method == TextUtils.TruncateAt.END_SMALL) ? 2202 TextUtils.ELLIPSIS_TWO_DOTS[0] : 2203 TextUtils.ELLIPSIS_NORMAL[0]; 2204 } 2205 2206 private void ellipsize(int start, int end, int line, 2207 char[] dest, int destoff, TextUtils.TruncateAt method) { 2208 int ellipsisCount = getEllipsisCount(line); 2209 2210 if (ellipsisCount == 0) { 2211 return; 2212 } 2213 2214 int ellipsisStart = getEllipsisStart(line); 2215 int linestart = getLineStart(line); 2216 2217 for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { 2218 char c; 2219 2220 if (i == ellipsisStart) { 2221 c = getEllipsisChar(method); // ellipsis 2222 } else { 2223 c = '\uFEFF'; // 0-width space 2224 } 2225 2226 int a = i + linestart; 2227 2228 if (a >= start && a < end) { 2229 dest[destoff + a - start] = c; 2230 } 2231 } 2232 } 2233 2234 /** 2235 * Stores information about bidirectional (left-to-right or right-to-left) 2236 * text within the layout of a line. 2237 */ 2238 public static class Directions { 2239 // Directions represents directional runs within a line of text. 2240 // Runs are pairs of ints listed in visual order, starting from the 2241 // leading margin. The first int of each pair is the offset from 2242 // the first character of the line to the start of the run. The 2243 // second int represents both the length and level of the run. 2244 // The length is in the lower bits, accessed by masking with 2245 // DIR_LENGTH_MASK. The level is in the higher bits, accessed 2246 // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK. 2247 // To simply test for an RTL direction, test the bit using 2248 // DIR_RTL_FLAG, if set then the direction is rtl. 2249 2250 /** 2251 * @hide 2252 */ 2253 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 2254 public int[] mDirections; 2255 2256 /** 2257 * @hide 2258 */ 2259 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 2260 public Directions(int[] dirs) { 2261 mDirections = dirs; 2262 } 2263 } 2264 2265 /** 2266 * Return the offset of the first character to be ellipsized away, 2267 * relative to the start of the line. (So 0 if the beginning of the 2268 * line is ellipsized, not getLineStart().) 2269 */ 2270 public abstract int getEllipsisStart(int line); 2271 2272 /** 2273 * Returns the number of characters to be ellipsized away, or 0 if 2274 * no ellipsis is to take place. 2275 */ 2276 public abstract int getEllipsisCount(int line); 2277 2278 /* package */ static class Ellipsizer implements CharSequence, GetChars { 2279 /* package */ CharSequence mText; 2280 /* package */ Layout mLayout; 2281 /* package */ int mWidth; 2282 /* package */ TextUtils.TruncateAt mMethod; 2283 2284 public Ellipsizer(CharSequence s) { 2285 mText = s; 2286 } 2287 2288 public char charAt(int off) { 2289 char[] buf = TextUtils.obtain(1); 2290 getChars(off, off + 1, buf, 0); 2291 char ret = buf[0]; 2292 2293 TextUtils.recycle(buf); 2294 return ret; 2295 } 2296 2297 public void getChars(int start, int end, char[] dest, int destoff) { 2298 int line1 = mLayout.getLineForOffset(start); 2299 int line2 = mLayout.getLineForOffset(end); 2300 2301 TextUtils.getChars(mText, start, end, dest, destoff); 2302 2303 for (int i = line1; i <= line2; i++) { 2304 mLayout.ellipsize(start, end, i, dest, destoff, mMethod); 2305 } 2306 } 2307 2308 public int length() { 2309 return mText.length(); 2310 } 2311 2312 public CharSequence subSequence(int start, int end) { 2313 char[] s = new char[end - start]; 2314 getChars(start, end, s, 0); 2315 return new String(s); 2316 } 2317 2318 @Override 2319 public String toString() { 2320 char[] s = new char[length()]; 2321 getChars(0, length(), s, 0); 2322 return new String(s); 2323 } 2324 2325 } 2326 2327 /* package */ static class SpannedEllipsizer extends Ellipsizer implements Spanned { 2328 private Spanned mSpanned; 2329 2330 public SpannedEllipsizer(CharSequence display) { 2331 super(display); 2332 mSpanned = (Spanned) display; 2333 } 2334 2335 public <T> T[] getSpans(int start, int end, Class<T> type) { 2336 return mSpanned.getSpans(start, end, type); 2337 } 2338 2339 public int getSpanStart(Object tag) { 2340 return mSpanned.getSpanStart(tag); 2341 } 2342 2343 public int getSpanEnd(Object tag) { 2344 return mSpanned.getSpanEnd(tag); 2345 } 2346 2347 public int getSpanFlags(Object tag) { 2348 return mSpanned.getSpanFlags(tag); 2349 } 2350 2351 @SuppressWarnings("rawtypes") 2352 public int nextSpanTransition(int start, int limit, Class type) { 2353 return mSpanned.nextSpanTransition(start, limit, type); 2354 } 2355 2356 @Override 2357 public CharSequence subSequence(int start, int end) { 2358 char[] s = new char[end - start]; 2359 getChars(start, end, s, 0); 2360 2361 SpannableString ss = new SpannableString(new String(s)); 2362 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); 2363 return ss; 2364 } 2365 } 2366 2367 private CharSequence mText; 2368 private TextPaint mPaint; 2369 private int mWidth; 2370 private Alignment mAlignment = Alignment.ALIGN_NORMAL; 2371 private float mSpacingMult; 2372 private float mSpacingAdd; 2373 private static final Rect sTempRect = new Rect(); 2374 private boolean mSpannedText; 2375 private TextDirectionHeuristic mTextDir; 2376 private SpanSet<LineBackgroundSpan> mLineBackgroundSpans; 2377 private int mJustificationMode; 2378 2379 public static final int DIR_LEFT_TO_RIGHT = 1; 2380 public static final int DIR_RIGHT_TO_LEFT = -1; 2381 2382 /* package */ static final int DIR_REQUEST_LTR = 1; 2383 /* package */ static final int DIR_REQUEST_RTL = -1; 2384 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2; 2385 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2; 2386 2387 /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff; 2388 /* package */ static final int RUN_LEVEL_SHIFT = 26; 2389 /* package */ static final int RUN_LEVEL_MASK = 0x3f; 2390 /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT; 2391 2392 public enum Alignment { 2393 ALIGN_NORMAL, 2394 ALIGN_OPPOSITE, 2395 ALIGN_CENTER, 2396 /** @hide */ 2397 ALIGN_LEFT, 2398 /** @hide */ 2399 ALIGN_RIGHT, 2400 } 2401 2402 private static final int TAB_INCREMENT = 20; 2403 2404 /** @hide */ 2405 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 2406 public static final Directions DIRS_ALL_LEFT_TO_RIGHT = 2407 new Directions(new int[] { 0, RUN_LENGTH_MASK }); 2408 2409 /** @hide */ 2410 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 2411 public static final Directions DIRS_ALL_RIGHT_TO_LEFT = 2412 new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG }); 2413 2414 } 2415