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