DynamicLayout.java revision 9fc6725428dc85a536b194d04fbb902a0a2b4343
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 17package android.text; 18 19import android.annotation.FloatRange; 20import android.annotation.IntRange; 21import android.annotation.NonNull; 22import android.annotation.Nullable; 23import android.graphics.Paint; 24import android.graphics.Rect; 25import android.text.style.ReplacementSpan; 26import android.text.style.UpdateLayout; 27import android.text.style.WrapTogetherSpan; 28import android.util.ArraySet; 29import android.util.Pools.SynchronizedPool; 30 31import com.android.internal.annotations.VisibleForTesting; 32import com.android.internal.util.ArrayUtils; 33import com.android.internal.util.GrowingArrayUtils; 34 35import java.lang.ref.WeakReference; 36 37/** 38 * DynamicLayout is a text layout that updates itself as the text is edited. 39 * <p>This is used by widgets to control text layout. You should not need 40 * to use this class directly unless you are implementing your own widget 41 * or custom display object, or need to call 42 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) 43 * Canvas.drawText()} directly.</p> 44 */ 45public class DynamicLayout extends Layout 46{ 47 private static final int PRIORITY = 128; 48 private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400; 49 50 /** 51 * Builder for dynamic layouts. The builder is the preferred pattern for constructing 52 * DynamicLayout objects and should be preferred over the constructors, particularly to access 53 * newer features. To build a dynamic layout, first call {@link #obtain} with the required 54 * arguments (base, paint, and width), then call setters for optional parameters, and finally 55 * {@link #build} to build the DynamicLayout object. Parameters not explicitly set will get 56 * default values. 57 */ 58 public static final class Builder { 59 private Builder() { 60 } 61 62 /** 63 * Obtain a builder for constructing DynamicLayout objects. 64 */ 65 @NonNull 66 public static Builder obtain(@NonNull CharSequence base, @NonNull TextPaint paint, 67 @IntRange(from = 0) int width) { 68 Builder b = sPool.acquire(); 69 if (b == null) { 70 b = new Builder(); 71 } 72 73 // set default initial values 74 b.mBase = base; 75 b.mDisplay = base; 76 b.mPaint = paint; 77 b.mWidth = width; 78 b.mAlignment = Alignment.ALIGN_NORMAL; 79 b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; 80 b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER; 81 b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION; 82 b.mIncludePad = true; 83 b.mFallbackLineSpacing = false; 84 b.mEllipsizedWidth = width; 85 b.mEllipsize = null; 86 b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE; 87 b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; 88 b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; 89 return b; 90 } 91 92 /** 93 * This method should be called after the layout is finished getting constructed and the 94 * builder needs to be cleaned up and returned to the pool. 95 */ 96 private static void recycle(@NonNull Builder b) { 97 b.mBase = null; 98 b.mDisplay = null; 99 b.mPaint = null; 100 sPool.release(b); 101 } 102 103 /** 104 * Set the transformed text (password transformation being the primary example of a 105 * transformation) that will be updated as the base text is changed. The default is the 106 * 'base' text passed to the builder's constructor. 107 * 108 * @param display the transformed text 109 * @return this builder, useful for chaining 110 */ 111 @NonNull 112 public Builder setDisplayText(@NonNull CharSequence display) { 113 mDisplay = display; 114 return this; 115 } 116 117 /** 118 * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}. 119 * 120 * @param alignment Alignment for the resulting {@link DynamicLayout} 121 * @return this builder, useful for chaining 122 */ 123 @NonNull 124 public Builder setAlignment(@NonNull Alignment alignment) { 125 mAlignment = alignment; 126 return this; 127 } 128 129 /** 130 * Set the text direction heuristic. The text direction heuristic is used to resolve text 131 * direction per-paragraph based on the input text. The default is 132 * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. 133 * 134 * @param textDir text direction heuristic for resolving bidi behavior. 135 * @return this builder, useful for chaining 136 */ 137 @NonNull 138 public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { 139 mTextDir = textDir; 140 return this; 141 } 142 143 /** 144 * Set line spacing parameters. Each line will have its line spacing multiplied by 145 * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for 146 * {@code spacingAdd} and 1.0 for {@code spacingMult}. 147 * 148 * @param spacingAdd the amount of line spacing addition 149 * @param spacingMult the line spacing multiplier 150 * @return this builder, useful for chaining 151 * @see android.widget.TextView#setLineSpacing 152 */ 153 @NonNull 154 public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) { 155 mSpacingAdd = spacingAdd; 156 mSpacingMult = spacingMult; 157 return this; 158 } 159 160 /** 161 * Set whether to include extra space beyond font ascent and descent (which is needed to 162 * avoid clipping in some languages, such as Arabic and Kannada). The default is 163 * {@code true}. 164 * 165 * @param includePad whether to include padding 166 * @return this builder, useful for chaining 167 * @see android.widget.TextView#setIncludeFontPadding 168 */ 169 @NonNull 170 public Builder setIncludePad(boolean includePad) { 171 mIncludePad = includePad; 172 return this; 173 } 174 175 /** 176 * Set whether to respect the ascent and descent of the fallback fonts that are used in 177 * displaying the text (which is needed to avoid text from consecutive lines running into 178 * each other). If set, fallback fonts that end up getting used can increase the ascent 179 * and descent of the lines that they are used on. 180 * 181 * <p>For backward compatibility reasons, the default is {@code false}, but setting this to 182 * true is strongly recommended. It is required to be true if text could be in languages 183 * like Burmese or Tibetan where text is typically much taller or deeper than Latin text. 184 * 185 * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts 186 * @return this builder, useful for chaining 187 */ 188 @NonNull 189 public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) { 190 mFallbackLineSpacing = useLineSpacingFromFallbacks; 191 return this; 192 } 193 194 /** 195 * Set the width as used for ellipsizing purposes, if it differs from the normal layout 196 * width. The default is the {@code width} passed to {@link #obtain}. 197 * 198 * @param ellipsizedWidth width used for ellipsizing, in pixels 199 * @return this builder, useful for chaining 200 * @see android.widget.TextView#setEllipsize 201 */ 202 @NonNull 203 public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) { 204 mEllipsizedWidth = ellipsizedWidth; 205 return this; 206 } 207 208 /** 209 * Set ellipsizing on the layout. Causes words that are longer than the view is wide, or 210 * exceeding the number of lines (see #setMaxLines) in the case of 211 * {@link android.text.TextUtils.TruncateAt#END} or 212 * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead of broken. 213 * The default is {@code null}, indicating no ellipsis is to be applied. 214 * 215 * @param ellipsize type of ellipsis behavior 216 * @return this builder, useful for chaining 217 * @see android.widget.TextView#setEllipsize 218 */ 219 public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) { 220 mEllipsize = ellipsize; 221 return this; 222 } 223 224 /** 225 * Set break strategy, useful for selecting high quality or balanced paragraph layout 226 * options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}. 227 * 228 * @param breakStrategy break strategy for paragraph layout 229 * @return this builder, useful for chaining 230 * @see android.widget.TextView#setBreakStrategy 231 */ 232 @NonNull 233 public Builder setBreakStrategy(@BreakStrategy int breakStrategy) { 234 mBreakStrategy = breakStrategy; 235 return this; 236 } 237 238 /** 239 * Set hyphenation frequency, to control the amount of automatic hyphenation used. The 240 * possible values are defined in {@link Layout}, by constants named with the pattern 241 * {@code HYPHENATION_FREQUENCY_*}. The default is 242 * {@link Layout#HYPHENATION_FREQUENCY_NONE}. 243 * 244 * @param hyphenationFrequency hyphenation frequency for the paragraph 245 * @return this builder, useful for chaining 246 * @see android.widget.TextView#setHyphenationFrequency 247 */ 248 @NonNull 249 public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) { 250 mHyphenationFrequency = hyphenationFrequency; 251 return this; 252 } 253 254 /** 255 * Set paragraph justification mode. The default value is 256 * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification, 257 * the last line will be displayed with the alignment set by {@link #setAlignment}. 258 * 259 * @param justificationMode justification mode for the paragraph. 260 * @return this builder, useful for chaining. 261 */ 262 @NonNull 263 public Builder setJustificationMode(@JustificationMode int justificationMode) { 264 mJustificationMode = justificationMode; 265 return this; 266 } 267 268 /** 269 * Build the {@link DynamicLayout} after options have been set. 270 * 271 * <p>Note: the builder object must not be reused in any way after calling this method. 272 * Setting parameters after calling this method, or calling it a second time on the same 273 * builder object, will likely lead to unexpected results. 274 * 275 * @return the newly constructed {@link DynamicLayout} object 276 */ 277 @NonNull 278 public DynamicLayout build() { 279 final DynamicLayout result = new DynamicLayout(this); 280 Builder.recycle(this); 281 return result; 282 } 283 284 private CharSequence mBase; 285 private CharSequence mDisplay; 286 private TextPaint mPaint; 287 private int mWidth; 288 private Alignment mAlignment; 289 private TextDirectionHeuristic mTextDir; 290 private float mSpacingMult; 291 private float mSpacingAdd; 292 private boolean mIncludePad; 293 private boolean mFallbackLineSpacing; 294 private int mBreakStrategy; 295 private int mHyphenationFrequency; 296 private int mJustificationMode; 297 private TextUtils.TruncateAt mEllipsize; 298 private int mEllipsizedWidth; 299 300 private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); 301 302 private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3); 303 } 304 305 /** 306 * Make a layout for the specified text that will be updated as the text is changed. 307 */ 308 public DynamicLayout(@NonNull CharSequence base, 309 @NonNull TextPaint paint, 310 @IntRange(from = 0) int width, @NonNull Alignment align, 311 @FloatRange(from = 0.0) float spacingmult, float spacingadd, 312 boolean includepad) { 313 this(base, base, paint, width, align, spacingmult, spacingadd, 314 includepad); 315 } 316 317 /** 318 * Make a layout for the transformed text (password transformation being the primary example of 319 * a transformation) that will be updated as the base text is changed. 320 */ 321 public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, 322 @NonNull TextPaint paint, 323 @IntRange(from = 0) int width, @NonNull Alignment align, 324 @FloatRange(from = 0.0) float spacingmult, float spacingadd, 325 boolean includepad) { 326 this(base, display, paint, width, align, spacingmult, spacingadd, 327 includepad, null, 0); 328 } 329 330 /** 331 * Make a layout for the transformed text (password transformation being the primary example of 332 * a transformation) that will be updated as the base text is changed. If ellipsize is non-null, 333 * the Layout will ellipsize the text down to ellipsizedWidth. 334 */ 335 public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, 336 @NonNull TextPaint paint, 337 @IntRange(from = 0) int width, @NonNull Alignment align, 338 @FloatRange(from = 0.0) float spacingmult, float spacingadd, 339 boolean includepad, 340 @Nullable TextUtils.TruncateAt ellipsize, 341 @IntRange(from = 0) int ellipsizedWidth) { 342 this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, 343 spacingmult, spacingadd, includepad, 344 Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE, 345 Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth); 346 } 347 348 /** 349 * Make a layout for the transformed text (password transformation being the primary example of 350 * a transformation) that will be updated as the base text is changed. If ellipsize is non-null, 351 * the Layout will ellipsize the text down to ellipsizedWidth. 352 * 353 * @hide 354 */ 355 public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, 356 @NonNull TextPaint paint, 357 @IntRange(from = 0) int width, 358 @NonNull Alignment align, @NonNull TextDirectionHeuristic textDir, 359 @FloatRange(from = 0.0) float spacingmult, float spacingadd, 360 boolean includepad, @BreakStrategy int breakStrategy, 361 @HyphenationFrequency int hyphenationFrequency, 362 @JustificationMode int justificationMode, 363 @Nullable TextUtils.TruncateAt ellipsize, 364 @IntRange(from = 0) int ellipsizedWidth) { 365 super(createEllipsizer(ellipsize, display), 366 paint, width, align, textDir, spacingmult, spacingadd); 367 368 final Builder b = Builder.obtain(base, paint, width) 369 .setAlignment(align) 370 .setTextDirection(textDir) 371 .setLineSpacing(spacingadd, spacingmult) 372 .setEllipsizedWidth(ellipsizedWidth) 373 .setEllipsize(ellipsize); 374 mDisplay = display; 375 mIncludePad = includepad; 376 mBreakStrategy = breakStrategy; 377 mJustificationMode = justificationMode; 378 mHyphenationFrequency = hyphenationFrequency; 379 380 generate(b); 381 382 Builder.recycle(b); 383 } 384 385 private DynamicLayout(@NonNull Builder b) { 386 super(createEllipsizer(b.mEllipsize, b.mDisplay), 387 b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd); 388 389 mDisplay = b.mDisplay; 390 mIncludePad = b.mIncludePad; 391 mBreakStrategy = b.mBreakStrategy; 392 mJustificationMode = b.mJustificationMode; 393 mHyphenationFrequency = b.mHyphenationFrequency; 394 395 generate(b); 396 } 397 398 @NonNull 399 private static CharSequence createEllipsizer(@Nullable TextUtils.TruncateAt ellipsize, 400 @NonNull CharSequence display) { 401 if (ellipsize == null) { 402 return display; 403 } else if (display instanceof Spanned) { 404 return new SpannedEllipsizer(display); 405 } else { 406 return new Ellipsizer(display); 407 } 408 } 409 410 private void generate(@NonNull Builder b) { 411 mBase = b.mBase; 412 mFallbackLineSpacing = b.mFallbackLineSpacing; 413 if (b.mEllipsize != null) { 414 mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); 415 mEllipsizedWidth = b.mEllipsizedWidth; 416 mEllipsizeAt = b.mEllipsize; 417 418 /* 419 * This is annoying, but we can't refer to the layout until superclass construction is 420 * finished, and the superclass constructor wants the reference to the display text. 421 * 422 * In other words, the two Ellipsizer classes in Layout.java need a 423 * (Dynamic|Static)Layout as a parameter to do their calculations, but the Ellipsizers 424 * also need to be the input to the superclass's constructor (Layout). In order to go 425 * around the circular dependency, we construct the Ellipsizer with only one of the 426 * parameters, the text (in createEllipsizer). And we fill in the rest of the needed 427 * information (layout, width, and method) later, here. 428 * 429 * This will break if the superclass constructor ever actually cares about the content 430 * instead of just holding the reference. 431 */ 432 final Ellipsizer e = (Ellipsizer) getText(); 433 e.mLayout = this; 434 e.mWidth = b.mEllipsizedWidth; 435 e.mMethod = b.mEllipsize; 436 mEllipsize = true; 437 } else { 438 mInts = new PackedIntVector(COLUMNS_NORMAL); 439 mEllipsizedWidth = b.mWidth; 440 mEllipsizeAt = null; 441 } 442 443 mObjects = new PackedObjectVector<Directions>(1); 444 445 // Initial state is a single line with 0 characters (0 to 0), with top at 0 and bottom at 446 // whatever is natural, and undefined ellipsis. 447 448 int[] start; 449 450 if (b.mEllipsize != null) { 451 start = new int[COLUMNS_ELLIPSIZE]; 452 start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 453 } else { 454 start = new int[COLUMNS_NORMAL]; 455 } 456 457 final Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT }; 458 459 final Paint.FontMetricsInt fm = b.mFontMetricsInt; 460 b.mPaint.getFontMetricsInt(fm); 461 final int asc = fm.ascent; 462 final int desc = fm.descent; 463 464 start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT; 465 start[TOP] = 0; 466 start[DESCENT] = desc; 467 mInts.insertAt(0, start); 468 469 start[TOP] = desc - asc; 470 mInts.insertAt(1, start); 471 472 mObjects.insertAt(0, dirs); 473 474 final int baseLength = mBase.length(); 475 // Update from 0 characters to whatever the real text is 476 reflow(mBase, 0, 0, baseLength); 477 478 if (mBase instanceof Spannable) { 479 if (mWatcher == null) 480 mWatcher = new ChangeWatcher(this); 481 482 // Strip out any watchers for other DynamicLayouts. 483 final Spannable sp = (Spannable) mBase; 484 final ChangeWatcher[] spans = sp.getSpans(0, baseLength, ChangeWatcher.class); 485 for (int i = 0; i < spans.length; i++) { 486 sp.removeSpan(spans[i]); 487 } 488 489 sp.setSpan(mWatcher, 0, baseLength, 490 Spannable.SPAN_INCLUSIVE_INCLUSIVE | 491 (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT)); 492 } 493 } 494 495 private void reflow(CharSequence s, int where, int before, int after) { 496 if (s != mBase) 497 return; 498 499 CharSequence text = mDisplay; 500 int len = text.length(); 501 502 // seek back to the start of the paragraph 503 504 int find = TextUtils.lastIndexOf(text, '\n', where - 1); 505 if (find < 0) 506 find = 0; 507 else 508 find = find + 1; 509 510 { 511 int diff = where - find; 512 before += diff; 513 after += diff; 514 where -= diff; 515 } 516 517 // seek forward to the end of the paragraph 518 519 int look = TextUtils.indexOf(text, '\n', where + after); 520 if (look < 0) 521 look = len; 522 else 523 look++; // we want the index after the \n 524 525 int change = look - (where + after); 526 before += change; 527 after += change; 528 529 // seek further out to cover anything that is forced to wrap together 530 531 if (text instanceof Spanned) { 532 Spanned sp = (Spanned) text; 533 boolean again; 534 535 do { 536 again = false; 537 538 Object[] force = sp.getSpans(where, where + after, 539 WrapTogetherSpan.class); 540 541 for (int i = 0; i < force.length; i++) { 542 int st = sp.getSpanStart(force[i]); 543 int en = sp.getSpanEnd(force[i]); 544 545 if (st < where) { 546 again = true; 547 548 int diff = where - st; 549 before += diff; 550 after += diff; 551 where -= diff; 552 } 553 554 if (en > where + after) { 555 again = true; 556 557 int diff = en - (where + after); 558 before += diff; 559 after += diff; 560 } 561 } 562 } while (again); 563 } 564 565 // find affected region of old layout 566 567 int startline = getLineForOffset(where); 568 int startv = getLineTop(startline); 569 570 int endline = getLineForOffset(where + before); 571 if (where + after == len) 572 endline = getLineCount(); 573 int endv = getLineTop(endline); 574 boolean islast = (endline == getLineCount()); 575 576 // generate new layout for affected text 577 578 StaticLayout reflowed; 579 StaticLayout.Builder b; 580 581 synchronized (sLock) { 582 reflowed = sStaticLayout; 583 b = sBuilder; 584 sStaticLayout = null; 585 sBuilder = null; 586 } 587 588 if (reflowed == null) { 589 reflowed = new StaticLayout(null); 590 b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth()); 591 } 592 593 b.setText(text, where, where + after) 594 .setPaint(getPaint()) 595 .setWidth(getWidth()) 596 .setTextDirection(getTextDirectionHeuristic()) 597 .setLineSpacing(getSpacingAdd(), getSpacingMultiplier()) 598 .setUseLineSpacingFromFallbacks(mFallbackLineSpacing) 599 .setEllipsizedWidth(mEllipsizedWidth) 600 .setEllipsize(mEllipsizeAt) 601 .setBreakStrategy(mBreakStrategy) 602 .setHyphenationFrequency(mHyphenationFrequency) 603 .setJustificationMode(mJustificationMode) 604 .setAddLastLineLineSpacing(!islast); 605 606 reflowed.generate(b, false /*includepad*/, true /*trackpad*/); 607 int n = reflowed.getLineCount(); 608 // If the new layout has a blank line at the end, but it is not 609 // the very end of the buffer, then we already have a line that 610 // starts there, so disregard the blank line. 611 612 if (where + after != len && reflowed.getLineStart(n - 1) == where + after) 613 n--; 614 615 // remove affected lines from old layout 616 mInts.deleteAt(startline, endline - startline); 617 mObjects.deleteAt(startline, endline - startline); 618 619 // adjust offsets in layout for new height and offsets 620 621 int ht = reflowed.getLineTop(n); 622 int toppad = 0, botpad = 0; 623 624 if (mIncludePad && startline == 0) { 625 toppad = reflowed.getTopPadding(); 626 mTopPadding = toppad; 627 ht -= toppad; 628 } 629 if (mIncludePad && islast) { 630 botpad = reflowed.getBottomPadding(); 631 mBottomPadding = botpad; 632 ht += botpad; 633 } 634 635 mInts.adjustValuesBelow(startline, START, after - before); 636 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht); 637 638 // insert new layout 639 640 int[] ints; 641 642 if (mEllipsize) { 643 ints = new int[COLUMNS_ELLIPSIZE]; 644 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 645 } else { 646 ints = new int[COLUMNS_NORMAL]; 647 } 648 649 Directions[] objects = new Directions[1]; 650 651 for (int i = 0; i < n; i++) { 652 final int start = reflowed.getLineStart(i); 653 ints[START] = start; 654 ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT; 655 ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0; 656 657 int top = reflowed.getLineTop(i) + startv; 658 if (i > 0) 659 top -= toppad; 660 ints[TOP] = top; 661 662 int desc = reflowed.getLineDescent(i); 663 if (i == n - 1) 664 desc += botpad; 665 666 ints[DESCENT] = desc; 667 ints[EXTRA] = reflowed.getLineExtra(i); 668 objects[0] = reflowed.getLineDirections(i); 669 670 final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1); 671 ints[HYPHEN] = reflowed.getHyphen(i) & HYPHEN_MASK; 672 ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |= 673 contentMayProtrudeFromLineTopOrBottom(text, start, end) ? 674 MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0; 675 676 if (mEllipsize) { 677 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); 678 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); 679 } 680 681 mInts.insertAt(startline + i, ints); 682 mObjects.insertAt(startline + i, objects); 683 } 684 685 updateBlocks(startline, endline - 1, n); 686 687 b.finish(); 688 synchronized (sLock) { 689 sStaticLayout = reflowed; 690 sBuilder = b; 691 } 692 } 693 694 private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) { 695 if (text instanceof Spanned) { 696 final Spanned spanned = (Spanned) text; 697 if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) { 698 return true; 699 } 700 } 701 // Spans other than ReplacementSpan can be ignored because line top and bottom are 702 // disjunction of all tops and bottoms, although it's not optimal. 703 final Paint paint = getPaint(); 704 paint.getTextBounds(text, start, end, mTempRect); 705 final Paint.FontMetricsInt fm = paint.getFontMetricsInt(); 706 return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom; 707 } 708 709 /** 710 * Create the initial block structure, cutting the text into blocks of at least 711 * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs. 712 */ 713 private void createBlocks() { 714 int offset = BLOCK_MINIMUM_CHARACTER_LENGTH; 715 mNumberOfBlocks = 0; 716 final CharSequence text = mDisplay; 717 718 while (true) { 719 offset = TextUtils.indexOf(text, '\n', offset); 720 if (offset < 0) { 721 addBlockAtOffset(text.length()); 722 break; 723 } else { 724 addBlockAtOffset(offset); 725 offset += BLOCK_MINIMUM_CHARACTER_LENGTH; 726 } 727 } 728 729 // mBlockIndices and mBlockEndLines should have the same length 730 mBlockIndices = new int[mBlockEndLines.length]; 731 for (int i = 0; i < mBlockEndLines.length; i++) { 732 mBlockIndices[i] = INVALID_BLOCK_INDEX; 733 } 734 } 735 736 /** 737 * @hide 738 */ 739 public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() { 740 return mBlocksAlwaysNeedToBeRedrawn; 741 } 742 743 private void updateAlwaysNeedsToBeRedrawn(int blockIndex) { 744 int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1); 745 int endLine = mBlockEndLines[blockIndex]; 746 for (int i = startLine; i <= endLine; i++) { 747 if (getContentMayProtrudeFromTopOrBottom(i)) { 748 if (mBlocksAlwaysNeedToBeRedrawn == null) { 749 mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>(); 750 } 751 mBlocksAlwaysNeedToBeRedrawn.add(blockIndex); 752 return; 753 } 754 } 755 if (mBlocksAlwaysNeedToBeRedrawn != null) { 756 mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex); 757 } 758 } 759 760 /** 761 * Create a new block, ending at the specified character offset. 762 * A block will actually be created only if has at least one line, i.e. this offset is 763 * not on the end line of the previous block. 764 */ 765 private void addBlockAtOffset(int offset) { 766 final int line = getLineForOffset(offset); 767 if (mBlockEndLines == null) { 768 // Initial creation of the array, no test on previous block ending line 769 mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1); 770 mBlockEndLines[mNumberOfBlocks] = line; 771 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks); 772 mNumberOfBlocks++; 773 return; 774 } 775 776 final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1]; 777 if (line > previousBlockEndLine) { 778 mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line); 779 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks); 780 mNumberOfBlocks++; 781 } 782 } 783 784 /** 785 * This method is called every time the layout is reflowed after an edition. 786 * It updates the internal block data structure. The text is split in blocks 787 * of contiguous lines, with at least one block for the entire text. 788 * When a range of lines is edited, new blocks (from 0 to 3 depending on the 789 * overlap structure) will replace the set of overlapping blocks. 790 * Blocks are listed in order and are represented by their ending line number. 791 * An index is associated to each block (which will be used by display lists), 792 * this class simply invalidates the index of blocks overlapping a modification. 793 * 794 * @param startLine the first line of the range of modified lines 795 * @param endLine the last line of the range, possibly equal to startLine, lower 796 * than getLineCount() 797 * @param newLineCount the number of lines that will replace the range, possibly 0 798 * 799 * @hide 800 */ 801 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 802 public void updateBlocks(int startLine, int endLine, int newLineCount) { 803 if (mBlockEndLines == null) { 804 createBlocks(); 805 return; 806 } 807 808 int firstBlock = -1; 809 int lastBlock = -1; 810 for (int i = 0; i < mNumberOfBlocks; i++) { 811 if (mBlockEndLines[i] >= startLine) { 812 firstBlock = i; 813 break; 814 } 815 } 816 for (int i = firstBlock; i < mNumberOfBlocks; i++) { 817 if (mBlockEndLines[i] >= endLine) { 818 lastBlock = i; 819 break; 820 } 821 } 822 final int lastBlockEndLine = mBlockEndLines[lastBlock]; 823 824 boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 : 825 mBlockEndLines[firstBlock - 1] + 1); 826 boolean createBlock = newLineCount > 0; 827 boolean createBlockAfter = endLine < mBlockEndLines[lastBlock]; 828 829 int numAddedBlocks = 0; 830 if (createBlockBefore) numAddedBlocks++; 831 if (createBlock) numAddedBlocks++; 832 if (createBlockAfter) numAddedBlocks++; 833 834 final int numRemovedBlocks = lastBlock - firstBlock + 1; 835 final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks; 836 837 if (newNumberOfBlocks == 0) { 838 // Even when text is empty, there is actually one line and hence one block 839 mBlockEndLines[0] = 0; 840 mBlockIndices[0] = INVALID_BLOCK_INDEX; 841 mNumberOfBlocks = 1; 842 return; 843 } 844 845 if (newNumberOfBlocks > mBlockEndLines.length) { 846 int[] blockEndLines = ArrayUtils.newUnpaddedIntArray( 847 Math.max(mBlockEndLines.length * 2, newNumberOfBlocks)); 848 int[] blockIndices = new int[blockEndLines.length]; 849 System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock); 850 System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock); 851 System.arraycopy(mBlockEndLines, lastBlock + 1, 852 blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 853 System.arraycopy(mBlockIndices, lastBlock + 1, 854 blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 855 mBlockEndLines = blockEndLines; 856 mBlockIndices = blockIndices; 857 } else if (numAddedBlocks + numRemovedBlocks != 0) { 858 System.arraycopy(mBlockEndLines, lastBlock + 1, 859 mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 860 System.arraycopy(mBlockIndices, lastBlock + 1, 861 mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 862 } 863 864 if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) { 865 final ArraySet<Integer> set = new ArraySet<>(); 866 for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) { 867 Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i); 868 if (block > firstBlock) { 869 block += numAddedBlocks - numRemovedBlocks; 870 } 871 set.add(block); 872 } 873 mBlocksAlwaysNeedToBeRedrawn = set; 874 } 875 876 mNumberOfBlocks = newNumberOfBlocks; 877 int newFirstChangedBlock; 878 final int deltaLines = newLineCount - (endLine - startLine + 1); 879 if (deltaLines != 0) { 880 // Display list whose index is >= mIndexFirstChangedBlock is valid 881 // but it needs to update its drawing location. 882 newFirstChangedBlock = firstBlock + numAddedBlocks; 883 for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) { 884 mBlockEndLines[i] += deltaLines; 885 } 886 } else { 887 newFirstChangedBlock = mNumberOfBlocks; 888 } 889 mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock); 890 891 int blockIndex = firstBlock; 892 if (createBlockBefore) { 893 mBlockEndLines[blockIndex] = startLine - 1; 894 updateAlwaysNeedsToBeRedrawn(blockIndex); 895 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 896 blockIndex++; 897 } 898 899 if (createBlock) { 900 mBlockEndLines[blockIndex] = startLine + newLineCount - 1; 901 updateAlwaysNeedsToBeRedrawn(blockIndex); 902 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 903 blockIndex++; 904 } 905 906 if (createBlockAfter) { 907 mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines; 908 updateAlwaysNeedsToBeRedrawn(blockIndex); 909 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 910 } 911 } 912 913 /** 914 * This method is used for test purposes only. 915 * @hide 916 */ 917 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 918 public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks, 919 int totalLines) { 920 mBlockEndLines = new int[blockEndLines.length]; 921 mBlockIndices = new int[blockIndices.length]; 922 System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length); 923 System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length); 924 mNumberOfBlocks = numberOfBlocks; 925 while (mInts.size() < totalLines) { 926 mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]); 927 } 928 } 929 930 /** 931 * @hide 932 */ 933 public int[] getBlockEndLines() { 934 return mBlockEndLines; 935 } 936 937 /** 938 * @hide 939 */ 940 public int[] getBlockIndices() { 941 return mBlockIndices; 942 } 943 944 /** 945 * @hide 946 */ 947 public int getBlockIndex(int index) { 948 return mBlockIndices[index]; 949 } 950 951 /** 952 * @hide 953 * @param index 954 */ 955 public void setBlockIndex(int index, int blockIndex) { 956 mBlockIndices[index] = blockIndex; 957 } 958 959 /** 960 * @hide 961 */ 962 public int getNumberOfBlocks() { 963 return mNumberOfBlocks; 964 } 965 966 /** 967 * @hide 968 */ 969 public int getIndexFirstChangedBlock() { 970 return mIndexFirstChangedBlock; 971 } 972 973 /** 974 * @hide 975 */ 976 public void setIndexFirstChangedBlock(int i) { 977 mIndexFirstChangedBlock = i; 978 } 979 980 @Override 981 public int getLineCount() { 982 return mInts.size() - 1; 983 } 984 985 @Override 986 public int getLineTop(int line) { 987 return mInts.getValue(line, TOP); 988 } 989 990 @Override 991 public int getLineDescent(int line) { 992 return mInts.getValue(line, DESCENT); 993 } 994 995 /** 996 * @hide 997 */ 998 @Override 999 public int getLineExtra(int line) { 1000 return mInts.getValue(line, EXTRA); 1001 } 1002 1003 @Override 1004 public int getLineStart(int line) { 1005 return mInts.getValue(line, START) & START_MASK; 1006 } 1007 1008 @Override 1009 public boolean getLineContainsTab(int line) { 1010 return (mInts.getValue(line, TAB) & TAB_MASK) != 0; 1011 } 1012 1013 @Override 1014 public int getParagraphDirection(int line) { 1015 return mInts.getValue(line, DIR) >> DIR_SHIFT; 1016 } 1017 1018 @Override 1019 public final Directions getLineDirections(int line) { 1020 return mObjects.getValue(line, 0); 1021 } 1022 1023 @Override 1024 public int getTopPadding() { 1025 return mTopPadding; 1026 } 1027 1028 @Override 1029 public int getBottomPadding() { 1030 return mBottomPadding; 1031 } 1032 1033 /** 1034 * @hide 1035 */ 1036 @Override 1037 public int getHyphen(int line) { 1038 return mInts.getValue(line, HYPHEN) & HYPHEN_MASK; 1039 } 1040 1041 private boolean getContentMayProtrudeFromTopOrBottom(int line) { 1042 return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM) 1043 & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0; 1044 } 1045 1046 @Override 1047 public int getEllipsizedWidth() { 1048 return mEllipsizedWidth; 1049 } 1050 1051 private static class ChangeWatcher implements TextWatcher, SpanWatcher { 1052 public ChangeWatcher(DynamicLayout layout) { 1053 mLayout = new WeakReference<DynamicLayout>(layout); 1054 } 1055 1056 private void reflow(CharSequence s, int where, int before, int after) { 1057 DynamicLayout ml = mLayout.get(); 1058 1059 if (ml != null) { 1060 ml.reflow(s, where, before, after); 1061 } else if (s instanceof Spannable) { 1062 ((Spannable) s).removeSpan(this); 1063 } 1064 } 1065 1066 public void beforeTextChanged(CharSequence s, int where, int before, int after) { 1067 // Intentionally empty 1068 } 1069 1070 public void onTextChanged(CharSequence s, int where, int before, int after) { 1071 reflow(s, where, before, after); 1072 } 1073 1074 public void afterTextChanged(Editable s) { 1075 // Intentionally empty 1076 } 1077 1078 public void onSpanAdded(Spannable s, Object o, int start, int end) { 1079 if (o instanceof UpdateLayout) 1080 reflow(s, start, end - start, end - start); 1081 } 1082 1083 public void onSpanRemoved(Spannable s, Object o, int start, int end) { 1084 if (o instanceof UpdateLayout) 1085 reflow(s, start, end - start, end - start); 1086 } 1087 1088 public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { 1089 if (o instanceof UpdateLayout) { 1090 reflow(s, start, end - start, end - start); 1091 reflow(s, nstart, nend - nstart, nend - nstart); 1092 } 1093 } 1094 1095 private WeakReference<DynamicLayout> mLayout; 1096 } 1097 1098 @Override 1099 public int getEllipsisStart(int line) { 1100 if (mEllipsizeAt == null) { 1101 return 0; 1102 } 1103 1104 return mInts.getValue(line, ELLIPSIS_START); 1105 } 1106 1107 @Override 1108 public int getEllipsisCount(int line) { 1109 if (mEllipsizeAt == null) { 1110 return 0; 1111 } 1112 1113 return mInts.getValue(line, ELLIPSIS_COUNT); 1114 } 1115 1116 private CharSequence mBase; 1117 private CharSequence mDisplay; 1118 private ChangeWatcher mWatcher; 1119 private boolean mIncludePad; 1120 private boolean mFallbackLineSpacing; 1121 private boolean mEllipsize; 1122 private int mEllipsizedWidth; 1123 private TextUtils.TruncateAt mEllipsizeAt; 1124 private int mBreakStrategy; 1125 private int mHyphenationFrequency; 1126 private int mJustificationMode; 1127 1128 private PackedIntVector mInts; 1129 private PackedObjectVector<Directions> mObjects; 1130 1131 /** 1132 * Value used in mBlockIndices when a block has been created or recycled and indicating that its 1133 * display list needs to be re-created. 1134 * @hide 1135 */ 1136 public static final int INVALID_BLOCK_INDEX = -1; 1137 // Stores the line numbers of the last line of each block (inclusive) 1138 private int[] mBlockEndLines; 1139 // The indices of this block's display list in TextView's internal display list array or 1140 // INVALID_BLOCK_INDEX if this block has been invalidated during an edition 1141 private int[] mBlockIndices; 1142 // Set of blocks that always need to be redrawn. 1143 private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn; 1144 // Number of items actually currently being used in the above 2 arrays 1145 private int mNumberOfBlocks; 1146 // The first index of the blocks whose locations are changed 1147 private int mIndexFirstChangedBlock; 1148 1149 private int mTopPadding, mBottomPadding; 1150 1151 private Rect mTempRect = new Rect(); 1152 1153 private static StaticLayout sStaticLayout = null; 1154 private static StaticLayout.Builder sBuilder = null; 1155 1156 private static final Object[] sLock = new Object[0]; 1157 1158 // START, DIR, and TAB share the same entry. 1159 private static final int START = 0; 1160 private static final int DIR = START; 1161 private static final int TAB = START; 1162 private static final int TOP = 1; 1163 private static final int DESCENT = 2; 1164 private static final int EXTRA = 3; 1165 // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry. 1166 private static final int HYPHEN = 4; 1167 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN; 1168 private static final int COLUMNS_NORMAL = 5; 1169 1170 private static final int ELLIPSIS_START = 5; 1171 private static final int ELLIPSIS_COUNT = 6; 1172 private static final int COLUMNS_ELLIPSIZE = 7; 1173 1174 private static final int START_MASK = 0x1FFFFFFF; 1175 private static final int DIR_SHIFT = 30; 1176 private static final int TAB_MASK = 0x20000000; 1177 private static final int HYPHEN_MASK = 0xFF; 1178 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100; 1179 1180 private static final int ELLIPSIS_UNDEFINED = 0x80000000; 1181} 1182