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