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