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_USE_BOUNDS_FOR_WIDTH; 21 22 import android.annotation.FlaggedApi; 23 import android.annotation.FloatRange; 24 import android.annotation.IntRange; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.annotation.SuppressLint; 28 import android.compat.annotation.UnsupportedAppUsage; 29 import android.graphics.Paint; 30 import android.graphics.RectF; 31 import android.graphics.text.LineBreakConfig; 32 import android.graphics.text.LineBreaker; 33 import android.os.Build; 34 import android.os.Trace; 35 import android.text.style.LeadingMarginSpan; 36 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; 37 import android.text.style.LineHeightSpan; 38 import android.text.style.TabStopSpan; 39 import android.util.Log; 40 import android.util.Pools.SynchronizedPool; 41 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.util.Arrays; 47 48 /** 49 * StaticLayout is a Layout for text that will not be edited after it 50 * is laid out. Use {@link DynamicLayout} for text that may change. 51 * <p>This is used by widgets to control text layout. You should not need 52 * to use this class directly unless you are implementing your own widget 53 * or custom display object, or would be tempted to call 54 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, 55 * float, float, android.graphics.Paint) 56 * Canvas.drawText()} directly.</p> 57 */ 58 @android.ravenwood.annotation.RavenwoodKeepWholeClass 59 public class StaticLayout extends Layout { 60 /* 61 * The break iteration is done in native code. The protocol for using the native code is as 62 * follows. 63 * 64 * First, call nInit to setup native line breaker object. Then, for each paragraph, do the 65 * following: 66 * 67 * - Create MeasuredParagraph by MeasuredParagraph.buildForStaticLayout which measures in 68 * native. 69 * - Run LineBreaker.computeLineBreaks() to obtain line breaks for the paragraph. 70 * 71 * After all paragraphs, call finish() to release expensive buffers. 72 */ 73 74 static final String TAG = "StaticLayout"; 75 76 /** 77 * Builder for static layouts. The builder is the preferred pattern for constructing 78 * StaticLayout objects and should be preferred over the constructors, particularly to access 79 * newer features. To build a static layout, first call {@link #obtain} with the required 80 * arguments (text, paint, and width), then call setters for optional parameters, and finally 81 * {@link #build} to build the StaticLayout object. Parameters not explicitly set will get 82 * default values. 83 */ 84 public final static class Builder { Builder()85 private Builder() {} 86 87 /** 88 * Obtain a builder for constructing StaticLayout objects. 89 * 90 * @param source The text to be laid out, optionally with spans 91 * @param start The index of the start of the text 92 * @param end The index + 1 of the end of the text 93 * @param paint The base paint used for layout 94 * @param width The width in pixels 95 * @return a builder object used for constructing the StaticLayout 96 */ 97 @NonNull obtain(@onNull CharSequence source, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextPaint paint, @IntRange(from = 0) int width)98 public static Builder obtain(@NonNull CharSequence source, @IntRange(from = 0) int start, 99 @IntRange(from = 0) int end, @NonNull TextPaint paint, 100 @IntRange(from = 0) int width) { 101 Builder b = sPool.acquire(); 102 if (b == null) { 103 b = new Builder(); 104 } 105 106 // set default initial values 107 b.mText = source; 108 b.mStart = start; 109 b.mEnd = end; 110 b.mPaint = paint; 111 b.mWidth = width; 112 b.mAlignment = Alignment.ALIGN_NORMAL; 113 b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; 114 b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER; 115 b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION; 116 b.mIncludePad = true; 117 b.mFallbackLineSpacing = false; 118 b.mEllipsizedWidth = width; 119 b.mEllipsize = null; 120 b.mMaxLines = Integer.MAX_VALUE; 121 b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE; 122 b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; 123 b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; 124 b.mLineBreakConfig = LineBreakConfig.NONE; 125 b.mUseBoundsForWidth = false; 126 b.mMinimumFontMetrics = null; 127 return b; 128 } 129 130 /** 131 * This method should be called after the layout is finished getting constructed and the 132 * builder needs to be cleaned up and returned to the pool. 133 */ recycle(@onNull Builder b)134 private static void recycle(@NonNull Builder b) { 135 b.mPaint = null; 136 b.mText = null; 137 b.mLeftIndents = null; 138 b.mRightIndents = null; 139 b.mMinimumFontMetrics = null; 140 sPool.release(b); 141 } 142 143 // release any expensive state finish()144 /* package */ void finish() { 145 mText = null; 146 mPaint = null; 147 mLeftIndents = null; 148 mRightIndents = null; 149 mMinimumFontMetrics = null; 150 } 151 setText(CharSequence source)152 public Builder setText(CharSequence source) { 153 return setText(source, 0, source.length()); 154 } 155 156 /** 157 * Set the text. Only useful when re-using the builder, which is done for 158 * the internal implementation of {@link DynamicLayout} but not as part 159 * of normal {@link StaticLayout} usage. 160 * 161 * @param source The text to be laid out, optionally with spans 162 * @param start The index of the start of the text 163 * @param end The index + 1 of the end of the text 164 * @return this builder, useful for chaining 165 * 166 * @hide 167 */ 168 @NonNull setText(@onNull CharSequence source, int start, int end)169 public Builder setText(@NonNull CharSequence source, int start, int end) { 170 mText = source; 171 mStart = start; 172 mEnd = end; 173 return this; 174 } 175 176 /** 177 * Set the paint. Internal for reuse cases only. 178 * 179 * @param paint The base paint used for layout 180 * @return this builder, useful for chaining 181 * 182 * @hide 183 */ 184 @NonNull setPaint(@onNull TextPaint paint)185 public Builder setPaint(@NonNull TextPaint paint) { 186 mPaint = paint; 187 return this; 188 } 189 190 /** 191 * Set the width. Internal for reuse cases only. 192 * 193 * @param width The width in pixels 194 * @return this builder, useful for chaining 195 * 196 * @hide 197 */ 198 @NonNull setWidth(@ntRangefrom = 0) int width)199 public Builder setWidth(@IntRange(from = 0) int width) { 200 mWidth = width; 201 if (mEllipsize == null) { 202 mEllipsizedWidth = width; 203 } 204 return this; 205 } 206 207 /** 208 * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}. 209 * 210 * @param alignment Alignment for the resulting {@link StaticLayout} 211 * @return this builder, useful for chaining 212 */ 213 @NonNull setAlignment(@onNull Alignment alignment)214 public Builder setAlignment(@NonNull Alignment alignment) { 215 mAlignment = alignment; 216 return this; 217 } 218 219 /** 220 * Set the text direction heuristic. The text direction heuristic is used to 221 * resolve text direction per-paragraph based on the input text. The default is 222 * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. 223 * 224 * @param textDir text direction heuristic for resolving bidi behavior. 225 * @return this builder, useful for chaining 226 */ 227 @NonNull setTextDirection(@onNull TextDirectionHeuristic textDir)228 public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { 229 mTextDir = textDir; 230 return this; 231 } 232 233 /** 234 * Set line spacing parameters. Each line will have its line spacing multiplied by 235 * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for 236 * {@code spacingAdd} and 1.0 for {@code spacingMult}. 237 * 238 * @param spacingAdd the amount of line spacing addition 239 * @param spacingMult the line spacing multiplier 240 * @return this builder, useful for chaining 241 * @see android.widget.TextView#setLineSpacing 242 */ 243 @NonNull setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult)244 public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) { 245 mSpacingAdd = spacingAdd; 246 mSpacingMult = spacingMult; 247 return this; 248 } 249 250 /** 251 * Set whether to include extra space beyond font ascent and descent (which is 252 * needed to avoid clipping in some languages, such as Arabic and Kannada). The 253 * default is {@code true}. 254 * 255 * @param includePad whether to include padding 256 * @return this builder, useful for chaining 257 * @see android.widget.TextView#setIncludeFontPadding 258 */ 259 @NonNull setIncludePad(boolean includePad)260 public Builder setIncludePad(boolean includePad) { 261 mIncludePad = includePad; 262 return this; 263 } 264 265 /** 266 * Set whether to respect the ascent and descent of the fallback fonts that are used in 267 * displaying the text (which is needed to avoid text from consecutive lines running into 268 * each other). If set, fallback fonts that end up getting used can increase the ascent 269 * and descent of the lines that they are used on. 270 * 271 * <p>For backward compatibility reasons, the default is {@code false}, but setting this to 272 * true is strongly recommended. It is required to be true if text could be in languages 273 * like Burmese or Tibetan where text is typically much taller or deeper than Latin text. 274 * 275 * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts 276 * @return this builder, useful for chaining 277 */ 278 @NonNull setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks)279 public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) { 280 mFallbackLineSpacing = useLineSpacingFromFallbacks; 281 return this; 282 } 283 284 /** 285 * Set the width as used for ellipsizing purposes, if it differs from the 286 * normal layout width. The default is the {@code width} 287 * passed to {@link #obtain}. 288 * 289 * @param ellipsizedWidth width used for ellipsizing, in pixels 290 * @return this builder, useful for chaining 291 * @see android.widget.TextView#setEllipsize 292 */ 293 @NonNull setEllipsizedWidth(@ntRangefrom = 0) int ellipsizedWidth)294 public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) { 295 mEllipsizedWidth = ellipsizedWidth; 296 return this; 297 } 298 299 /** 300 * Set ellipsizing on the layout. Causes words that are longer than the view 301 * is wide, or exceeding the number of lines (see #setMaxLines) in the case 302 * of {@link android.text.TextUtils.TruncateAt#END} or 303 * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead 304 * of broken. The default is {@code null}, indicating no ellipsis is to be applied. 305 * 306 * @param ellipsize type of ellipsis behavior 307 * @return this builder, useful for chaining 308 * @see android.widget.TextView#setEllipsize 309 */ 310 @NonNull setEllipsize(@ullable TextUtils.TruncateAt ellipsize)311 public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) { 312 mEllipsize = ellipsize; 313 return this; 314 } 315 316 /** 317 * Set maximum number of lines. This is particularly useful in the case of 318 * ellipsizing, where it changes the layout of the last line. The default is 319 * unlimited. 320 * 321 * @param maxLines maximum number of lines in the layout 322 * @return this builder, useful for chaining 323 * @see android.widget.TextView#setMaxLines 324 */ 325 @NonNull setMaxLines(@ntRangefrom = 0) int maxLines)326 public Builder setMaxLines(@IntRange(from = 0) int maxLines) { 327 mMaxLines = maxLines; 328 return this; 329 } 330 331 /** 332 * Set break strategy, useful for selecting high quality or balanced paragraph 333 * layout options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}. 334 * <p/> 335 * Enabling hyphenation with either using {@link Layout#HYPHENATION_FREQUENCY_NORMAL} or 336 * {@link Layout#HYPHENATION_FREQUENCY_FULL} while line breaking is set to one of 337 * {@link Layout#BREAK_STRATEGY_BALANCED}, {@link Layout#BREAK_STRATEGY_HIGH_QUALITY} 338 * improves the structure of text layout however has performance impact and requires more 339 * time to do the text layout. 340 * 341 * @param breakStrategy break strategy for paragraph layout 342 * @return this builder, useful for chaining 343 * @see android.widget.TextView#setBreakStrategy 344 * @see #setHyphenationFrequency(int) 345 */ 346 @NonNull setBreakStrategy(@reakStrategy int breakStrategy)347 public Builder setBreakStrategy(@BreakStrategy int breakStrategy) { 348 mBreakStrategy = breakStrategy; 349 return this; 350 } 351 352 /** 353 * Set hyphenation frequency, to control the amount of automatic hyphenation used. The 354 * possible values are defined in {@link Layout}, by constants named with the pattern 355 * {@code HYPHENATION_FREQUENCY_*}. The default is 356 * {@link Layout#HYPHENATION_FREQUENCY_NONE}. 357 * <p/> 358 * Enabling hyphenation with either using {@link Layout#HYPHENATION_FREQUENCY_NORMAL} or 359 * {@link Layout#HYPHENATION_FREQUENCY_FULL} while line breaking is set to one of 360 * {@link Layout#BREAK_STRATEGY_BALANCED}, {@link Layout#BREAK_STRATEGY_HIGH_QUALITY} 361 * improves the structure of text layout however has performance impact and requires more 362 * time to do the text layout. 363 * 364 * @param hyphenationFrequency hyphenation frequency for the paragraph 365 * @return this builder, useful for chaining 366 * @see android.widget.TextView#setHyphenationFrequency 367 * @see #setBreakStrategy(int) 368 */ 369 @NonNull setHyphenationFrequency(@yphenationFrequency int hyphenationFrequency)370 public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) { 371 mHyphenationFrequency = hyphenationFrequency; 372 return this; 373 } 374 375 /** 376 * Set indents. Arguments are arrays holding an indent amount, one per line, measured in 377 * pixels. For lines past the last element in the array, the last element repeats. 378 * 379 * @param leftIndents array of indent values for left margin, in pixels 380 * @param rightIndents array of indent values for right margin, in pixels 381 * @return this builder, useful for chaining 382 */ 383 @NonNull setIndents(@ullable int[] leftIndents, @Nullable int[] rightIndents)384 public Builder setIndents(@Nullable int[] leftIndents, @Nullable int[] rightIndents) { 385 mLeftIndents = leftIndents; 386 mRightIndents = rightIndents; 387 return this; 388 } 389 390 /** 391 * Set paragraph justification mode. The default value is 392 * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification, 393 * the last line will be displayed with the alignment set by {@link #setAlignment}. 394 * When Justification mode is JUSTIFICATION_MODE_INTER_WORD, wordSpacing on the given 395 * {@link Paint} will be ignored. This behavior also affects Spans which change the 396 * wordSpacing. 397 * 398 * @param justificationMode justification mode for the paragraph. 399 * @return this builder, useful for chaining. 400 * @see Paint#setWordSpacing(float) 401 */ 402 @NonNull setJustificationMode(@ustificationMode int justificationMode)403 public Builder setJustificationMode(@JustificationMode int justificationMode) { 404 mJustificationMode = justificationMode; 405 return this; 406 } 407 408 /** 409 * Sets whether the line spacing should be applied for the last line. Default value is 410 * {@code false}. 411 * 412 * @hide 413 */ 414 @NonNull setAddLastLineLineSpacing(boolean value)415 /* package */ Builder setAddLastLineLineSpacing(boolean value) { 416 mAddLastLineLineSpacing = value; 417 return this; 418 } 419 420 /** 421 * Set the line break configuration. The line break will be passed to native used for 422 * calculating the text wrapping. The default value of the line break style is 423 * {@link LineBreakConfig#LINE_BREAK_STYLE_NONE} 424 * 425 * @param lineBreakConfig the line break configuration for text wrapping. 426 * @return this builder, useful for chaining. 427 * @see android.widget.TextView#setLineBreakStyle 428 * @see android.widget.TextView#setLineBreakWordStyle 429 */ 430 @NonNull setLineBreakConfig(@onNull LineBreakConfig lineBreakConfig)431 public Builder setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) { 432 mLineBreakConfig = lineBreakConfig; 433 return this; 434 } 435 436 /** 437 * Set true for using width of bounding box as a source of automatic line breaking and 438 * drawing. 439 * 440 * If this value is false, the Layout determines the drawing offset and automatic line 441 * breaking based on total advances. By setting true, use all joined glyph's bounding boxes 442 * as a source of text width. 443 * 444 * If the font has glyphs that have negative bearing X or its xMax is greater than advance, 445 * the glyph clipping can happen because the drawing area may be bigger. By setting this to 446 * true, the Layout will reserve more spaces for drawing. 447 * 448 * @param useBoundsForWidth True for using bounding box, false for advances. 449 * @return this builder instance 450 * @see Layout#getUseBoundsForWidth() 451 * @see Layout.Builder#setUseBoundsForWidth(boolean) 452 */ 453 @SuppressLint("MissingGetterMatchingBuilder") // The base class `Layout` has a getter. 454 @NonNull 455 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) setUseBoundsForWidth(boolean useBoundsForWidth)456 public Builder setUseBoundsForWidth(boolean useBoundsForWidth) { 457 mUseBoundsForWidth = useBoundsForWidth; 458 return this; 459 } 460 461 /** 462 * Set true for shifting the drawing x offset for showing overhang at the start position. 463 * 464 * This flag is ignored if the {@link #getUseBoundsForWidth()} is false. 465 * 466 * If this value is false, the Layout draws text from the zero even if there is a glyph 467 * stroke in a region where the x coordinate is negative. 468 * 469 * If this value is true, the Layout draws text with shifting the x coordinate of the 470 * drawing bounding box. 471 * 472 * This value is false by default. 473 * 474 * @param shiftDrawingOffsetForStartOverhang true for shifting the drawing offset for 475 * showing the stroke that is in the region where 476 * the x coordinate is negative. 477 * @see #setUseBoundsForWidth(boolean) 478 * @see #getUseBoundsForWidth() 479 */ 480 @NonNull 481 // The corresponding getter is getShiftDrawingOffsetForStartOverhang() 482 @SuppressLint("MissingGetterMatchingBuilder") 483 @FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH) setShiftDrawingOffsetForStartOverhang( boolean shiftDrawingOffsetForStartOverhang)484 public Builder setShiftDrawingOffsetForStartOverhang( 485 boolean shiftDrawingOffsetForStartOverhang) { 486 mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang; 487 return this; 488 } 489 490 /** 491 * Internal API that tells underlying line breaker that calculating bounding boxes even if 492 * the line break is performed with advances. This is useful for DynamicLayout internal 493 * implementation because it uses bounding box as well as advances. 494 * @hide 495 */ setCalculateBounds(boolean value)496 public Builder setCalculateBounds(boolean value) { 497 mCalculateBounds = value; 498 return this; 499 } 500 501 /** 502 * Set the minimum font metrics used for line spacing. 503 * 504 * <p> 505 * {@code null} is the default value. If {@code null} is set or left as default, the 506 * font metrics obtained by {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} is 507 * used. 508 * 509 * <p> 510 * The minimum meaning here is the minimum value of line spacing: maximum value of 511 * {@link Paint#ascent()}, minimum value of {@link Paint#descent()}. 512 * 513 * <p> 514 * By setting this value, each line will have minimum line spacing regardless of the text 515 * rendered. For example, usually Japanese script has larger vertical metrics than Latin 516 * script. By setting the metrics obtained by 517 * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} for Japanese or leave it 518 * {@code null} if the Paint's locale is Japanese, the line spacing for Japanese is reserved 519 * if the text is an English text. If the vertical metrics of the text is larger than 520 * Japanese, for example Burmese, the bigger font metrics is used. 521 * 522 * @param minimumFontMetrics A minimum font metrics. Passing {@code null} for using the 523 * value obtained by 524 * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} 525 * @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics) 526 * @see android.widget.TextView#getMinimumFontMetrics() 527 * @see Layout#getMinimumFontMetrics() 528 * @see Layout.Builder#setMinimumFontMetrics(Paint.FontMetrics) 529 * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) 530 */ 531 @NonNull 532 @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) setMinimumFontMetrics(@ullable Paint.FontMetrics minimumFontMetrics)533 public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) { 534 mMinimumFontMetrics = minimumFontMetrics; 535 return this; 536 } 537 538 /** 539 * Build the {@link StaticLayout} after options have been set. 540 * 541 * <p>Note: the builder object must not be reused in any way after calling this 542 * method. Setting parameters after calling this method, or calling it a second 543 * time on the same builder object, will likely lead to unexpected results. 544 * 545 * @return the newly constructed {@link StaticLayout} object 546 */ 547 @NonNull build()548 public StaticLayout build() { 549 StaticLayout result = new StaticLayout(this, mIncludePad, mEllipsize != null 550 ? COLUMNS_ELLIPSIZE : COLUMNS_NORMAL); 551 Builder.recycle(this); 552 return result; 553 } 554 555 /** 556 * DO NOT USE THIS METHOD OTHER THAN DynamicLayout. 557 * 558 * This class generates a very weird StaticLayout only for getting a result of line break. 559 * Since DynamicLayout keeps StaticLayout reference in the static context for object 560 * recycling but keeping text reference in static context will end up with leaking Context 561 * due to TextWatcher via TextView. 562 * 563 * So, this is a dirty work around that creating StaticLayout without passing text reference 564 * to the super constructor, but calculating the text layout by calling generate function 565 * directly. 566 */ buildPartialStaticLayoutForDynamicLayout( boolean trackpadding, StaticLayout recycle)567 /* package */ @NonNull StaticLayout buildPartialStaticLayoutForDynamicLayout( 568 boolean trackpadding, StaticLayout recycle) { 569 if (recycle == null) { 570 recycle = new StaticLayout(); 571 } 572 Trace.beginSection("Generating StaticLayout For DynamicLayout"); 573 try { 574 recycle.generate(this, mIncludePad, trackpadding); 575 } finally { 576 Trace.endSection(); 577 } 578 return recycle; 579 } 580 581 private CharSequence mText; 582 private int mStart; 583 private int mEnd; 584 private TextPaint mPaint; 585 private int mWidth; 586 private Alignment mAlignment; 587 private TextDirectionHeuristic mTextDir; 588 private float mSpacingMult; 589 private float mSpacingAdd; 590 private boolean mIncludePad; 591 private boolean mFallbackLineSpacing; 592 private int mEllipsizedWidth; 593 private TextUtils.TruncateAt mEllipsize; 594 private int mMaxLines; 595 private int mBreakStrategy; 596 private int mHyphenationFrequency; 597 @Nullable private int[] mLeftIndents; 598 @Nullable private int[] mRightIndents; 599 private int mJustificationMode; 600 private boolean mAddLastLineLineSpacing; 601 private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE; 602 private boolean mUseBoundsForWidth; 603 private boolean mShiftDrawingOffsetForStartOverhang; 604 private boolean mCalculateBounds; 605 @Nullable private Paint.FontMetrics mMinimumFontMetrics; 606 607 private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); 608 609 private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3); 610 } 611 612 /** 613 * DO NOT USE THIS CONSTRUCTOR OTHER THAN FOR DYNAMIC LAYOUT. 614 * See Builder#buildPartialStaticLayoutForDynamicLayout for the reason of this constructor. 615 */ StaticLayout()616 private StaticLayout() { 617 super( 618 null, // text 619 null, // paint 620 0, // width 621 null, // alignment 622 null, // textDir 623 1, // spacing multiplier 624 0, // spacing amount 625 false, // include font padding 626 false, // fallback line spacing 627 0, // ellipsized width 628 null, // ellipsize 629 1, // maxLines 630 BREAK_STRATEGY_SIMPLE, 631 HYPHENATION_FREQUENCY_NONE, 632 null, // leftIndents 633 null, // rightIndents 634 JUSTIFICATION_MODE_NONE, 635 null, // lineBreakConfig, 636 false, // useBoundsForWidth 637 false, // shiftDrawingOffsetForStartOverhang 638 null // minimumFontMetrics 639 ); 640 641 mColumns = COLUMNS_ELLIPSIZE; 642 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2); 643 mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns); 644 } 645 646 /** 647 * @deprecated Use {@link Builder} instead. 648 */ 649 @Deprecated StaticLayout(CharSequence source, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)650 public StaticLayout(CharSequence source, TextPaint paint, 651 int width, 652 Alignment align, float spacingmult, float spacingadd, 653 boolean includepad) { 654 this(source, 0, source.length(), paint, width, align, 655 spacingmult, spacingadd, includepad); 656 } 657 658 /** 659 * @deprecated Use {@link Builder} instead. 660 */ 661 @Deprecated StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad)662 public StaticLayout(CharSequence source, int bufstart, int bufend, 663 TextPaint paint, int outerwidth, 664 Alignment align, 665 float spacingmult, float spacingadd, 666 boolean includepad) { 667 this(source, bufstart, bufend, paint, outerwidth, align, 668 spacingmult, spacingadd, includepad, null, 0); 669 } 670 671 /** 672 * @deprecated Use {@link Builder} instead. 673 */ 674 @Deprecated StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)675 public StaticLayout(CharSequence source, int bufstart, int bufend, 676 TextPaint paint, int outerwidth, 677 Alignment align, 678 float spacingmult, float spacingadd, 679 boolean includepad, 680 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 681 this(source, bufstart, bufend, paint, outerwidth, align, 682 TextDirectionHeuristics.FIRSTSTRONG_LTR, 683 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE); 684 } 685 686 /** 687 * @hide 688 * @deprecated Use {@link Builder} instead. 689 */ 690 @Deprecated 691 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 117521430) StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines)692 public StaticLayout(CharSequence source, int bufstart, int bufend, 693 TextPaint paint, int outerwidth, 694 Alignment align, TextDirectionHeuristic textDir, 695 float spacingmult, float spacingadd, 696 boolean includepad, 697 TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) { 698 this(Builder.obtain(source, bufstart, bufend, paint, outerwidth) 699 .setAlignment(align) 700 .setTextDirection(textDir) 701 .setLineSpacing(spacingadd, spacingmult) 702 .setIncludePad(includepad) 703 .setEllipsize(ellipsize) 704 .setEllipsizedWidth(ellipsizedWidth) 705 .setMaxLines(maxLines), includepad, 706 ellipsize != null ? COLUMNS_ELLIPSIZE : COLUMNS_NORMAL); 707 } 708 StaticLayout(Builder b, boolean trackPadding, int columnSize)709 private StaticLayout(Builder b, boolean trackPadding, int columnSize) { 710 super((b.mEllipsize == null) ? b.mText : (b.mText instanceof Spanned) 711 ? new SpannedEllipsizer(b.mText) : new Ellipsizer(b.mText), 712 b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd, 713 b.mIncludePad, b.mFallbackLineSpacing, b.mEllipsizedWidth, b.mEllipsize, 714 b.mMaxLines, b.mBreakStrategy, b.mHyphenationFrequency, b.mLeftIndents, 715 b.mRightIndents, b.mJustificationMode, b.mLineBreakConfig, b.mUseBoundsForWidth, 716 b.mShiftDrawingOffsetForStartOverhang, b.mMinimumFontMetrics); 717 718 mColumns = columnSize; 719 if (b.mEllipsize != null) { 720 Ellipsizer e = (Ellipsizer) getText(); 721 722 e.mLayout = this; 723 e.mWidth = b.mEllipsizedWidth; 724 e.mMethod = b.mEllipsize; 725 } 726 727 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2); 728 mLines = ArrayUtils.newUnpaddedIntArray(2 * mColumns); 729 mMaximumVisibleLineCount = b.mMaxLines; 730 731 mLeftIndents = b.mLeftIndents; 732 mRightIndents = b.mRightIndents; 733 734 Trace.beginSection("Constructing StaticLayout"); 735 try { 736 generate(b, b.mIncludePad, trackPadding); 737 } finally { 738 Trace.endSection(); 739 } 740 } 741 getBaseHyphenationFrequency(int frequency)742 private static int getBaseHyphenationFrequency(int frequency) { 743 switch (frequency) { 744 case Layout.HYPHENATION_FREQUENCY_FULL: 745 case Layout.HYPHENATION_FREQUENCY_FULL_FAST: 746 return LineBreaker.HYPHENATION_FREQUENCY_FULL; 747 case Layout.HYPHENATION_FREQUENCY_NORMAL: 748 case Layout.HYPHENATION_FREQUENCY_NORMAL_FAST: 749 return LineBreaker.HYPHENATION_FREQUENCY_NORMAL; 750 case Layout.HYPHENATION_FREQUENCY_NONE: 751 default: 752 return LineBreaker.HYPHENATION_FREQUENCY_NONE; 753 } 754 } 755 generate(Builder b, boolean includepad, boolean trackpad)756 /* package */ void generate(Builder b, boolean includepad, boolean trackpad) { 757 final CharSequence source = b.mText; 758 final int bufStart = b.mStart; 759 final int bufEnd = b.mEnd; 760 TextPaint paint = b.mPaint; 761 int outerWidth = b.mWidth; 762 TextDirectionHeuristic textDir = b.mTextDir; 763 float spacingmult = b.mSpacingMult; 764 float spacingadd = b.mSpacingAdd; 765 float ellipsizedWidth = b.mEllipsizedWidth; 766 TextUtils.TruncateAt ellipsize = b.mEllipsize; 767 final boolean addLastLineSpacing = b.mAddLastLineLineSpacing; 768 769 int lineBreakCapacity = 0; 770 int[] breaks = null; 771 float[] lineWidths = null; 772 float[] ascents = null; 773 float[] descents = null; 774 boolean[] hasTabs = null; 775 int[] hyphenEdits = null; 776 777 mLineCount = 0; 778 mEllipsized = false; 779 mMaxLineHeight = mMaximumVisibleLineCount < 1 ? 0 : DEFAULT_MAX_LINE_HEIGHT; 780 mDrawingBounds = null; 781 boolean isFallbackLineSpacing = b.mFallbackLineSpacing; 782 783 int v = 0; 784 boolean needMultiply = (spacingmult != 1 || spacingadd != 0); 785 786 Paint.FontMetricsInt fm = b.mFontMetricsInt; 787 int[] chooseHtv = null; 788 789 final int[] indents; 790 if (mLeftIndents != null || mRightIndents != null) { 791 final int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length; 792 final int rightLen = mRightIndents == null ? 0 : mRightIndents.length; 793 final int indentsLen = Math.max(leftLen, rightLen); 794 indents = new int[indentsLen]; 795 for (int i = 0; i < leftLen; i++) { 796 indents[i] = mLeftIndents[i]; 797 } 798 for (int i = 0; i < rightLen; i++) { 799 indents[i] += mRightIndents[i]; 800 } 801 } else { 802 indents = null; 803 } 804 805 int defaultTop; 806 final int defaultAscent; 807 final int defaultDescent; 808 int defaultBottom; 809 if (Flags.fixLineHeightForLocale() && b.mMinimumFontMetrics != null) { 810 defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top); 811 defaultAscent = Math.round(b.mMinimumFontMetrics.ascent); 812 defaultDescent = Math.round(b.mMinimumFontMetrics.descent); 813 defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom); 814 815 // Because the font metrics is provided by public APIs, adjust the top/bottom with 816 // ascent/descent: top must be smaller than ascent, bottom must be larger than descent. 817 defaultTop = Math.min(defaultTop, defaultAscent); 818 defaultBottom = Math.max(defaultBottom, defaultDescent); 819 } else { 820 defaultTop = 0; 821 defaultAscent = 0; 822 defaultDescent = 0; 823 defaultBottom = 0; 824 } 825 826 final LineBreaker lineBreaker = new LineBreaker.Builder() 827 .setBreakStrategy(b.mBreakStrategy) 828 .setHyphenationFrequency(getBaseHyphenationFrequency(b.mHyphenationFrequency)) 829 // TODO: Support more justification mode, e.g. letter spacing, stretching. 830 .setJustificationMode(b.mJustificationMode) 831 .setIndents(indents) 832 .setUseBoundsForWidth(b.mUseBoundsForWidth) 833 .build(); 834 835 LineBreaker.ParagraphConstraints constraints = 836 new LineBreaker.ParagraphConstraints(); 837 838 PrecomputedText.ParagraphInfo[] paragraphInfo = null; 839 final Spanned spanned = (source instanceof Spanned) ? (Spanned) source : null; 840 if (source instanceof PrecomputedText) { 841 PrecomputedText precomputed = (PrecomputedText) source; 842 final @PrecomputedText.Params.CheckResultUsableResult int checkResult = 843 precomputed.checkResultUsable(bufStart, bufEnd, textDir, paint, 844 b.mBreakStrategy, b.mHyphenationFrequency, b.mLineBreakConfig); 845 switch (checkResult) { 846 case PrecomputedText.Params.UNUSABLE: 847 break; 848 case PrecomputedText.Params.NEED_RECOMPUTE: 849 final PrecomputedText.Params newParams = 850 new PrecomputedText.Params.Builder(paint) 851 .setBreakStrategy(b.mBreakStrategy) 852 .setHyphenationFrequency(b.mHyphenationFrequency) 853 .setTextDirection(textDir) 854 .setLineBreakConfig(b.mLineBreakConfig) 855 .build(); 856 precomputed = PrecomputedText.create(precomputed, newParams); 857 paragraphInfo = precomputed.getParagraphInfo(); 858 break; 859 case PrecomputedText.Params.USABLE: 860 // Some parameters are different from the ones when measured text is created. 861 paragraphInfo = precomputed.getParagraphInfo(); 862 break; 863 } 864 } 865 866 if (paragraphInfo == null) { 867 final PrecomputedText.Params param = new PrecomputedText.Params(paint, 868 b.mLineBreakConfig, textDir, b.mBreakStrategy, b.mHyphenationFrequency); 869 paragraphInfo = PrecomputedText.createMeasuredParagraphs(source, param, bufStart, 870 bufEnd, false /* computeLayout */, b.mCalculateBounds); 871 } 872 873 for (int paraIndex = 0; paraIndex < paragraphInfo.length; paraIndex++) { 874 final int paraStart = paraIndex == 0 875 ? bufStart : paragraphInfo[paraIndex - 1].paragraphEnd; 876 final int paraEnd = paragraphInfo[paraIndex].paragraphEnd; 877 878 int firstWidthLineCount = 1; 879 int firstWidth = outerWidth; 880 int restWidth = outerWidth; 881 882 LineHeightSpan[] chooseHt = null; 883 if (spanned != null) { 884 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd, 885 LeadingMarginSpan.class); 886 for (int i = 0; i < sp.length; i++) { 887 LeadingMarginSpan lms = sp[i]; 888 firstWidth -= sp[i].getLeadingMargin(true); 889 restWidth -= sp[i].getLeadingMargin(false); 890 891 // LeadingMarginSpan2 is odd. The count affects all 892 // leading margin spans, not just this particular one 893 if (lms instanceof LeadingMarginSpan2) { 894 LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms; 895 firstWidthLineCount = Math.max(firstWidthLineCount, 896 lms2.getLeadingMarginLineCount()); 897 } 898 } 899 900 chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class); 901 902 if (chooseHt.length == 0) { 903 chooseHt = null; // So that out() would not assume it has any contents 904 } else { 905 if (chooseHtv == null || chooseHtv.length < chooseHt.length) { 906 chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length); 907 } 908 909 for (int i = 0; i < chooseHt.length; i++) { 910 int o = spanned.getSpanStart(chooseHt[i]); 911 912 if (o < paraStart) { 913 // starts in this layout, before the 914 // current paragraph 915 916 chooseHtv[i] = getLineTop(getLineForOffset(o)); 917 } else { 918 // starts in this paragraph 919 920 chooseHtv[i] = v; 921 } 922 } 923 } 924 } 925 // tab stop locations 926 float[] variableTabStops = null; 927 if (spanned != null) { 928 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, 929 paraEnd, TabStopSpan.class); 930 if (spans.length > 0) { 931 float[] stops = new float[spans.length]; 932 for (int i = 0; i < spans.length; i++) { 933 stops[i] = (float) spans[i].getTabStop(); 934 } 935 Arrays.sort(stops, 0, stops.length); 936 variableTabStops = stops; 937 } 938 } 939 940 final MeasuredParagraph measuredPara = paragraphInfo[paraIndex].measured; 941 final char[] chs = measuredPara.getChars(); 942 final int[] spanEndCache = measuredPara.getSpanEndCache().getRawArray(); 943 final int[] fmCache = measuredPara.getFontMetrics().getRawArray(); 944 945 constraints.setWidth(restWidth); 946 constraints.setIndent(firstWidth, firstWidthLineCount); 947 constraints.setTabStops(variableTabStops, TAB_INCREMENT); 948 949 LineBreaker.Result res = lineBreaker.computeLineBreaks( 950 measuredPara.getMeasuredText(), constraints, mLineCount); 951 int breakCount = res.getLineCount(); 952 if (lineBreakCapacity < breakCount) { 953 lineBreakCapacity = breakCount; 954 breaks = new int[lineBreakCapacity]; 955 lineWidths = new float[lineBreakCapacity]; 956 ascents = new float[lineBreakCapacity]; 957 descents = new float[lineBreakCapacity]; 958 hasTabs = new boolean[lineBreakCapacity]; 959 hyphenEdits = new int[lineBreakCapacity]; 960 } 961 962 for (int i = 0; i < breakCount; ++i) { 963 breaks[i] = res.getLineBreakOffset(i); 964 lineWidths[i] = res.getLineWidth(i); 965 ascents[i] = res.getLineAscent(i); 966 descents[i] = res.getLineDescent(i); 967 hasTabs[i] = res.hasLineTab(i); 968 hyphenEdits[i] = 969 packHyphenEdit(res.getStartLineHyphenEdit(i), res.getEndLineHyphenEdit(i)); 970 } 971 972 final int remainingLineCount = mMaximumVisibleLineCount - mLineCount; 973 final boolean ellipsisMayBeApplied = ellipsize != null 974 && (ellipsize == TextUtils.TruncateAt.END 975 || (mMaximumVisibleLineCount == 1 976 && ellipsize != TextUtils.TruncateAt.MARQUEE)); 977 if (0 < remainingLineCount && remainingLineCount < breakCount 978 && ellipsisMayBeApplied) { 979 // Calculate width 980 float width = 0; 981 boolean hasTab = false; // XXX May need to also have starting hyphen edit 982 for (int i = remainingLineCount - 1; i < breakCount; i++) { 983 if (i == breakCount - 1) { 984 width += lineWidths[i]; 985 } else { 986 for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) { 987 width += measuredPara.getCharWidthAt(j); 988 } 989 } 990 hasTab |= hasTabs[i]; 991 } 992 // Treat the last line and overflowed lines as a single line. 993 breaks[remainingLineCount - 1] = breaks[breakCount - 1]; 994 lineWidths[remainingLineCount - 1] = width; 995 hasTabs[remainingLineCount - 1] = hasTab; 996 997 breakCount = remainingLineCount; 998 } 999 1000 // here is the offset of the starting character of the line we are currently 1001 // measuring 1002 int here = paraStart; 1003 1004 int fmTop = defaultTop; 1005 int fmBottom = defaultBottom; 1006 int fmAscent = defaultAscent; 1007 int fmDescent = defaultDescent; 1008 int fmCacheIndex = 0; 1009 int spanEndCacheIndex = 0; 1010 int breakIndex = 0; 1011 for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { 1012 // retrieve end of span 1013 spanEnd = spanEndCache[spanEndCacheIndex++]; 1014 1015 // retrieve cached metrics, order matches above 1016 fm.top = fmCache[fmCacheIndex * 4 + 0]; 1017 fm.bottom = fmCache[fmCacheIndex * 4 + 1]; 1018 fm.ascent = fmCache[fmCacheIndex * 4 + 2]; 1019 fm.descent = fmCache[fmCacheIndex * 4 + 3]; 1020 fmCacheIndex++; 1021 1022 if (fm.top < fmTop) { 1023 fmTop = fm.top; 1024 } 1025 if (fm.ascent < fmAscent) { 1026 fmAscent = fm.ascent; 1027 } 1028 if (fm.descent > fmDescent) { 1029 fmDescent = fm.descent; 1030 } 1031 if (fm.bottom > fmBottom) { 1032 fmBottom = fm.bottom; 1033 } 1034 1035 // skip breaks ending before current span range 1036 while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) { 1037 breakIndex++; 1038 } 1039 1040 while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) { 1041 int endPos = paraStart + breaks[breakIndex]; 1042 1043 boolean moreChars = (endPos < bufEnd); 1044 1045 final int ascent = isFallbackLineSpacing 1046 ? Math.min(fmAscent, Math.round(ascents[breakIndex])) 1047 : fmAscent; 1048 final int descent = isFallbackLineSpacing 1049 ? Math.max(fmDescent, Math.round(descents[breakIndex])) 1050 : fmDescent; 1051 1052 // The fallback ascent/descent may be larger than top/bottom of the default font 1053 // metrics. Adjust top/bottom with ascent/descent for avoiding unexpected 1054 // clipping. 1055 if (isFallbackLineSpacing) { 1056 if (ascent < fmTop) { 1057 fmTop = ascent; 1058 } 1059 if (descent > fmBottom) { 1060 fmBottom = descent; 1061 } 1062 } 1063 1064 v = out(source, here, endPos, 1065 ascent, descent, fmTop, fmBottom, 1066 v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, 1067 hasTabs[breakIndex], hyphenEdits[breakIndex], needMultiply, 1068 measuredPara, bufEnd, includepad, trackpad, addLastLineSpacing, chs, 1069 paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex], 1070 paint, moreChars); 1071 1072 if (endPos < spanEnd) { 1073 // preserve metrics for current span 1074 fmTop = Math.min(defaultTop, fm.top); 1075 fmBottom = Math.max(defaultBottom, fm.bottom); 1076 fmAscent = Math.min(defaultAscent, fm.ascent); 1077 fmDescent = Math.max(defaultDescent, fm.descent); 1078 } else { 1079 fmTop = fmBottom = fmAscent = fmDescent = 0; 1080 } 1081 1082 here = endPos; 1083 breakIndex++; 1084 1085 if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) { 1086 return; 1087 } 1088 } 1089 } 1090 1091 if (paraEnd == bufEnd) { 1092 break; 1093 } 1094 } 1095 1096 if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) 1097 && mLineCount < mMaximumVisibleLineCount) { 1098 final MeasuredParagraph measuredPara = 1099 MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null); 1100 if (defaultAscent != 0 && defaultDescent != 0) { 1101 fm.top = defaultTop; 1102 fm.ascent = defaultAscent; 1103 fm.descent = defaultDescent; 1104 fm.bottom = defaultBottom; 1105 } else { 1106 paint.getFontMetricsInt(fm); 1107 } 1108 1109 v = out(source, 1110 bufEnd, bufEnd, fm.ascent, fm.descent, 1111 fm.top, fm.bottom, 1112 v, 1113 spacingmult, spacingadd, null, 1114 null, fm, false, 0, 1115 needMultiply, measuredPara, bufEnd, 1116 includepad, trackpad, addLastLineSpacing, null, 1117 bufStart, ellipsize, 1118 ellipsizedWidth, 0, paint, false); 1119 } 1120 } 1121 1122 private int out(final CharSequence text, final int start, final int end, int above, int below, 1123 int top, int bottom, int v, final float spacingmult, final float spacingadd, 1124 final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm, 1125 final boolean hasTab, final int hyphenEdit, final boolean needMultiply, 1126 @NonNull final MeasuredParagraph measured, 1127 final int bufEnd, final boolean includePad, final boolean trackPad, 1128 final boolean addLastLineLineSpacing, final char[] chs, 1129 final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth, 1130 final float textWidth, final TextPaint paint, final boolean moreChars) { 1131 final int j = mLineCount; 1132 final int off = j * mColumns; 1133 final int want = off + mColumns + TOP; 1134 int[] lines = mLines; 1135 final int dir = measured.getParagraphDir(); 1136 1137 if (want >= lines.length) { 1138 final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want)); 1139 System.arraycopy(lines, 0, grow, 0, lines.length); 1140 mLines = grow; 1141 lines = grow; 1142 } 1143 1144 if (j >= mLineDirections.length) { 1145 final Directions[] grow = ArrayUtils.newUnpaddedArray(Directions.class, 1146 GrowingArrayUtils.growSize(j)); 1147 System.arraycopy(mLineDirections, 0, grow, 0, mLineDirections.length); 1148 mLineDirections = grow; 1149 } 1150 1151 if (chooseHt != null) { 1152 fm.ascent = above; 1153 fm.descent = below; 1154 fm.top = top; 1155 fm.bottom = bottom; 1156 1157 for (int i = 0; i < chooseHt.length; i++) { 1158 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { 1159 ((LineHeightSpan.WithDensity) chooseHt[i]) 1160 .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); 1161 } else { 1162 chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); 1163 } 1164 } 1165 1166 above = fm.ascent; 1167 below = fm.descent; 1168 top = fm.top; 1169 bottom = fm.bottom; 1170 } 1171 1172 boolean firstLine = (j == 0); 1173 boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); 1174 1175 if (ellipsize != null) { 1176 // If there is only one line, then do any type of ellipsis except when it is MARQUEE 1177 // if there are multiple lines, just allow END ellipsis on the last line 1178 boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount); 1179 1180 boolean doEllipsis = 1181 (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) && 1182 ellipsize != TextUtils.TruncateAt.MARQUEE) || 1183 (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) && 1184 ellipsize == TextUtils.TruncateAt.END); 1185 if (doEllipsis) { 1186 calculateEllipsis(start, end, measured, widthStart, 1187 ellipsisWidth, ellipsize, j, 1188 textWidth, paint, forceEllipsis); 1189 } else { 1190 mLines[mColumns * j + ELLIPSIS_START] = 0; 1191 mLines[mColumns * j + ELLIPSIS_COUNT] = 0; 1192 } 1193 } 1194 1195 final boolean lastLine; 1196 if (mEllipsized) { 1197 lastLine = true; 1198 } else { 1199 final boolean lastCharIsNewLine = widthStart != bufEnd && bufEnd > 0 1200 && text.charAt(bufEnd - 1) == CHAR_NEW_LINE; 1201 if (end == bufEnd && !lastCharIsNewLine) { 1202 lastLine = true; 1203 } else if (start == bufEnd && lastCharIsNewLine) { 1204 lastLine = true; 1205 } else { 1206 lastLine = false; 1207 } 1208 } 1209 1210 if (firstLine) { 1211 if (trackPad) { 1212 mTopPadding = top - above; 1213 } 1214 1215 if (includePad) { 1216 above = top; 1217 } 1218 } 1219 1220 int extra; 1221 1222 if (lastLine) { 1223 if (trackPad) { 1224 mBottomPadding = bottom - below; 1225 } 1226 1227 if (includePad) { 1228 below = bottom; 1229 } 1230 } 1231 1232 if (needMultiply && (addLastLineLineSpacing || !lastLine)) { 1233 double ex = (below - above) * (spacingmult - 1) + spacingadd; 1234 if (ex >= 0) { 1235 extra = (int)(ex + EXTRA_ROUNDING); 1236 } else { 1237 extra = -(int)(-ex + EXTRA_ROUNDING); 1238 } 1239 } else { 1240 extra = 0; 1241 } 1242 1243 lines[off + START] = start; 1244 lines[off + TOP] = v; 1245 lines[off + DESCENT] = below + extra; 1246 lines[off + EXTRA] = extra; 1247 1248 // special case for non-ellipsized last visible line when maxLines is set 1249 // store the height as if it was ellipsized 1250 if (!mEllipsized && currentLineIsTheLastVisibleOne) { 1251 // below calculation as if it was the last line 1252 int maxLineBelow = includePad ? bottom : below; 1253 // similar to the calculation of v below, without the extra. 1254 mMaxLineHeight = v + (maxLineBelow - above); 1255 } 1256 1257 v += (below - above) + extra; 1258 lines[off + mColumns + START] = end; 1259 lines[off + mColumns + TOP] = v; 1260 1261 // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining 1262 // one bit for start field 1263 lines[off + TAB] |= hasTab ? TAB_MASK : 0; 1264 if (mEllipsized) { 1265 if (ellipsize == TextUtils.TruncateAt.START) { 1266 lines[off + HYPHEN] = packHyphenEdit(Paint.START_HYPHEN_EDIT_NO_EDIT, 1267 unpackEndHyphenEdit(hyphenEdit)); 1268 } else if (ellipsize == TextUtils.TruncateAt.END) { 1269 lines[off + HYPHEN] = packHyphenEdit(unpackStartHyphenEdit(hyphenEdit), 1270 Paint.END_HYPHEN_EDIT_NO_EDIT); 1271 } else { // Middle and marquee ellipsize should show text at the start/end edge. 1272 lines[off + HYPHEN] = packHyphenEdit( 1273 Paint.START_HYPHEN_EDIT_NO_EDIT, Paint.END_HYPHEN_EDIT_NO_EDIT); 1274 } 1275 } else { 1276 lines[off + HYPHEN] = hyphenEdit; 1277 } 1278 1279 lines[off + DIR] |= dir << DIR_SHIFT; 1280 mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart); 1281 1282 mLineCount++; 1283 return v; 1284 } 1285 1286 private void calculateEllipsis(int lineStart, int lineEnd, 1287 MeasuredParagraph measured, int widthStart, 1288 float avail, TextUtils.TruncateAt where, 1289 int line, float textWidth, TextPaint paint, 1290 boolean forceEllipsis) { 1291 avail -= getTotalInsets(line); 1292 if (textWidth <= avail && !forceEllipsis) { 1293 // Everything fits! 1294 mLines[mColumns * line + ELLIPSIS_START] = 0; 1295 mLines[mColumns * line + ELLIPSIS_COUNT] = 0; 1296 return; 1297 } 1298 1299 float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where)); 1300 int ellipsisStart = 0; 1301 int ellipsisCount = 0; 1302 int len = lineEnd - lineStart; 1303 1304 // We only support start ellipsis on a single line 1305 if (where == TextUtils.TruncateAt.START) { 1306 if (mMaximumVisibleLineCount == 1) { 1307 float sum = 0; 1308 int i; 1309 1310 for (i = len; i > 0; i--) { 1311 float w = measured.getCharWidthAt(i - 1 + lineStart - widthStart); 1312 if (w + sum + ellipsisWidth > avail) { 1313 while (i < len 1314 && measured.getCharWidthAt(i + lineStart - widthStart) == 0.0f) { 1315 i++; 1316 } 1317 break; 1318 } 1319 1320 sum += w; 1321 } 1322 1323 ellipsisStart = 0; 1324 ellipsisCount = i; 1325 } else { 1326 if (Log.isLoggable(TAG, Log.WARN)) { 1327 Log.w(TAG, "Start Ellipsis only supported with one line"); 1328 } 1329 } 1330 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE || 1331 where == TextUtils.TruncateAt.END_SMALL) { 1332 float sum = 0; 1333 int i; 1334 1335 for (i = 0; i < len; i++) { 1336 float w = measured.getCharWidthAt(i + lineStart - widthStart); 1337 1338 if (w + sum + ellipsisWidth > avail) { 1339 break; 1340 } 1341 1342 sum += w; 1343 } 1344 1345 ellipsisStart = i; 1346 ellipsisCount = len - i; 1347 if (forceEllipsis && ellipsisCount == 0 && len > 0) { 1348 ellipsisStart = len - 1; 1349 ellipsisCount = 1; 1350 } 1351 } else { 1352 // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line 1353 if (mMaximumVisibleLineCount == 1) { 1354 float lsum = 0, rsum = 0; 1355 int left = 0, right = len; 1356 1357 float ravail = (avail - ellipsisWidth) / 2; 1358 for (right = len; right > 0; right--) { 1359 float w = measured.getCharWidthAt(right - 1 + lineStart - widthStart); 1360 1361 if (w + rsum > ravail) { 1362 while (right < len 1363 && measured.getCharWidthAt(right + lineStart - widthStart) 1364 == 0.0f) { 1365 right++; 1366 } 1367 break; 1368 } 1369 rsum += w; 1370 } 1371 1372 float lavail = avail - ellipsisWidth - rsum; 1373 for (left = 0; left < right; left++) { 1374 float w = measured.getCharWidthAt(left + lineStart - widthStart); 1375 1376 if (w + lsum > lavail) { 1377 break; 1378 } 1379 1380 lsum += w; 1381 } 1382 1383 ellipsisStart = left; 1384 ellipsisCount = right - left; 1385 } else { 1386 if (Log.isLoggable(TAG, Log.WARN)) { 1387 Log.w(TAG, "Middle Ellipsis only supported with one line"); 1388 } 1389 } 1390 } 1391 mEllipsized = true; 1392 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; 1393 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; 1394 } 1395 1396 private float getTotalInsets(int line) { 1397 int totalIndent = 0; 1398 if (mLeftIndents != null) { 1399 totalIndent = mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1400 } 1401 if (mRightIndents != null) { 1402 totalIndent += mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1403 } 1404 return totalIndent; 1405 } 1406 1407 // Override the base class so we can directly access our members, 1408 // rather than relying on member functions. 1409 // The logic mirrors that of Layout.getLineForVertical 1410 // FIXME: It may be faster to do a linear search for layouts without many lines. 1411 @Override 1412 public int getLineForVertical(int vertical) { 1413 int high = mLineCount; 1414 int low = -1; 1415 int guess; 1416 int[] lines = mLines; 1417 while (high - low > 1) { 1418 guess = (high + low) >> 1; 1419 if (lines[mColumns * guess + TOP] > vertical){ 1420 high = guess; 1421 } else { 1422 low = guess; 1423 } 1424 } 1425 if (low < 0) { 1426 return 0; 1427 } else { 1428 return low; 1429 } 1430 } 1431 1432 @Override 1433 public int getLineCount() { 1434 return mLineCount; 1435 } 1436 1437 @Override 1438 public int getLineTop(int line) { 1439 return mLines[mColumns * line + TOP]; 1440 } 1441 1442 /** 1443 * @hide 1444 */ 1445 @Override 1446 public int getLineExtra(int line) { 1447 return mLines[mColumns * line + EXTRA]; 1448 } 1449 1450 @Override 1451 public int getLineDescent(int line) { 1452 return mLines[mColumns * line + DESCENT]; 1453 } 1454 1455 @Override 1456 public int getLineStart(int line) { 1457 return mLines[mColumns * line + START] & START_MASK; 1458 } 1459 1460 @Override 1461 public int getParagraphDirection(int line) { 1462 return mLines[mColumns * line + DIR] >> DIR_SHIFT; 1463 } 1464 1465 @Override 1466 public boolean getLineContainsTab(int line) { 1467 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0; 1468 } 1469 1470 @Override 1471 public final Directions getLineDirections(int line) { 1472 if (line > getLineCount()) { 1473 throw new ArrayIndexOutOfBoundsException(); 1474 } 1475 return mLineDirections[line]; 1476 } 1477 1478 @Override 1479 public int getTopPadding() { 1480 return mTopPadding; 1481 } 1482 1483 @Override 1484 public int getBottomPadding() { 1485 return mBottomPadding; 1486 } 1487 1488 // To store into single int field, pack the pair of start and end hyphen edit. 1489 static int packHyphenEdit( 1490 @Paint.StartHyphenEdit int start, @Paint.EndHyphenEdit int end) { 1491 return start << START_HYPHEN_BITS_SHIFT | end; 1492 } 1493 1494 static int unpackStartHyphenEdit(int packedHyphenEdit) { 1495 return (packedHyphenEdit & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT; 1496 } 1497 1498 static int unpackEndHyphenEdit(int packedHyphenEdit) { 1499 return packedHyphenEdit & END_HYPHEN_MASK; 1500 } 1501 1502 /** 1503 * Returns the start hyphen edit value for this line. 1504 * 1505 * @param lineNumber a line number 1506 * @return A start hyphen edit value. 1507 * @hide 1508 */ 1509 @Override 1510 public @Paint.StartHyphenEdit int getStartHyphenEdit(int lineNumber) { 1511 return unpackStartHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK); 1512 } 1513 1514 /** 1515 * Returns the packed hyphen edit value for this line. 1516 * 1517 * @param lineNumber a line number 1518 * @return An end hyphen edit value. 1519 * @hide 1520 */ 1521 @Override 1522 public @Paint.EndHyphenEdit int getEndHyphenEdit(int lineNumber) { 1523 return unpackEndHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK); 1524 } 1525 1526 /** 1527 * @hide 1528 */ 1529 @Override 1530 public int getIndentAdjust(int line, Alignment align) { 1531 if (align == Alignment.ALIGN_LEFT) { 1532 if (mLeftIndents == null) { 1533 return 0; 1534 } else { 1535 return mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1536 } 1537 } else if (align == Alignment.ALIGN_RIGHT) { 1538 if (mRightIndents == null) { 1539 return 0; 1540 } else { 1541 return -mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1542 } 1543 } else if (align == Alignment.ALIGN_CENTER) { 1544 int left = 0; 1545 if (mLeftIndents != null) { 1546 left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1547 } 1548 int right = 0; 1549 if (mRightIndents != null) { 1550 right = mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1551 } 1552 return (left - right) >> 1; 1553 } else { 1554 throw new AssertionError("unhandled alignment " + align); 1555 } 1556 } 1557 1558 @Override 1559 public int getEllipsisCount(int line) { 1560 if (mColumns < COLUMNS_ELLIPSIZE) { 1561 return 0; 1562 } 1563 1564 return mLines[mColumns * line + ELLIPSIS_COUNT]; 1565 } 1566 1567 @Override 1568 public int getEllipsisStart(int line) { 1569 if (mColumns < COLUMNS_ELLIPSIZE) { 1570 return 0; 1571 } 1572 1573 return mLines[mColumns * line + ELLIPSIS_START]; 1574 } 1575 1576 @Override 1577 @NonNull 1578 public RectF computeDrawingBoundingBox() { 1579 // Cache the drawing bounds result because it does not change after created. 1580 if (mDrawingBounds == null) { 1581 mDrawingBounds = super.computeDrawingBoundingBox(); 1582 } 1583 return mDrawingBounds; 1584 } 1585 1586 /** 1587 * Return the total height of this layout. 1588 * 1589 * @param cap if true and max lines is set, returns the height of the layout at the max lines. 1590 * 1591 * @hide 1592 */ 1593 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 1594 public int getHeight(boolean cap) { 1595 if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1 1596 && Log.isLoggable(TAG, Log.WARN)) { 1597 Log.w(TAG, "maxLineHeight should not be -1. " 1598 + " maxLines:" + mMaximumVisibleLineCount 1599 + " lineCount:" + mLineCount); 1600 } 1601 1602 return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1 1603 ? mMaxLineHeight : super.getHeight(); 1604 } 1605 1606 @UnsupportedAppUsage 1607 private int mLineCount; 1608 private int mTopPadding, mBottomPadding; 1609 @UnsupportedAppUsage 1610 private int mColumns; 1611 private RectF mDrawingBounds = null; // lazy calculation. 1612 1613 /** 1614 * Keeps track if ellipsize is applied to the text. 1615 */ 1616 private boolean mEllipsized; 1617 1618 /** 1619 * If maxLines is set, ellipsize is not set, and the actual line count of text is greater than 1620 * or equal to maxLine, this variable holds the ideal visual height of the maxLine'th line 1621 * starting from the top of the layout. If maxLines is not set its value will be -1. 1622 * 1623 * The value is the same as getLineTop(maxLines) for ellipsized version where structurally no 1624 * more than maxLines is contained. 1625 */ 1626 private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT; 1627 1628 private static final int COLUMNS_NORMAL = 5; 1629 private static final int COLUMNS_ELLIPSIZE = 7; 1630 private static final int START = 0; 1631 private static final int DIR = START; 1632 private static final int TAB = START; 1633 private static final int TOP = 1; 1634 private static final int DESCENT = 2; 1635 private static final int EXTRA = 3; 1636 private static final int HYPHEN = 4; 1637 @UnsupportedAppUsage 1638 private static final int ELLIPSIS_START = 5; 1639 private static final int ELLIPSIS_COUNT = 6; 1640 1641 @UnsupportedAppUsage 1642 private int[] mLines; 1643 @UnsupportedAppUsage 1644 private Directions[] mLineDirections; 1645 @UnsupportedAppUsage 1646 private int mMaximumVisibleLineCount = Integer.MAX_VALUE; 1647 1648 private static final int START_MASK = 0x1FFFFFFF; 1649 private static final int DIR_SHIFT = 30; 1650 private static final int TAB_MASK = 0x20000000; 1651 private static final int HYPHEN_MASK = 0xFF; 1652 private static final int START_HYPHEN_BITS_SHIFT = 3; 1653 private static final int START_HYPHEN_MASK = 0x18; // 0b11000 1654 private static final int END_HYPHEN_MASK = 0x7; // 0b00111 1655 1656 private static final float TAB_INCREMENT = 20; // same as Layout, but that's private 1657 1658 private static final char CHAR_NEW_LINE = '\n'; 1659 1660 private static final double EXTRA_ROUNDING = 0.5; 1661 1662 private static final int DEFAULT_MAX_LINE_HEIGHT = -1; 1663 1664 // Unused, here because of gray list private API accesses. 1665 /*package*/ static class LineBreaks { 1666 private static final int INITIAL_SIZE = 16; 1667 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1668 public int[] breaks = new int[INITIAL_SIZE]; 1669 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1670 public float[] widths = new float[INITIAL_SIZE]; 1671 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1672 public float[] ascents = new float[INITIAL_SIZE]; 1673 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1674 public float[] descents = new float[INITIAL_SIZE]; 1675 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1676 public int[] flags = new int[INITIAL_SIZE]; // hasTab 1677 // breaks, widths, and flags should all have the same length 1678 } 1679 1680 @Nullable private int[] mLeftIndents; 1681 @Nullable private int[] mRightIndents; 1682 } 1683