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