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