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