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