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