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