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 } else { 981 mLines[mColumns * j + ELLIPSIS_START] = 0; 982 mLines[mColumns * j + ELLIPSIS_COUNT] = 0; 983 } 984 } 985 986 final boolean lastLine; 987 if (mEllipsized) { 988 lastLine = true; 989 } else { 990 final boolean lastCharIsNewLine = widthStart != bufEnd && bufEnd > 0 991 && text.charAt(bufEnd - 1) == CHAR_NEW_LINE; 992 if (end == bufEnd && !lastCharIsNewLine) { 993 lastLine = true; 994 } else if (start == bufEnd && lastCharIsNewLine) { 995 lastLine = true; 996 } else { 997 lastLine = false; 998 } 999 } 1000 1001 if (firstLine) { 1002 if (trackPad) { 1003 mTopPadding = top - above; 1004 } 1005 1006 if (includePad) { 1007 above = top; 1008 } 1009 } 1010 1011 int extra; 1012 1013 if (lastLine) { 1014 if (trackPad) { 1015 mBottomPadding = bottom - below; 1016 } 1017 1018 if (includePad) { 1019 below = bottom; 1020 } 1021 } 1022 1023 if (needMultiply && (addLastLineLineSpacing || !lastLine)) { 1024 double ex = (below - above) * (spacingmult - 1) + spacingadd; 1025 if (ex >= 0) { 1026 extra = (int)(ex + EXTRA_ROUNDING); 1027 } else { 1028 extra = -(int)(-ex + EXTRA_ROUNDING); 1029 } 1030 } else { 1031 extra = 0; 1032 } 1033 1034 lines[off + START] = start; 1035 lines[off + TOP] = v; 1036 lines[off + DESCENT] = below + extra; 1037 lines[off + EXTRA] = extra; 1038 1039 // special case for non-ellipsized last visible line when maxLines is set 1040 // store the height as if it was ellipsized 1041 if (!mEllipsized && currentLineIsTheLastVisibleOne) { 1042 // below calculation as if it was the last line 1043 int maxLineBelow = includePad ? bottom : below; 1044 // similar to the calculation of v below, without the extra. 1045 mMaxLineHeight = v + (maxLineBelow - above); 1046 } 1047 1048 v += (below - above) + extra; 1049 lines[off + mColumns + START] = end; 1050 lines[off + mColumns + TOP] = v; 1051 1052 // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining 1053 // one bit for start field 1054 lines[off + TAB] |= hasTab ? TAB_MASK : 0; 1055 lines[off + HYPHEN] = hyphenEdit; 1056 lines[off + DIR] |= dir << DIR_SHIFT; 1057 mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart); 1058 1059 mLineCount++; 1060 return v; 1061 } 1062 1063 private void calculateEllipsis(int lineStart, int lineEnd, 1064 MeasuredParagraph measured, int widthStart, 1065 float avail, TextUtils.TruncateAt where, 1066 int line, float textWidth, TextPaint paint, 1067 boolean forceEllipsis) { 1068 avail -= getTotalInsets(line); 1069 if (textWidth <= avail && !forceEllipsis) { 1070 // Everything fits! 1071 mLines[mColumns * line + ELLIPSIS_START] = 0; 1072 mLines[mColumns * line + ELLIPSIS_COUNT] = 0; 1073 return; 1074 } 1075 1076 float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where)); 1077 int ellipsisStart = 0; 1078 int ellipsisCount = 0; 1079 int len = lineEnd - lineStart; 1080 1081 // We only support start ellipsis on a single line 1082 if (where == TextUtils.TruncateAt.START) { 1083 if (mMaximumVisibleLineCount == 1) { 1084 float sum = 0; 1085 int i; 1086 1087 for (i = len; i > 0; i--) { 1088 float w = measured.getCharWidthAt(i - 1 + lineStart - widthStart); 1089 if (w + sum + ellipsisWidth > avail) { 1090 while (i < len 1091 && measured.getCharWidthAt(i + lineStart - widthStart) == 0.0f) { 1092 i++; 1093 } 1094 break; 1095 } 1096 1097 sum += w; 1098 } 1099 1100 ellipsisStart = 0; 1101 ellipsisCount = i; 1102 } else { 1103 if (Log.isLoggable(TAG, Log.WARN)) { 1104 Log.w(TAG, "Start Ellipsis only supported with one line"); 1105 } 1106 } 1107 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE || 1108 where == TextUtils.TruncateAt.END_SMALL) { 1109 float sum = 0; 1110 int i; 1111 1112 for (i = 0; i < len; i++) { 1113 float w = measured.getCharWidthAt(i + lineStart - widthStart); 1114 1115 if (w + sum + ellipsisWidth > avail) { 1116 break; 1117 } 1118 1119 sum += w; 1120 } 1121 1122 ellipsisStart = i; 1123 ellipsisCount = len - i; 1124 if (forceEllipsis && ellipsisCount == 0 && len > 0) { 1125 ellipsisStart = len - 1; 1126 ellipsisCount = 1; 1127 } 1128 } else { 1129 // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line 1130 if (mMaximumVisibleLineCount == 1) { 1131 float lsum = 0, rsum = 0; 1132 int left = 0, right = len; 1133 1134 float ravail = (avail - ellipsisWidth) / 2; 1135 for (right = len; right > 0; right--) { 1136 float w = measured.getCharWidthAt(right - 1 + lineStart - widthStart); 1137 1138 if (w + rsum > ravail) { 1139 while (right < len 1140 && measured.getCharWidthAt(right + lineStart - widthStart) 1141 == 0.0f) { 1142 right++; 1143 } 1144 break; 1145 } 1146 rsum += w; 1147 } 1148 1149 float lavail = avail - ellipsisWidth - rsum; 1150 for (left = 0; left < right; left++) { 1151 float w = measured.getCharWidthAt(left + lineStart - widthStart); 1152 1153 if (w + lsum > lavail) { 1154 break; 1155 } 1156 1157 lsum += w; 1158 } 1159 1160 ellipsisStart = left; 1161 ellipsisCount = right - left; 1162 } else { 1163 if (Log.isLoggable(TAG, Log.WARN)) { 1164 Log.w(TAG, "Middle Ellipsis only supported with one line"); 1165 } 1166 } 1167 } 1168 mEllipsized = true; 1169 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; 1170 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; 1171 } 1172 1173 private float getTotalInsets(int line) { 1174 int totalIndent = 0; 1175 if (mLeftIndents != null) { 1176 totalIndent = mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1177 } 1178 if (mRightIndents != null) { 1179 totalIndent += mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1180 } 1181 return totalIndent; 1182 } 1183 1184 // Override the base class so we can directly access our members, 1185 // rather than relying on member functions. 1186 // The logic mirrors that of Layout.getLineForVertical 1187 // FIXME: It may be faster to do a linear search for layouts without many lines. 1188 @Override 1189 public int getLineForVertical(int vertical) { 1190 int high = mLineCount; 1191 int low = -1; 1192 int guess; 1193 int[] lines = mLines; 1194 while (high - low > 1) { 1195 guess = (high + low) >> 1; 1196 if (lines[mColumns * guess + TOP] > vertical){ 1197 high = guess; 1198 } else { 1199 low = guess; 1200 } 1201 } 1202 if (low < 0) { 1203 return 0; 1204 } else { 1205 return low; 1206 } 1207 } 1208 1209 @Override 1210 public int getLineCount() { 1211 return mLineCount; 1212 } 1213 1214 @Override 1215 public int getLineTop(int line) { 1216 return mLines[mColumns * line + TOP]; 1217 } 1218 1219 /** 1220 * @hide 1221 */ 1222 @Override 1223 public int getLineExtra(int line) { 1224 return mLines[mColumns * line + EXTRA]; 1225 } 1226 1227 @Override 1228 public int getLineDescent(int line) { 1229 return mLines[mColumns * line + DESCENT]; 1230 } 1231 1232 @Override 1233 public int getLineStart(int line) { 1234 return mLines[mColumns * line + START] & START_MASK; 1235 } 1236 1237 @Override 1238 public int getParagraphDirection(int line) { 1239 return mLines[mColumns * line + DIR] >> DIR_SHIFT; 1240 } 1241 1242 @Override 1243 public boolean getLineContainsTab(int line) { 1244 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0; 1245 } 1246 1247 @Override 1248 public final Directions getLineDirections(int line) { 1249 if (line > getLineCount()) { 1250 throw new ArrayIndexOutOfBoundsException(); 1251 } 1252 return mLineDirections[line]; 1253 } 1254 1255 @Override 1256 public int getTopPadding() { 1257 return mTopPadding; 1258 } 1259 1260 @Override 1261 public int getBottomPadding() { 1262 return mBottomPadding; 1263 } 1264 1265 // To store into single int field, pack the pair of start and end hyphen edit. 1266 static int packHyphenEdit( 1267 @Paint.StartHyphenEdit int start, @Paint.EndHyphenEdit int end) { 1268 return start << START_HYPHEN_BITS_SHIFT | end; 1269 } 1270 1271 static int unpackStartHyphenEdit(int packedHyphenEdit) { 1272 return (packedHyphenEdit & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT; 1273 } 1274 1275 static int unpackEndHyphenEdit(int packedHyphenEdit) { 1276 return packedHyphenEdit & END_HYPHEN_MASK; 1277 } 1278 1279 /** 1280 * Returns the start hyphen edit value for this line. 1281 * 1282 * @param lineNumber a line number 1283 * @return A start hyphen edit value. 1284 * @hide 1285 */ 1286 @Override 1287 public @Paint.StartHyphenEdit int getStartHyphenEdit(int lineNumber) { 1288 return unpackStartHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK); 1289 } 1290 1291 /** 1292 * Returns the packed hyphen edit value for this line. 1293 * 1294 * @param lineNumber a line number 1295 * @return An end hyphen edit value. 1296 * @hide 1297 */ 1298 @Override 1299 public @Paint.EndHyphenEdit int getEndHyphenEdit(int lineNumber) { 1300 return unpackEndHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK); 1301 } 1302 1303 /** 1304 * @hide 1305 */ 1306 @Override 1307 public int getIndentAdjust(int line, Alignment align) { 1308 if (align == Alignment.ALIGN_LEFT) { 1309 if (mLeftIndents == null) { 1310 return 0; 1311 } else { 1312 return mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1313 } 1314 } else if (align == Alignment.ALIGN_RIGHT) { 1315 if (mRightIndents == null) { 1316 return 0; 1317 } else { 1318 return -mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1319 } 1320 } else if (align == Alignment.ALIGN_CENTER) { 1321 int left = 0; 1322 if (mLeftIndents != null) { 1323 left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1324 } 1325 int right = 0; 1326 if (mRightIndents != null) { 1327 right = mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1328 } 1329 return (left - right) >> 1; 1330 } else { 1331 throw new AssertionError("unhandled alignment " + align); 1332 } 1333 } 1334 1335 @Override 1336 public int getEllipsisCount(int line) { 1337 if (mColumns < COLUMNS_ELLIPSIZE) { 1338 return 0; 1339 } 1340 1341 return mLines[mColumns * line + ELLIPSIS_COUNT]; 1342 } 1343 1344 @Override 1345 public int getEllipsisStart(int line) { 1346 if (mColumns < COLUMNS_ELLIPSIZE) { 1347 return 0; 1348 } 1349 1350 return mLines[mColumns * line + ELLIPSIS_START]; 1351 } 1352 1353 @Override 1354 public int getEllipsizedWidth() { 1355 return mEllipsizedWidth; 1356 } 1357 1358 /** 1359 * Return the total height of this layout. 1360 * 1361 * @param cap if true and max lines is set, returns the height of the layout at the max lines. 1362 * 1363 * @hide 1364 */ 1365 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 1366 public int getHeight(boolean cap) { 1367 if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1 1368 && Log.isLoggable(TAG, Log.WARN)) { 1369 Log.w(TAG, "maxLineHeight should not be -1. " 1370 + " maxLines:" + mMaximumVisibleLineCount 1371 + " lineCount:" + mLineCount); 1372 } 1373 1374 return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1 1375 ? mMaxLineHeight : super.getHeight(); 1376 } 1377 1378 @UnsupportedAppUsage 1379 private int mLineCount; 1380 private int mTopPadding, mBottomPadding; 1381 @UnsupportedAppUsage 1382 private int mColumns; 1383 private int mEllipsizedWidth; 1384 1385 /** 1386 * Keeps track if ellipsize is applied to the text. 1387 */ 1388 private boolean mEllipsized; 1389 1390 /** 1391 * If maxLines is set, ellipsize is not set, and the actual line count of text is greater than 1392 * or equal to maxLine, this variable holds the ideal visual height of the maxLine'th line 1393 * starting from the top of the layout. If maxLines is not set its value will be -1. 1394 * 1395 * The value is the same as getLineTop(maxLines) for ellipsized version where structurally no 1396 * more than maxLines is contained. 1397 */ 1398 private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT; 1399 1400 private static final int COLUMNS_NORMAL = 5; 1401 private static final int COLUMNS_ELLIPSIZE = 7; 1402 private static final int START = 0; 1403 private static final int DIR = START; 1404 private static final int TAB = START; 1405 private static final int TOP = 1; 1406 private static final int DESCENT = 2; 1407 private static final int EXTRA = 3; 1408 private static final int HYPHEN = 4; 1409 @UnsupportedAppUsage 1410 private static final int ELLIPSIS_START = 5; 1411 private static final int ELLIPSIS_COUNT = 6; 1412 1413 @UnsupportedAppUsage 1414 private int[] mLines; 1415 @UnsupportedAppUsage 1416 private Directions[] mLineDirections; 1417 @UnsupportedAppUsage 1418 private int mMaximumVisibleLineCount = Integer.MAX_VALUE; 1419 1420 private static final int START_MASK = 0x1FFFFFFF; 1421 private static final int DIR_SHIFT = 30; 1422 private static final int TAB_MASK = 0x20000000; 1423 private static final int HYPHEN_MASK = 0xFF; 1424 private static final int START_HYPHEN_BITS_SHIFT = 3; 1425 private static final int START_HYPHEN_MASK = 0x18; // 0b11000 1426 private static final int END_HYPHEN_MASK = 0x7; // 0b00111 1427 1428 private static final float TAB_INCREMENT = 20; // same as Layout, but that's private 1429 1430 private static final char CHAR_NEW_LINE = '\n'; 1431 1432 private static final double EXTRA_ROUNDING = 0.5; 1433 1434 private static final int DEFAULT_MAX_LINE_HEIGHT = -1; 1435 1436 // Unused, here because of gray list private API accesses. 1437 /*package*/ static class LineBreaks { 1438 private static final int INITIAL_SIZE = 16; 1439 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1440 public int[] breaks = new int[INITIAL_SIZE]; 1441 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1442 public float[] widths = new float[INITIAL_SIZE]; 1443 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1444 public float[] ascents = new float[INITIAL_SIZE]; 1445 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1446 public float[] descents = new float[INITIAL_SIZE]; 1447 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1448 public int[] flags = new int[INITIAL_SIZE]; // hasTab 1449 // breaks, widths, and flags should all have the same length 1450 } 1451 1452 @Nullable private int[] mLeftIndents; 1453 @Nullable private int[] mRightIndents; 1454 } 1455