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