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 if (mEllipsized) { 1102 if (ellipsize == TextUtils.TruncateAt.START) { 1103 lines[off + HYPHEN] = packHyphenEdit(Paint.START_HYPHEN_EDIT_NO_EDIT, 1104 unpackEndHyphenEdit(hyphenEdit)); 1105 } else if (ellipsize == TextUtils.TruncateAt.END) { 1106 lines[off + HYPHEN] = packHyphenEdit(unpackStartHyphenEdit(hyphenEdit), 1107 Paint.END_HYPHEN_EDIT_NO_EDIT); 1108 } else { // Middle and marquee ellipsize should show text at the start/end edge. 1109 lines[off + HYPHEN] = packHyphenEdit( 1110 Paint.START_HYPHEN_EDIT_NO_EDIT, Paint.END_HYPHEN_EDIT_NO_EDIT); 1111 } 1112 } else { 1113 lines[off + HYPHEN] = hyphenEdit; 1114 } 1115 1116 lines[off + DIR] |= dir << DIR_SHIFT; 1117 mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart); 1118 1119 mLineCount++; 1120 return v; 1121 } 1122 1123 private void calculateEllipsis(int lineStart, int lineEnd, 1124 MeasuredParagraph measured, int widthStart, 1125 float avail, TextUtils.TruncateAt where, 1126 int line, float textWidth, TextPaint paint, 1127 boolean forceEllipsis) { 1128 avail -= getTotalInsets(line); 1129 if (textWidth <= avail && !forceEllipsis) { 1130 // Everything fits! 1131 mLines[mColumns * line + ELLIPSIS_START] = 0; 1132 mLines[mColumns * line + ELLIPSIS_COUNT] = 0; 1133 return; 1134 } 1135 1136 float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where)); 1137 int ellipsisStart = 0; 1138 int ellipsisCount = 0; 1139 int len = lineEnd - lineStart; 1140 1141 // We only support start ellipsis on a single line 1142 if (where == TextUtils.TruncateAt.START) { 1143 if (mMaximumVisibleLineCount == 1) { 1144 float sum = 0; 1145 int i; 1146 1147 for (i = len; i > 0; i--) { 1148 float w = measured.getCharWidthAt(i - 1 + lineStart - widthStart); 1149 if (w + sum + ellipsisWidth > avail) { 1150 while (i < len 1151 && measured.getCharWidthAt(i + lineStart - widthStart) == 0.0f) { 1152 i++; 1153 } 1154 break; 1155 } 1156 1157 sum += w; 1158 } 1159 1160 ellipsisStart = 0; 1161 ellipsisCount = i; 1162 } else { 1163 if (Log.isLoggable(TAG, Log.WARN)) { 1164 Log.w(TAG, "Start Ellipsis only supported with one line"); 1165 } 1166 } 1167 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE || 1168 where == TextUtils.TruncateAt.END_SMALL) { 1169 float sum = 0; 1170 int i; 1171 1172 for (i = 0; i < len; i++) { 1173 float w = measured.getCharWidthAt(i + lineStart - widthStart); 1174 1175 if (w + sum + ellipsisWidth > avail) { 1176 break; 1177 } 1178 1179 sum += w; 1180 } 1181 1182 ellipsisStart = i; 1183 ellipsisCount = len - i; 1184 if (forceEllipsis && ellipsisCount == 0 && len > 0) { 1185 ellipsisStart = len - 1; 1186 ellipsisCount = 1; 1187 } 1188 } else { 1189 // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line 1190 if (mMaximumVisibleLineCount == 1) { 1191 float lsum = 0, rsum = 0; 1192 int left = 0, right = len; 1193 1194 float ravail = (avail - ellipsisWidth) / 2; 1195 for (right = len; right > 0; right--) { 1196 float w = measured.getCharWidthAt(right - 1 + lineStart - widthStart); 1197 1198 if (w + rsum > ravail) { 1199 while (right < len 1200 && measured.getCharWidthAt(right + lineStart - widthStart) 1201 == 0.0f) { 1202 right++; 1203 } 1204 break; 1205 } 1206 rsum += w; 1207 } 1208 1209 float lavail = avail - ellipsisWidth - rsum; 1210 for (left = 0; left < right; left++) { 1211 float w = measured.getCharWidthAt(left + lineStart - widthStart); 1212 1213 if (w + lsum > lavail) { 1214 break; 1215 } 1216 1217 lsum += w; 1218 } 1219 1220 ellipsisStart = left; 1221 ellipsisCount = right - left; 1222 } else { 1223 if (Log.isLoggable(TAG, Log.WARN)) { 1224 Log.w(TAG, "Middle Ellipsis only supported with one line"); 1225 } 1226 } 1227 } 1228 mEllipsized = true; 1229 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; 1230 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; 1231 } 1232 1233 private float getTotalInsets(int line) { 1234 int totalIndent = 0; 1235 if (mLeftIndents != null) { 1236 totalIndent = mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1237 } 1238 if (mRightIndents != null) { 1239 totalIndent += mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1240 } 1241 return totalIndent; 1242 } 1243 1244 // Override the base class so we can directly access our members, 1245 // rather than relying on member functions. 1246 // The logic mirrors that of Layout.getLineForVertical 1247 // FIXME: It may be faster to do a linear search for layouts without many lines. 1248 @Override 1249 public int getLineForVertical(int vertical) { 1250 int high = mLineCount; 1251 int low = -1; 1252 int guess; 1253 int[] lines = mLines; 1254 while (high - low > 1) { 1255 guess = (high + low) >> 1; 1256 if (lines[mColumns * guess + TOP] > vertical){ 1257 high = guess; 1258 } else { 1259 low = guess; 1260 } 1261 } 1262 if (low < 0) { 1263 return 0; 1264 } else { 1265 return low; 1266 } 1267 } 1268 1269 @Override 1270 public int getLineCount() { 1271 return mLineCount; 1272 } 1273 1274 @Override 1275 public int getLineTop(int line) { 1276 return mLines[mColumns * line + TOP]; 1277 } 1278 1279 /** 1280 * @hide 1281 */ 1282 @Override 1283 public int getLineExtra(int line) { 1284 return mLines[mColumns * line + EXTRA]; 1285 } 1286 1287 @Override 1288 public int getLineDescent(int line) { 1289 return mLines[mColumns * line + DESCENT]; 1290 } 1291 1292 @Override 1293 public int getLineStart(int line) { 1294 return mLines[mColumns * line + START] & START_MASK; 1295 } 1296 1297 @Override 1298 public int getParagraphDirection(int line) { 1299 return mLines[mColumns * line + DIR] >> DIR_SHIFT; 1300 } 1301 1302 @Override 1303 public boolean getLineContainsTab(int line) { 1304 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0; 1305 } 1306 1307 @Override 1308 public final Directions getLineDirections(int line) { 1309 if (line > getLineCount()) { 1310 throw new ArrayIndexOutOfBoundsException(); 1311 } 1312 return mLineDirections[line]; 1313 } 1314 1315 @Override 1316 public int getTopPadding() { 1317 return mTopPadding; 1318 } 1319 1320 @Override 1321 public int getBottomPadding() { 1322 return mBottomPadding; 1323 } 1324 1325 // To store into single int field, pack the pair of start and end hyphen edit. 1326 static int packHyphenEdit( 1327 @Paint.StartHyphenEdit int start, @Paint.EndHyphenEdit int end) { 1328 return start << START_HYPHEN_BITS_SHIFT | end; 1329 } 1330 1331 static int unpackStartHyphenEdit(int packedHyphenEdit) { 1332 return (packedHyphenEdit & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT; 1333 } 1334 1335 static int unpackEndHyphenEdit(int packedHyphenEdit) { 1336 return packedHyphenEdit & END_HYPHEN_MASK; 1337 } 1338 1339 /** 1340 * Returns the start hyphen edit value for this line. 1341 * 1342 * @param lineNumber a line number 1343 * @return A start hyphen edit value. 1344 * @hide 1345 */ 1346 @Override 1347 public @Paint.StartHyphenEdit int getStartHyphenEdit(int lineNumber) { 1348 return unpackStartHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK); 1349 } 1350 1351 /** 1352 * Returns the packed hyphen edit value for this line. 1353 * 1354 * @param lineNumber a line number 1355 * @return An end hyphen edit value. 1356 * @hide 1357 */ 1358 @Override 1359 public @Paint.EndHyphenEdit int getEndHyphenEdit(int lineNumber) { 1360 return unpackEndHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK); 1361 } 1362 1363 /** 1364 * @hide 1365 */ 1366 @Override 1367 public int getIndentAdjust(int line, Alignment align) { 1368 if (align == Alignment.ALIGN_LEFT) { 1369 if (mLeftIndents == null) { 1370 return 0; 1371 } else { 1372 return mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1373 } 1374 } else if (align == Alignment.ALIGN_RIGHT) { 1375 if (mRightIndents == null) { 1376 return 0; 1377 } else { 1378 return -mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1379 } 1380 } else if (align == Alignment.ALIGN_CENTER) { 1381 int left = 0; 1382 if (mLeftIndents != null) { 1383 left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1384 } 1385 int right = 0; 1386 if (mRightIndents != null) { 1387 right = mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1388 } 1389 return (left - right) >> 1; 1390 } else { 1391 throw new AssertionError("unhandled alignment " + align); 1392 } 1393 } 1394 1395 @Override 1396 public int getEllipsisCount(int line) { 1397 if (mColumns < COLUMNS_ELLIPSIZE) { 1398 return 0; 1399 } 1400 1401 return mLines[mColumns * line + ELLIPSIS_COUNT]; 1402 } 1403 1404 @Override 1405 public int getEllipsisStart(int line) { 1406 if (mColumns < COLUMNS_ELLIPSIZE) { 1407 return 0; 1408 } 1409 1410 return mLines[mColumns * line + ELLIPSIS_START]; 1411 } 1412 1413 @Override 1414 public int getEllipsizedWidth() { 1415 return mEllipsizedWidth; 1416 } 1417 1418 @Override 1419 public boolean isFallbackLineSpacingEnabled() { 1420 return mFallbackLineSpacing; 1421 } 1422 1423 /** 1424 * Return the total height of this layout. 1425 * 1426 * @param cap if true and max lines is set, returns the height of the layout at the max lines. 1427 * 1428 * @hide 1429 */ 1430 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 1431 public int getHeight(boolean cap) { 1432 if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1 1433 && Log.isLoggable(TAG, Log.WARN)) { 1434 Log.w(TAG, "maxLineHeight should not be -1. " 1435 + " maxLines:" + mMaximumVisibleLineCount 1436 + " lineCount:" + mLineCount); 1437 } 1438 1439 return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1 1440 ? mMaxLineHeight : super.getHeight(); 1441 } 1442 1443 @UnsupportedAppUsage 1444 private int mLineCount; 1445 private int mTopPadding, mBottomPadding; 1446 @UnsupportedAppUsage 1447 private int mColumns; 1448 private int mEllipsizedWidth; 1449 private boolean mFallbackLineSpacing; 1450 1451 /** 1452 * Keeps track if ellipsize is applied to the text. 1453 */ 1454 private boolean mEllipsized; 1455 1456 /** 1457 * If maxLines is set, ellipsize is not set, and the actual line count of text is greater than 1458 * or equal to maxLine, this variable holds the ideal visual height of the maxLine'th line 1459 * starting from the top of the layout. If maxLines is not set its value will be -1. 1460 * 1461 * The value is the same as getLineTop(maxLines) for ellipsized version where structurally no 1462 * more than maxLines is contained. 1463 */ 1464 private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT; 1465 1466 private static final int COLUMNS_NORMAL = 5; 1467 private static final int COLUMNS_ELLIPSIZE = 7; 1468 private static final int START = 0; 1469 private static final int DIR = START; 1470 private static final int TAB = START; 1471 private static final int TOP = 1; 1472 private static final int DESCENT = 2; 1473 private static final int EXTRA = 3; 1474 private static final int HYPHEN = 4; 1475 @UnsupportedAppUsage 1476 private static final int ELLIPSIS_START = 5; 1477 private static final int ELLIPSIS_COUNT = 6; 1478 1479 @UnsupportedAppUsage 1480 private int[] mLines; 1481 @UnsupportedAppUsage 1482 private Directions[] mLineDirections; 1483 @UnsupportedAppUsage 1484 private int mMaximumVisibleLineCount = Integer.MAX_VALUE; 1485 1486 private static final int START_MASK = 0x1FFFFFFF; 1487 private static final int DIR_SHIFT = 30; 1488 private static final int TAB_MASK = 0x20000000; 1489 private static final int HYPHEN_MASK = 0xFF; 1490 private static final int START_HYPHEN_BITS_SHIFT = 3; 1491 private static final int START_HYPHEN_MASK = 0x18; // 0b11000 1492 private static final int END_HYPHEN_MASK = 0x7; // 0b00111 1493 1494 private static final float TAB_INCREMENT = 20; // same as Layout, but that's private 1495 1496 private static final char CHAR_NEW_LINE = '\n'; 1497 1498 private static final double EXTRA_ROUNDING = 0.5; 1499 1500 private static final int DEFAULT_MAX_LINE_HEIGHT = -1; 1501 1502 // Unused, here because of gray list private API accesses. 1503 /*package*/ static class LineBreaks { 1504 private static final int INITIAL_SIZE = 16; 1505 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1506 public int[] breaks = new int[INITIAL_SIZE]; 1507 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1508 public float[] widths = new float[INITIAL_SIZE]; 1509 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1510 public float[] ascents = new float[INITIAL_SIZE]; 1511 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1512 public float[] descents = new float[INITIAL_SIZE]; 1513 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1514 public int[] flags = new int[INITIAL_SIZE]; // hasTab 1515 // breaks, widths, and flags should all have the same length 1516 } 1517 1518 @Nullable private int[] mLeftIndents; 1519 @Nullable private int[] mRightIndents; 1520 } 1521