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.Nullable; 20 import android.graphics.Paint; 21 import android.text.style.LeadingMarginSpan; 22 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; 23 import android.text.style.LineHeightSpan; 24 import android.text.style.MetricAffectingSpan; 25 import android.text.style.TabStopSpan; 26 import android.util.Log; 27 import android.util.Pools.SynchronizedPool; 28 29 import com.android.internal.util.ArrayUtils; 30 import com.android.internal.util.GrowingArrayUtils; 31 32 import java.util.Arrays; 33 import java.util.Locale; 34 35 /** 36 * StaticLayout is a Layout for text that will not be edited after it 37 * is laid out. Use {@link DynamicLayout} for text that may change. 38 * <p>This is used by widgets to control text layout. You should not need 39 * to use this class directly unless you are implementing your own widget 40 * or custom display object, or would be tempted to call 41 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, 42 * float, float, android.graphics.Paint) 43 * Canvas.drawText()} directly.</p> 44 */ 45 public class StaticLayout extends Layout { 46 47 static final String TAG = "StaticLayout"; 48 49 /** 50 * Builder for static layouts. The builder is a newer pattern for constructing 51 * StaticLayout objects and should be preferred over the constructors, 52 * particularly to access newer features. To build a static layout, first 53 * call {@link #obtain} with the required arguments (text, paint, and width), 54 * then call setters for optional parameters, and finally {@link #build} 55 * to build the StaticLayout object. Parameters not explicitly set will get 56 * default values. 57 */ 58 public final static class Builder { Builder()59 private Builder() { 60 mNativePtr = nNewBuilder(); 61 } 62 63 /** 64 * Obtain a builder for constructing StaticLayout objects 65 * 66 * @param source The text to be laid out, optionally with spans 67 * @param start The index of the start of the text 68 * @param end The index + 1 of the end of the text 69 * @param paint The base paint used for layout 70 * @param width The width in pixels 71 * @return a builder object used for constructing the StaticLayout 72 */ obtain(CharSequence source, int start, int end, TextPaint paint, int width)73 public static Builder obtain(CharSequence source, int start, int end, TextPaint paint, 74 int width) { 75 Builder b = sPool.acquire(); 76 if (b == null) { 77 b = new Builder(); 78 } 79 80 // set default initial values 81 b.mText = source; 82 b.mStart = start; 83 b.mEnd = end; 84 b.mPaint = paint; 85 b.mWidth = width; 86 b.mAlignment = Alignment.ALIGN_NORMAL; 87 b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; 88 b.mSpacingMult = 1.0f; 89 b.mSpacingAdd = 0.0f; 90 b.mIncludePad = true; 91 b.mEllipsizedWidth = width; 92 b.mEllipsize = null; 93 b.mMaxLines = Integer.MAX_VALUE; 94 b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE; 95 b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; 96 97 b.mMeasuredText = MeasuredText.obtain(); 98 return b; 99 } 100 recycle(Builder b)101 private static void recycle(Builder b) { 102 b.mPaint = null; 103 b.mText = null; 104 MeasuredText.recycle(b.mMeasuredText); 105 b.mMeasuredText = null; 106 b.mLeftIndents = null; 107 b.mRightIndents = null; 108 nFinishBuilder(b.mNativePtr); 109 sPool.release(b); 110 } 111 112 // release any expensive state finish()113 /* package */ void finish() { 114 nFinishBuilder(mNativePtr); 115 mText = null; 116 mPaint = null; 117 mLeftIndents = null; 118 mRightIndents = null; 119 mMeasuredText.finish(); 120 } 121 setText(CharSequence source)122 public Builder setText(CharSequence source) { 123 return setText(source, 0, source.length()); 124 } 125 126 /** 127 * Set the text. Only useful when re-using the builder, which is done for 128 * the internal implementation of {@link DynamicLayout} but not as part 129 * of normal {@link StaticLayout} usage. 130 * 131 * @param source The text to be laid out, optionally with spans 132 * @param start The index of the start of the text 133 * @param end The index + 1 of the end of the text 134 * @return this builder, useful for chaining 135 * 136 * @hide 137 */ setText(CharSequence source, int start, int end)138 public Builder setText(CharSequence source, int start, int end) { 139 mText = source; 140 mStart = start; 141 mEnd = end; 142 return this; 143 } 144 145 /** 146 * Set the paint. Internal for reuse cases only. 147 * 148 * @param paint The base paint used for layout 149 * @return this builder, useful for chaining 150 * 151 * @hide 152 */ setPaint(TextPaint paint)153 public Builder setPaint(TextPaint paint) { 154 mPaint = paint; 155 return this; 156 } 157 158 /** 159 * Set the width. Internal for reuse cases only. 160 * 161 * @param width The width in pixels 162 * @return this builder, useful for chaining 163 * 164 * @hide 165 */ setWidth(int width)166 public Builder setWidth(int width) { 167 mWidth = width; 168 if (mEllipsize == null) { 169 mEllipsizedWidth = width; 170 } 171 return this; 172 } 173 174 /** 175 * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}. 176 * 177 * @param alignment Alignment for the resulting {@link StaticLayout} 178 * @return this builder, useful for chaining 179 */ setAlignment(Alignment alignment)180 public Builder setAlignment(Alignment alignment) { 181 mAlignment = alignment; 182 return this; 183 } 184 185 /** 186 * Set the text direction heuristic. The text direction heuristic is used to 187 * resolve text direction based per-paragraph based on the input text. The default is 188 * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. 189 * 190 * @param textDir text direction heuristic for resolving BiDi behavior. 191 * @return this builder, useful for chaining 192 */ setTextDirection(TextDirectionHeuristic textDir)193 public Builder setTextDirection(TextDirectionHeuristic textDir) { 194 mTextDir = textDir; 195 return this; 196 } 197 198 /** 199 * Set line spacing parameters. The default is 0.0 for {@code spacingAdd} 200 * and 1.0 for {@code spacingMult}. 201 * 202 * @param spacingAdd line spacing add 203 * @param spacingMult line spacing multiplier 204 * @return this builder, useful for chaining 205 * @see android.widget.TextView#setLineSpacing 206 */ setLineSpacing(float spacingAdd, float spacingMult)207 public Builder setLineSpacing(float spacingAdd, float spacingMult) { 208 mSpacingAdd = spacingAdd; 209 mSpacingMult = spacingMult; 210 return this; 211 } 212 213 /** 214 * Set whether to include extra space beyond font ascent and descent (which is 215 * needed to avoid clipping in some languages, such as Arabic and Kannada). The 216 * default is {@code true}. 217 * 218 * @param includePad whether to include padding 219 * @return this builder, useful for chaining 220 * @see android.widget.TextView#setIncludeFontPadding 221 */ setIncludePad(boolean includePad)222 public Builder setIncludePad(boolean includePad) { 223 mIncludePad = includePad; 224 return this; 225 } 226 227 /** 228 * Set the width as used for ellipsizing purposes, if it differs from the 229 * normal layout width. The default is the {@code width} 230 * passed to {@link #obtain}. 231 * 232 * @param ellipsizedWidth width used for ellipsizing, in pixels 233 * @return this builder, useful for chaining 234 * @see android.widget.TextView#setEllipsize 235 */ setEllipsizedWidth(int ellipsizedWidth)236 public Builder setEllipsizedWidth(int ellipsizedWidth) { 237 mEllipsizedWidth = ellipsizedWidth; 238 return this; 239 } 240 241 /** 242 * Set ellipsizing on the layout. Causes words that are longer than the view 243 * is wide, or exceeding the number of lines (see #setMaxLines) in the case 244 * of {@link android.text.TextUtils.TruncateAt#END} or 245 * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead 246 * of broken. The default is 247 * {@code null}, indicating no ellipsis is to be applied. 248 * 249 * @param ellipsize type of ellipsis behavior 250 * @return this builder, useful for chaining 251 * @see android.widget.TextView#setEllipsize 252 */ setEllipsize(@ullable TextUtils.TruncateAt ellipsize)253 public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) { 254 mEllipsize = ellipsize; 255 return this; 256 } 257 258 /** 259 * Set maximum number of lines. This is particularly useful in the case of 260 * ellipsizing, where it changes the layout of the last line. The default is 261 * unlimited. 262 * 263 * @param maxLines maximum number of lines in the layout 264 * @return this builder, useful for chaining 265 * @see android.widget.TextView#setMaxLines 266 */ setMaxLines(int maxLines)267 public Builder setMaxLines(int maxLines) { 268 mMaxLines = maxLines; 269 return this; 270 } 271 272 /** 273 * Set break strategy, useful for selecting high quality or balanced paragraph 274 * layout options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}. 275 * 276 * @param breakStrategy break strategy for paragraph layout 277 * @return this builder, useful for chaining 278 * @see android.widget.TextView#setBreakStrategy 279 */ setBreakStrategy(@reakStrategy int breakStrategy)280 public Builder setBreakStrategy(@BreakStrategy int breakStrategy) { 281 mBreakStrategy = breakStrategy; 282 return this; 283 } 284 285 /** 286 * Set hyphenation frequency, to control the amount of automatic hyphenation used. The 287 * default is {@link Layout#HYPHENATION_FREQUENCY_NONE}. 288 * 289 * @param hyphenationFrequency hyphenation frequency for the paragraph 290 * @return this builder, useful for chaining 291 * @see android.widget.TextView#setHyphenationFrequency 292 */ setHyphenationFrequency(@yphenationFrequency int hyphenationFrequency)293 public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) { 294 mHyphenationFrequency = hyphenationFrequency; 295 return this; 296 } 297 298 /** 299 * Set indents. Arguments are arrays holding an indent amount, one per line, measured in 300 * pixels. For lines past the last element in the array, the last element repeats. 301 * 302 * @param leftIndents array of indent values for left margin, in pixels 303 * @param rightIndents array of indent values for right margin, in pixels 304 * @return this builder, useful for chaining 305 */ setIndents(int[] leftIndents, int[] rightIndents)306 public Builder setIndents(int[] leftIndents, int[] rightIndents) { 307 mLeftIndents = leftIndents; 308 mRightIndents = rightIndents; 309 int leftLen = leftIndents == null ? 0 : leftIndents.length; 310 int rightLen = rightIndents == null ? 0 : rightIndents.length; 311 int[] indents = new int[Math.max(leftLen, rightLen)]; 312 for (int i = 0; i < indents.length; i++) { 313 int leftMargin = i < leftLen ? leftIndents[i] : 0; 314 int rightMargin = i < rightLen ? rightIndents[i] : 0; 315 indents[i] = leftMargin + rightMargin; 316 } 317 nSetIndents(mNativePtr, indents); 318 return this; 319 } 320 321 /** 322 * Measurement and break iteration is done in native code. The protocol for using 323 * the native code is as follows. 324 * 325 * For each paragraph, do a nSetupParagraph, which sets paragraph text, line width, tab 326 * stops, break strategy, and hyphenation frequency (and possibly other parameters in the 327 * future). 328 * 329 * Then, for each run within the paragraph: 330 * - setLocale (this must be done at least for the first run, optional afterwards) 331 * - one of the following, depending on the type of run: 332 * + addStyleRun (a text run, to be measured in native code) 333 * + addMeasuredRun (a run already measured in Java, passed into native code) 334 * + addReplacementRun (a replacement run, width is given) 335 * 336 * After measurement, nGetWidths() is valid if the widths are needed (eg for ellipsis). 337 * Run nComputeLineBreaks() to obtain line breaks for the paragraph. 338 * 339 * After all paragraphs, call finish() to release expensive buffers. 340 */ 341 342 private void setLocale(Locale locale) { 343 if (!locale.equals(mLocale)) { 344 nSetLocale(mNativePtr, locale.toLanguageTag(), Hyphenator.get(locale)); 345 mLocale = locale; 346 } 347 } 348 349 /* package */ float addStyleRun(TextPaint paint, int start, int end, boolean isRtl) { 350 return nAddStyleRun(mNativePtr, paint.getNativeInstance(), paint.mNativeTypeface, 351 start, end, isRtl); 352 } 353 354 /* package */ void addMeasuredRun(int start, int end, float[] widths) { 355 nAddMeasuredRun(mNativePtr, start, end, widths); 356 } 357 358 /* package */ void addReplacementRun(int start, int end, float width) { 359 nAddReplacementRun(mNativePtr, start, end, width); 360 } 361 362 /** 363 * Build the {@link StaticLayout} after options have been set. 364 * 365 * <p>Note: the builder object must not be reused in any way after calling this 366 * method. Setting parameters after calling this method, or calling it a second 367 * time on the same builder object, will likely lead to unexpected results. 368 * 369 * @return the newly constructed {@link StaticLayout} object 370 */ 371 public StaticLayout build() { 372 StaticLayout result = new StaticLayout(this); 373 Builder.recycle(this); 374 return result; 375 } 376 377 @Override 378 protected void finalize() throws Throwable { 379 try { 380 nFreeBuilder(mNativePtr); 381 } finally { 382 super.finalize(); 383 } 384 } 385 386 /* package */ long mNativePtr; 387 388 CharSequence mText; 389 int mStart; 390 int mEnd; 391 TextPaint mPaint; 392 int mWidth; 393 Alignment mAlignment; 394 TextDirectionHeuristic mTextDir; 395 float mSpacingMult; 396 float mSpacingAdd; 397 boolean mIncludePad; 398 int mEllipsizedWidth; 399 TextUtils.TruncateAt mEllipsize; 400 int mMaxLines; 401 int mBreakStrategy; 402 int mHyphenationFrequency; 403 int[] mLeftIndents; 404 int[] mRightIndents; 405 406 Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); 407 408 // This will go away and be subsumed by native builder code 409 MeasuredText mMeasuredText; 410 411 Locale mLocale; 412 413 private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3); 414 } 415 416 public StaticLayout(CharSequence source, TextPaint paint, 417 int width, 418 Alignment align, float spacingmult, float spacingadd, 419 boolean includepad) { 420 this(source, 0, source.length(), paint, width, align, 421 spacingmult, spacingadd, includepad); 422 } 423 424 /** 425 * @hide 426 */ 427 public StaticLayout(CharSequence source, TextPaint paint, 428 int width, Alignment align, TextDirectionHeuristic textDir, 429 float spacingmult, float spacingadd, 430 boolean includepad) { 431 this(source, 0, source.length(), paint, width, align, textDir, 432 spacingmult, spacingadd, includepad); 433 } 434 435 public StaticLayout(CharSequence source, int bufstart, int bufend, 436 TextPaint paint, int outerwidth, 437 Alignment align, 438 float spacingmult, float spacingadd, 439 boolean includepad) { 440 this(source, bufstart, bufend, paint, outerwidth, align, 441 spacingmult, spacingadd, includepad, null, 0); 442 } 443 444 /** 445 * @hide 446 */ 447 public StaticLayout(CharSequence source, int bufstart, int bufend, 448 TextPaint paint, int outerwidth, 449 Alignment align, TextDirectionHeuristic textDir, 450 float spacingmult, float spacingadd, 451 boolean includepad) { 452 this(source, bufstart, bufend, paint, outerwidth, align, textDir, 453 spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE); 454 } 455 456 public StaticLayout(CharSequence source, int bufstart, int bufend, 457 TextPaint paint, int outerwidth, 458 Alignment align, 459 float spacingmult, float spacingadd, 460 boolean includepad, 461 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 462 this(source, bufstart, bufend, paint, outerwidth, align, 463 TextDirectionHeuristics.FIRSTSTRONG_LTR, 464 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE); 465 } 466 467 /** 468 * @hide 469 */ 470 public StaticLayout(CharSequence source, int bufstart, int bufend, 471 TextPaint paint, int outerwidth, 472 Alignment align, TextDirectionHeuristic textDir, 473 float spacingmult, float spacingadd, 474 boolean includepad, 475 TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) { 476 super((ellipsize == null) 477 ? source 478 : (source instanceof Spanned) 479 ? new SpannedEllipsizer(source) 480 : new Ellipsizer(source), 481 paint, outerwidth, align, textDir, spacingmult, spacingadd); 482 483 Builder b = Builder.obtain(source, bufstart, bufend, paint, outerwidth) 484 .setAlignment(align) 485 .setTextDirection(textDir) 486 .setLineSpacing(spacingadd, spacingmult) 487 .setIncludePad(includepad) 488 .setEllipsizedWidth(ellipsizedWidth) 489 .setEllipsize(ellipsize) 490 .setMaxLines(maxLines); 491 /* 492 * This is annoying, but we can't refer to the layout until 493 * superclass construction is finished, and the superclass 494 * constructor wants the reference to the display text. 495 * 496 * This will break if the superclass constructor ever actually 497 * cares about the content instead of just holding the reference. 498 */ 499 if (ellipsize != null) { 500 Ellipsizer e = (Ellipsizer) getText(); 501 502 e.mLayout = this; 503 e.mWidth = ellipsizedWidth; 504 e.mMethod = ellipsize; 505 mEllipsizedWidth = ellipsizedWidth; 506 507 mColumns = COLUMNS_ELLIPSIZE; 508 } else { 509 mColumns = COLUMNS_NORMAL; 510 mEllipsizedWidth = outerwidth; 511 } 512 513 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); 514 mLines = new int[mLineDirections.length]; 515 mMaximumVisibleLineCount = maxLines; 516 517 generate(b, b.mIncludePad, b.mIncludePad); 518 519 Builder.recycle(b); 520 } 521 522 /* package */ StaticLayout(CharSequence text) { 523 super(text, null, 0, null, 0, 0); 524 525 mColumns = COLUMNS_ELLIPSIZE; 526 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); 527 mLines = new int[mLineDirections.length]; 528 } 529 530 private StaticLayout(Builder b) { 531 super((b.mEllipsize == null) 532 ? b.mText 533 : (b.mText instanceof Spanned) 534 ? new SpannedEllipsizer(b.mText) 535 : new Ellipsizer(b.mText), 536 b.mPaint, b.mWidth, b.mAlignment, b.mSpacingMult, b.mSpacingAdd); 537 538 if (b.mEllipsize != null) { 539 Ellipsizer e = (Ellipsizer) getText(); 540 541 e.mLayout = this; 542 e.mWidth = b.mEllipsizedWidth; 543 e.mMethod = b.mEllipsize; 544 mEllipsizedWidth = b.mEllipsizedWidth; 545 546 mColumns = COLUMNS_ELLIPSIZE; 547 } else { 548 mColumns = COLUMNS_NORMAL; 549 mEllipsizedWidth = b.mWidth; 550 } 551 552 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); 553 mLines = new int[mLineDirections.length]; 554 mMaximumVisibleLineCount = b.mMaxLines; 555 556 mLeftIndents = b.mLeftIndents; 557 mRightIndents = b.mRightIndents; 558 559 generate(b, b.mIncludePad, b.mIncludePad); 560 } 561 562 /* package */ void generate(Builder b, boolean includepad, boolean trackpad) { 563 CharSequence source = b.mText; 564 int bufStart = b.mStart; 565 int bufEnd = b.mEnd; 566 TextPaint paint = b.mPaint; 567 int outerWidth = b.mWidth; 568 TextDirectionHeuristic textDir = b.mTextDir; 569 float spacingmult = b.mSpacingMult; 570 float spacingadd = b.mSpacingAdd; 571 float ellipsizedWidth = b.mEllipsizedWidth; 572 TextUtils.TruncateAt ellipsize = b.mEllipsize; 573 LineBreaks lineBreaks = new LineBreaks(); // TODO: move to builder to avoid allocation costs 574 // store span end locations 575 int[] spanEndCache = new int[4]; 576 // store fontMetrics per span range 577 // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range) 578 int[] fmCache = new int[4 * 4]; 579 b.setLocale(paint.getTextLocale()); // TODO: also respect LocaleSpan within the text 580 581 mLineCount = 0; 582 583 int v = 0; 584 boolean needMultiply = (spacingmult != 1 || spacingadd != 0); 585 586 Paint.FontMetricsInt fm = b.mFontMetricsInt; 587 int[] chooseHtv = null; 588 589 MeasuredText measured = b.mMeasuredText; 590 591 Spanned spanned = null; 592 if (source instanceof Spanned) 593 spanned = (Spanned) source; 594 595 int paraEnd; 596 for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) { 597 paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd); 598 if (paraEnd < 0) 599 paraEnd = bufEnd; 600 else 601 paraEnd++; 602 603 int firstWidthLineCount = 1; 604 int firstWidth = outerWidth; 605 int restWidth = outerWidth; 606 607 LineHeightSpan[] chooseHt = null; 608 609 if (spanned != null) { 610 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd, 611 LeadingMarginSpan.class); 612 for (int i = 0; i < sp.length; i++) { 613 LeadingMarginSpan lms = sp[i]; 614 firstWidth -= sp[i].getLeadingMargin(true); 615 restWidth -= sp[i].getLeadingMargin(false); 616 617 // LeadingMarginSpan2 is odd. The count affects all 618 // leading margin spans, not just this particular one 619 if (lms instanceof LeadingMarginSpan2) { 620 LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms; 621 firstWidthLineCount = Math.max(firstWidthLineCount, 622 lms2.getLeadingMarginLineCount()); 623 } 624 } 625 626 chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class); 627 628 if (chooseHt.length != 0) { 629 if (chooseHtv == null || 630 chooseHtv.length < chooseHt.length) { 631 chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length); 632 } 633 634 for (int i = 0; i < chooseHt.length; i++) { 635 int o = spanned.getSpanStart(chooseHt[i]); 636 637 if (o < paraStart) { 638 // starts in this layout, before the 639 // current paragraph 640 641 chooseHtv[i] = getLineTop(getLineForOffset(o)); 642 } else { 643 // starts in this paragraph 644 645 chooseHtv[i] = v; 646 } 647 } 648 } 649 } 650 651 measured.setPara(source, paraStart, paraEnd, textDir, b); 652 char[] chs = measured.mChars; 653 float[] widths = measured.mWidths; 654 byte[] chdirs = measured.mLevels; 655 int dir = measured.mDir; 656 boolean easy = measured.mEasy; 657 658 // tab stop locations 659 int[] variableTabStops = null; 660 if (spanned != null) { 661 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, 662 paraEnd, TabStopSpan.class); 663 if (spans.length > 0) { 664 int[] stops = new int[spans.length]; 665 for (int i = 0; i < spans.length; i++) { 666 stops[i] = spans[i].getTabStop(); 667 } 668 Arrays.sort(stops, 0, stops.length); 669 variableTabStops = stops; 670 } 671 } 672 673 nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart, 674 firstWidth, firstWidthLineCount, restWidth, 675 variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency); 676 if (mLeftIndents != null || mRightIndents != null) { 677 // TODO(raph) performance: it would be better to do this once per layout rather 678 // than once per paragraph, but that would require a change to the native 679 // interface. 680 int leftLen = mLeftIndents == null ? 0 : mLeftIndents.length; 681 int rightLen = mRightIndents == null ? 0 : mRightIndents.length; 682 int indentsLen = Math.max(1, Math.min(leftLen, rightLen) - mLineCount); 683 int[] indents = new int[indentsLen]; 684 for (int i = 0; i < indentsLen; i++) { 685 int leftMargin = mLeftIndents == null ? 0 : 686 mLeftIndents[Math.min(i + mLineCount, leftLen - 1)]; 687 int rightMargin = mRightIndents == null ? 0 : 688 mRightIndents[Math.min(i + mLineCount, rightLen - 1)]; 689 indents[i] = leftMargin + rightMargin; 690 } 691 nSetIndents(b.mNativePtr, indents); 692 } 693 694 // measurement has to be done before performing line breaking 695 // but we don't want to recompute fontmetrics or span ranges the 696 // second time, so we cache those and then use those stored values 697 int fmCacheCount = 0; 698 int spanEndCacheCount = 0; 699 for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { 700 if (fmCacheCount * 4 >= fmCache.length) { 701 int[] grow = new int[fmCacheCount * 4 * 2]; 702 System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4); 703 fmCache = grow; 704 } 705 706 if (spanEndCacheCount >= spanEndCache.length) { 707 int[] grow = new int[spanEndCacheCount * 2]; 708 System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount); 709 spanEndCache = grow; 710 } 711 712 if (spanned == null) { 713 spanEnd = paraEnd; 714 int spanLen = spanEnd - spanStart; 715 measured.addStyleRun(paint, spanLen, fm); 716 } else { 717 spanEnd = spanned.nextSpanTransition(spanStart, paraEnd, 718 MetricAffectingSpan.class); 719 int spanLen = spanEnd - spanStart; 720 MetricAffectingSpan[] spans = 721 spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); 722 spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class); 723 measured.addStyleRun(paint, spans, spanLen, fm); 724 } 725 726 // the order of storage here (top, bottom, ascent, descent) has to match the code below 727 // where these values are retrieved 728 fmCache[fmCacheCount * 4 + 0] = fm.top; 729 fmCache[fmCacheCount * 4 + 1] = fm.bottom; 730 fmCache[fmCacheCount * 4 + 2] = fm.ascent; 731 fmCache[fmCacheCount * 4 + 3] = fm.descent; 732 fmCacheCount++; 733 734 spanEndCache[spanEndCacheCount] = spanEnd; 735 spanEndCacheCount++; 736 } 737 738 nGetWidths(b.mNativePtr, widths); 739 int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks, 740 lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length); 741 742 int[] breaks = lineBreaks.breaks; 743 float[] lineWidths = lineBreaks.widths; 744 int[] flags = lineBreaks.flags; 745 746 final int remainingLineCount = mMaximumVisibleLineCount - mLineCount; 747 final boolean ellipsisMayBeApplied = ellipsize != null 748 && (ellipsize == TextUtils.TruncateAt.END 749 || (mMaximumVisibleLineCount == 1 750 && ellipsize != TextUtils.TruncateAt.MARQUEE)); 751 if (remainingLineCount > 0 && remainingLineCount < breakCount && 752 ellipsisMayBeApplied) { 753 // Treat the last line and overflowed lines as a single line. 754 breaks[remainingLineCount - 1] = breaks[breakCount - 1]; 755 // Calculate width and flag. 756 float width = 0; 757 int flag = 0; 758 for (int i = remainingLineCount - 1; i < breakCount; i++) { 759 width += lineWidths[i]; 760 flag |= flags[i] & TAB_MASK; 761 } 762 lineWidths[remainingLineCount - 1] = width; 763 flags[remainingLineCount - 1] = flag; 764 765 breakCount = remainingLineCount; 766 } 767 768 // here is the offset of the starting character of the line we are currently measuring 769 int here = paraStart; 770 771 int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0; 772 int fmCacheIndex = 0; 773 int spanEndCacheIndex = 0; 774 int breakIndex = 0; 775 for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { 776 // retrieve end of span 777 spanEnd = spanEndCache[spanEndCacheIndex++]; 778 779 // retrieve cached metrics, order matches above 780 fm.top = fmCache[fmCacheIndex * 4 + 0]; 781 fm.bottom = fmCache[fmCacheIndex * 4 + 1]; 782 fm.ascent = fmCache[fmCacheIndex * 4 + 2]; 783 fm.descent = fmCache[fmCacheIndex * 4 + 3]; 784 fmCacheIndex++; 785 786 if (fm.top < fmTop) { 787 fmTop = fm.top; 788 } 789 if (fm.ascent < fmAscent) { 790 fmAscent = fm.ascent; 791 } 792 if (fm.descent > fmDescent) { 793 fmDescent = fm.descent; 794 } 795 if (fm.bottom > fmBottom) { 796 fmBottom = fm.bottom; 797 } 798 799 // skip breaks ending before current span range 800 while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) { 801 breakIndex++; 802 } 803 804 while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) { 805 int endPos = paraStart + breaks[breakIndex]; 806 807 boolean moreChars = (endPos < bufEnd); 808 809 v = out(source, here, endPos, 810 fmAscent, fmDescent, fmTop, fmBottom, 811 v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, flags[breakIndex], 812 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad, 813 chs, widths, paraStart, ellipsize, ellipsizedWidth, 814 lineWidths[breakIndex], paint, moreChars); 815 816 if (endPos < spanEnd) { 817 // preserve metrics for current span 818 fmTop = fm.top; 819 fmBottom = fm.bottom; 820 fmAscent = fm.ascent; 821 fmDescent = fm.descent; 822 } else { 823 fmTop = fmBottom = fmAscent = fmDescent = 0; 824 } 825 826 here = endPos; 827 breakIndex++; 828 829 if (mLineCount >= mMaximumVisibleLineCount) { 830 return; 831 } 832 } 833 } 834 835 if (paraEnd == bufEnd) 836 break; 837 } 838 839 if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) && 840 mLineCount < mMaximumVisibleLineCount) { 841 // Log.e("text", "output last " + bufEnd); 842 843 measured.setPara(source, bufEnd, bufEnd, textDir, b); 844 845 paint.getFontMetricsInt(fm); 846 847 v = out(source, 848 bufEnd, bufEnd, fm.ascent, fm.descent, 849 fm.top, fm.bottom, 850 v, 851 spacingmult, spacingadd, null, 852 null, fm, 0, 853 needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd, 854 includepad, trackpad, null, 855 null, bufStart, ellipsize, 856 ellipsizedWidth, 0, paint, false); 857 } 858 } 859 860 private int out(CharSequence text, int start, int end, 861 int above, int below, int top, int bottom, int v, 862 float spacingmult, float spacingadd, 863 LineHeightSpan[] chooseHt, int[] chooseHtv, 864 Paint.FontMetricsInt fm, int flags, 865 boolean needMultiply, byte[] chdirs, int dir, 866 boolean easy, int bufEnd, boolean includePad, 867 boolean trackPad, char[] chs, 868 float[] widths, int widthStart, TextUtils.TruncateAt ellipsize, 869 float ellipsisWidth, float textWidth, 870 TextPaint paint, boolean moreChars) { 871 int j = mLineCount; 872 int off = j * mColumns; 873 int want = off + mColumns + TOP; 874 int[] lines = mLines; 875 876 if (want >= lines.length) { 877 Directions[] grow2 = ArrayUtils.newUnpaddedArray( 878 Directions.class, GrowingArrayUtils.growSize(want)); 879 System.arraycopy(mLineDirections, 0, grow2, 0, 880 mLineDirections.length); 881 mLineDirections = grow2; 882 883 int[] grow = new int[grow2.length]; 884 System.arraycopy(lines, 0, grow, 0, lines.length); 885 mLines = grow; 886 lines = grow; 887 } 888 889 if (chooseHt != null) { 890 fm.ascent = above; 891 fm.descent = below; 892 fm.top = top; 893 fm.bottom = bottom; 894 895 for (int i = 0; i < chooseHt.length; i++) { 896 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { 897 ((LineHeightSpan.WithDensity) chooseHt[i]). 898 chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); 899 900 } else { 901 chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); 902 } 903 } 904 905 above = fm.ascent; 906 below = fm.descent; 907 top = fm.top; 908 bottom = fm.bottom; 909 } 910 911 boolean firstLine = (j == 0); 912 boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); 913 boolean lastLine = currentLineIsTheLastVisibleOne || (end == bufEnd); 914 915 if (firstLine) { 916 if (trackPad) { 917 mTopPadding = top - above; 918 } 919 920 if (includePad) { 921 above = top; 922 } 923 } 924 925 int extra; 926 927 if (lastLine) { 928 if (trackPad) { 929 mBottomPadding = bottom - below; 930 } 931 932 if (includePad) { 933 below = bottom; 934 } 935 } 936 937 938 if (needMultiply && !lastLine) { 939 double ex = (below - above) * (spacingmult - 1) + spacingadd; 940 if (ex >= 0) { 941 extra = (int)(ex + EXTRA_ROUNDING); 942 } else { 943 extra = -(int)(-ex + EXTRA_ROUNDING); 944 } 945 } else { 946 extra = 0; 947 } 948 949 lines[off + START] = start; 950 lines[off + TOP] = v; 951 lines[off + DESCENT] = below + extra; 952 953 v += (below - above) + extra; 954 lines[off + mColumns + START] = end; 955 lines[off + mColumns + TOP] = v; 956 957 // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining 958 // one bit for start field 959 lines[off + TAB] |= flags & TAB_MASK; 960 lines[off + HYPHEN] = flags; 961 962 lines[off + DIR] |= dir << DIR_SHIFT; 963 Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; 964 // easy means all chars < the first RTL, so no emoji, no nothing 965 // XXX a run with no text or all spaces is easy but might be an empty 966 // RTL paragraph. Make sure easy is false if this is the case. 967 if (easy) { 968 mLineDirections[j] = linedirs; 969 } else { 970 mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs, 971 start - widthStart, end - start); 972 } 973 974 if (ellipsize != null) { 975 // If there is only one line, then do any type of ellipsis except when it is MARQUEE 976 // if there are multiple lines, just allow END ellipsis on the last line 977 boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount); 978 979 boolean doEllipsis = 980 (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) && 981 ellipsize != TextUtils.TruncateAt.MARQUEE) || 982 (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) && 983 ellipsize == TextUtils.TruncateAt.END); 984 if (doEllipsis) { 985 calculateEllipsis(start, end, widths, widthStart, 986 ellipsisWidth, ellipsize, j, 987 textWidth, paint, forceEllipsis); 988 } 989 } 990 991 mLineCount++; 992 return v; 993 } 994 995 private void calculateEllipsis(int lineStart, int lineEnd, 996 float[] widths, int widthStart, 997 float avail, TextUtils.TruncateAt where, 998 int line, float textWidth, TextPaint paint, 999 boolean forceEllipsis) { 1000 if (textWidth <= avail && !forceEllipsis) { 1001 // Everything fits! 1002 mLines[mColumns * line + ELLIPSIS_START] = 0; 1003 mLines[mColumns * line + ELLIPSIS_COUNT] = 0; 1004 return; 1005 } 1006 1007 float ellipsisWidth = paint.measureText( 1008 (where == TextUtils.TruncateAt.END_SMALL) ? 1009 TextUtils.ELLIPSIS_TWO_DOTS : TextUtils.ELLIPSIS_NORMAL, 0, 1); 1010 int ellipsisStart = 0; 1011 int ellipsisCount = 0; 1012 int len = lineEnd - lineStart; 1013 1014 // We only support start ellipsis on a single line 1015 if (where == TextUtils.TruncateAt.START) { 1016 if (mMaximumVisibleLineCount == 1) { 1017 float sum = 0; 1018 int i; 1019 1020 for (i = len; i > 0; i--) { 1021 float w = widths[i - 1 + lineStart - widthStart]; 1022 1023 if (w + sum + ellipsisWidth > avail) { 1024 break; 1025 } 1026 1027 sum += w; 1028 } 1029 1030 ellipsisStart = 0; 1031 ellipsisCount = i; 1032 } else { 1033 if (Log.isLoggable(TAG, Log.WARN)) { 1034 Log.w(TAG, "Start Ellipsis only supported with one line"); 1035 } 1036 } 1037 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE || 1038 where == TextUtils.TruncateAt.END_SMALL) { 1039 float sum = 0; 1040 int i; 1041 1042 for (i = 0; i < len; i++) { 1043 float w = widths[i + lineStart - widthStart]; 1044 1045 if (w + sum + ellipsisWidth > avail) { 1046 break; 1047 } 1048 1049 sum += w; 1050 } 1051 1052 ellipsisStart = i; 1053 ellipsisCount = len - i; 1054 if (forceEllipsis && ellipsisCount == 0 && len > 0) { 1055 ellipsisStart = len - 1; 1056 ellipsisCount = 1; 1057 } 1058 } else { 1059 // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line 1060 if (mMaximumVisibleLineCount == 1) { 1061 float lsum = 0, rsum = 0; 1062 int left = 0, right = len; 1063 1064 float ravail = (avail - ellipsisWidth) / 2; 1065 for (right = len; right > 0; right--) { 1066 float w = widths[right - 1 + lineStart - widthStart]; 1067 1068 if (w + rsum > ravail) { 1069 break; 1070 } 1071 1072 rsum += w; 1073 } 1074 1075 float lavail = avail - ellipsisWidth - rsum; 1076 for (left = 0; left < right; left++) { 1077 float w = widths[left + lineStart - widthStart]; 1078 1079 if (w + lsum > lavail) { 1080 break; 1081 } 1082 1083 lsum += w; 1084 } 1085 1086 ellipsisStart = left; 1087 ellipsisCount = right - left; 1088 } else { 1089 if (Log.isLoggable(TAG, Log.WARN)) { 1090 Log.w(TAG, "Middle Ellipsis only supported with one line"); 1091 } 1092 } 1093 } 1094 1095 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; 1096 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; 1097 } 1098 1099 // Override the base class so we can directly access our members, 1100 // rather than relying on member functions. 1101 // The logic mirrors that of Layout.getLineForVertical 1102 // FIXME: It may be faster to do a linear search for layouts without many lines. 1103 @Override 1104 public int getLineForVertical(int vertical) { 1105 int high = mLineCount; 1106 int low = -1; 1107 int guess; 1108 int[] lines = mLines; 1109 while (high - low > 1) { 1110 guess = (high + low) >> 1; 1111 if (lines[mColumns * guess + TOP] > vertical){ 1112 high = guess; 1113 } else { 1114 low = guess; 1115 } 1116 } 1117 if (low < 0) { 1118 return 0; 1119 } else { 1120 return low; 1121 } 1122 } 1123 1124 @Override 1125 public int getLineCount() { 1126 return mLineCount; 1127 } 1128 1129 @Override 1130 public int getLineTop(int line) { 1131 int top = mLines[mColumns * line + TOP]; 1132 if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount && 1133 line != mLineCount) { 1134 top += getBottomPadding(); 1135 } 1136 return top; 1137 } 1138 1139 @Override 1140 public int getLineDescent(int line) { 1141 int descent = mLines[mColumns * line + DESCENT]; 1142 if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended 1143 line != mLineCount) { 1144 descent += getBottomPadding(); 1145 } 1146 return descent; 1147 } 1148 1149 @Override 1150 public int getLineStart(int line) { 1151 return mLines[mColumns * line + START] & START_MASK; 1152 } 1153 1154 @Override 1155 public int getParagraphDirection(int line) { 1156 return mLines[mColumns * line + DIR] >> DIR_SHIFT; 1157 } 1158 1159 @Override 1160 public boolean getLineContainsTab(int line) { 1161 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0; 1162 } 1163 1164 @Override 1165 public final Directions getLineDirections(int line) { 1166 return mLineDirections[line]; 1167 } 1168 1169 @Override 1170 public int getTopPadding() { 1171 return mTopPadding; 1172 } 1173 1174 @Override 1175 public int getBottomPadding() { 1176 return mBottomPadding; 1177 } 1178 1179 /** 1180 * @hide 1181 */ 1182 @Override 1183 public int getHyphen(int line) { 1184 return mLines[mColumns * line + HYPHEN] & 0xff; 1185 } 1186 1187 /** 1188 * @hide 1189 */ 1190 @Override 1191 public int getIndentAdjust(int line, Alignment align) { 1192 if (align == Alignment.ALIGN_LEFT) { 1193 if (mLeftIndents == null) { 1194 return 0; 1195 } else { 1196 return mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1197 } 1198 } else if (align == Alignment.ALIGN_RIGHT) { 1199 if (mRightIndents == null) { 1200 return 0; 1201 } else { 1202 return -mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1203 } 1204 } else if (align == Alignment.ALIGN_CENTER) { 1205 int left = 0; 1206 if (mLeftIndents != null) { 1207 left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)]; 1208 } 1209 int right = 0; 1210 if (mRightIndents != null) { 1211 right = mRightIndents[Math.min(line, mRightIndents.length - 1)]; 1212 } 1213 return (left - right) >> 1; 1214 } else { 1215 throw new AssertionError("unhandled alignment " + align); 1216 } 1217 } 1218 1219 @Override 1220 public int getEllipsisCount(int line) { 1221 if (mColumns < COLUMNS_ELLIPSIZE) { 1222 return 0; 1223 } 1224 1225 return mLines[mColumns * line + ELLIPSIS_COUNT]; 1226 } 1227 1228 @Override 1229 public int getEllipsisStart(int line) { 1230 if (mColumns < COLUMNS_ELLIPSIZE) { 1231 return 0; 1232 } 1233 1234 return mLines[mColumns * line + ELLIPSIS_START]; 1235 } 1236 1237 @Override 1238 public int getEllipsizedWidth() { 1239 return mEllipsizedWidth; 1240 } 1241 1242 private static native long nNewBuilder(); 1243 private static native void nFreeBuilder(long nativePtr); 1244 private static native void nFinishBuilder(long nativePtr); 1245 1246 /* package */ static native long nLoadHyphenator(String patternData); 1247 1248 private static native void nSetLocale(long nativePtr, String locale, long nativeHyphenator); 1249 1250 private static native void nSetIndents(long nativePtr, int[] indents); 1251 1252 // Set up paragraph text and settings; done as one big method to minimize jni crossings 1253 private static native void nSetupParagraph(long nativePtr, char[] text, int length, 1254 float firstWidth, int firstWidthLineCount, float restWidth, 1255 int[] variableTabStops, int defaultTabStop, int breakStrategy, int hyphenationFrequency); 1256 1257 private static native float nAddStyleRun(long nativePtr, long nativePaint, 1258 long nativeTypeface, int start, int end, boolean isRtl); 1259 1260 private static native void nAddMeasuredRun(long nativePtr, 1261 int start, int end, float[] widths); 1262 1263 private static native void nAddReplacementRun(long nativePtr, int start, int end, float width); 1264 1265 private static native void nGetWidths(long nativePtr, float[] widths); 1266 1267 // populates LineBreaks and returns the number of breaks found 1268 // 1269 // the arrays inside the LineBreaks objects are passed in as well 1270 // to reduce the number of JNI calls in the common case where the 1271 // arrays do not have to be resized 1272 private static native int nComputeLineBreaks(long nativePtr, LineBreaks recycle, 1273 int[] recycleBreaks, float[] recycleWidths, int[] recycleFlags, int recycleLength); 1274 1275 private int mLineCount; 1276 private int mTopPadding, mBottomPadding; 1277 private int mColumns; 1278 private int mEllipsizedWidth; 1279 1280 private static final int COLUMNS_NORMAL = 4; 1281 private static final int COLUMNS_ELLIPSIZE = 6; 1282 private static final int START = 0; 1283 private static final int DIR = START; 1284 private static final int TAB = START; 1285 private static final int TOP = 1; 1286 private static final int DESCENT = 2; 1287 private static final int HYPHEN = 3; 1288 private static final int ELLIPSIS_START = 4; 1289 private static final int ELLIPSIS_COUNT = 5; 1290 1291 private int[] mLines; 1292 private Directions[] mLineDirections; 1293 private int mMaximumVisibleLineCount = Integer.MAX_VALUE; 1294 1295 private static final int START_MASK = 0x1FFFFFFF; 1296 private static final int DIR_SHIFT = 30; 1297 private static final int TAB_MASK = 0x20000000; 1298 1299 private static final int TAB_INCREMENT = 20; // same as Layout, but that's private 1300 1301 private static final char CHAR_NEW_LINE = '\n'; 1302 1303 private static final double EXTRA_ROUNDING = 0.5; 1304 1305 // This is used to return three arrays from a single JNI call when 1306 // performing line breaking 1307 /*package*/ static class LineBreaks { 1308 private static final int INITIAL_SIZE = 16; 1309 public int[] breaks = new int[INITIAL_SIZE]; 1310 public float[] widths = new float[INITIAL_SIZE]; 1311 public int[] flags = new int[INITIAL_SIZE]; // hasTabOrEmoji 1312 // breaks, widths, and flags should all have the same length 1313 } 1314 1315 private int[] mLeftIndents; 1316 private int[] mRightIndents; 1317 } 1318