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