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