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