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.max(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 // Calculate width and flag. 758 float width = 0; 759 int flag = 0; 760 for (int i = remainingLineCount - 1; i < breakCount; i++) { 761 if (i == breakCount - 1) { 762 width += lineWidths[i]; 763 } else { 764 for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) { 765 width += widths[j]; 766 } 767 } 768 flag |= flags[i] & TAB_MASK; 769 } 770 // Treat the last line and overflowed lines as a single line. 771 breaks[remainingLineCount - 1] = breaks[breakCount - 1]; 772 lineWidths[remainingLineCount - 1] = width; 773 flags[remainingLineCount - 1] = flag; 774 775 breakCount = remainingLineCount; 776 } 777 778 // here is the offset of the starting character of the line we are currently measuring 779 int here = paraStart; 780 781 int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0; 782 int fmCacheIndex = 0; 783 int spanEndCacheIndex = 0; 784 int breakIndex = 0; 785 for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { 786 // retrieve end of span 787 spanEnd = spanEndCache[spanEndCacheIndex++]; 788 789 // retrieve cached metrics, order matches above 790 fm.top = fmCache[fmCacheIndex * 4 + 0]; 791 fm.bottom = fmCache[fmCacheIndex * 4 + 1]; 792 fm.ascent = fmCache[fmCacheIndex * 4 + 2]; 793 fm.descent = fmCache[fmCacheIndex * 4 + 3]; 794 fmCacheIndex++; 795 796 if (fm.top < fmTop) { 797 fmTop = fm.top; 798 } 799 if (fm.ascent < fmAscent) { 800 fmAscent = fm.ascent; 801 } 802 if (fm.descent > fmDescent) { 803 fmDescent = fm.descent; 804 } 805 if (fm.bottom > fmBottom) { 806 fmBottom = fm.bottom; 807 } 808 809 // skip breaks ending before current span range 810 while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) { 811 breakIndex++; 812 } 813 814 while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) { 815 int endPos = paraStart + breaks[breakIndex]; 816 817 boolean moreChars = (endPos < bufEnd); 818 819 v = out(source, here, endPos, 820 fmAscent, fmDescent, fmTop, fmBottom, 821 v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, flags[breakIndex], 822 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad, 823 chs, widths, paraStart, ellipsize, ellipsizedWidth, 824 lineWidths[breakIndex], paint, moreChars); 825 826 if (endPos < spanEnd) { 827 // preserve metrics for current span 828 fmTop = fm.top; 829 fmBottom = fm.bottom; 830 fmAscent = fm.ascent; 831 fmDescent = fm.descent; 832 } else { 833 fmTop = fmBottom = fmAscent = fmDescent = 0; 834 } 835 836 here = endPos; 837 breakIndex++; 838 839 if (mLineCount >= mMaximumVisibleLineCount) { 840 return; 841 } 842 } 843 } 844 845 if (paraEnd == bufEnd) 846 break; 847 } 848 849 if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) && 850 mLineCount < mMaximumVisibleLineCount) { 851 // Log.e("text", "output last " + bufEnd); 852 853 measured.setPara(source, bufEnd, bufEnd, textDir, b); 854 855 paint.getFontMetricsInt(fm); 856 857 v = out(source, 858 bufEnd, bufEnd, fm.ascent, fm.descent, 859 fm.top, fm.bottom, 860 v, 861 spacingmult, spacingadd, null, 862 null, fm, 0, 863 needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd, 864 includepad, trackpad, null, 865 null, bufStart, ellipsize, 866 ellipsizedWidth, 0, paint, false); 867 } 868 } 869 870 private int out(CharSequence text, int start, int end, 871 int above, int below, int top, int bottom, int v, 872 float spacingmult, float spacingadd, 873 LineHeightSpan[] chooseHt, int[] chooseHtv, 874 Paint.FontMetricsInt fm, int flags, 875 boolean needMultiply, byte[] chdirs, int dir, 876 boolean easy, int bufEnd, boolean includePad, 877 boolean trackPad, char[] chs, 878 float[] widths, int widthStart, TextUtils.TruncateAt ellipsize, 879 float ellipsisWidth, float textWidth, 880 TextPaint paint, boolean moreChars) { 881 int j = mLineCount; 882 int off = j * mColumns; 883 int want = off + mColumns + TOP; 884 int[] lines = mLines; 885 886 if (want >= lines.length) { 887 Directions[] grow2 = ArrayUtils.newUnpaddedArray( 888 Directions.class, GrowingArrayUtils.growSize(want)); 889 System.arraycopy(mLineDirections, 0, grow2, 0, 890 mLineDirections.length); 891 mLineDirections = grow2; 892 893 int[] grow = new int[grow2.length]; 894 System.arraycopy(lines, 0, grow, 0, lines.length); 895 mLines = grow; 896 lines = grow; 897 } 898 899 if (chooseHt != null) { 900 fm.ascent = above; 901 fm.descent = below; 902 fm.top = top; 903 fm.bottom = bottom; 904 905 for (int i = 0; i < chooseHt.length; i++) { 906 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { 907 ((LineHeightSpan.WithDensity) chooseHt[i]). 908 chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); 909 910 } else { 911 chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); 912 } 913 } 914 915 above = fm.ascent; 916 below = fm.descent; 917 top = fm.top; 918 bottom = fm.bottom; 919 } 920 921 boolean firstLine = (j == 0); 922 boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); 923 boolean lastLine = currentLineIsTheLastVisibleOne || (end == bufEnd); 924 925 if (firstLine) { 926 if (trackPad) { 927 mTopPadding = top - above; 928 } 929 930 if (includePad) { 931 above = top; 932 } 933 } 934 935 int extra; 936 937 if (lastLine) { 938 if (trackPad) { 939 mBottomPadding = bottom - below; 940 } 941 942 if (includePad) { 943 below = bottom; 944 } 945 } 946 947 948 if (needMultiply && !lastLine) { 949 double ex = (below - above) * (spacingmult - 1) + spacingadd; 950 if (ex >= 0) { 951 extra = (int)(ex + EXTRA_ROUNDING); 952 } else { 953 extra = -(int)(-ex + EXTRA_ROUNDING); 954 } 955 } else { 956 extra = 0; 957 } 958 959 lines[off + START] = start; 960 lines[off + TOP] = v; 961 lines[off + DESCENT] = below + extra; 962 963 v += (below - above) + extra; 964 lines[off + mColumns + START] = end; 965 lines[off + mColumns + TOP] = v; 966 967 // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining 968 // one bit for start field 969 lines[off + TAB] |= flags & TAB_MASK; 970 lines[off + HYPHEN] = flags; 971 972 lines[off + DIR] |= dir << DIR_SHIFT; 973 Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; 974 // easy means all chars < the first RTL, so no emoji, no nothing 975 // XXX a run with no text or all spaces is easy but might be an empty 976 // RTL paragraph. Make sure easy is false if this is the case. 977 if (easy) { 978 mLineDirections[j] = linedirs; 979 } else { 980 mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs, 981 start - widthStart, end - start); 982 } 983 984 if (ellipsize != null) { 985 // If there is only one line, then do any type of ellipsis except when it is MARQUEE 986 // if there are multiple lines, just allow END ellipsis on the last line 987 boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount); 988 989 boolean doEllipsis = 990 (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) && 991 ellipsize != TextUtils.TruncateAt.MARQUEE) || 992 (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) && 993 ellipsize == TextUtils.TruncateAt.END); 994 if (doEllipsis) { 995 calculateEllipsis(start, end, widths, widthStart, 996 ellipsisWidth, ellipsize, j, 997 textWidth, paint, forceEllipsis); 998 } 999 } 1000 1001 mLineCount++; 1002 return v; 1003 } 1004 1005 private void calculateEllipsis(int lineStart, int lineEnd, 1006 float[] widths, int widthStart, 1007 float avail, TextUtils.TruncateAt where, 1008 int line, float textWidth, TextPaint paint, 1009 boolean forceEllipsis) { 1010 if (textWidth <= avail && !forceEllipsis) { 1011 // Everything fits! 1012 mLines[mColumns * line + ELLIPSIS_START] = 0; 1013 mLines[mColumns * line + ELLIPSIS_COUNT] = 0; 1014 return; 1015 } 1016 1017 float ellipsisWidth = paint.measureText( 1018 (where == TextUtils.TruncateAt.END_SMALL) ? 1019 TextUtils.ELLIPSIS_TWO_DOTS : TextUtils.ELLIPSIS_NORMAL, 0, 1); 1020 int ellipsisStart = 0; 1021 int ellipsisCount = 0; 1022 int len = lineEnd - lineStart; 1023 1024 // We only support start ellipsis on a single line 1025 if (where == TextUtils.TruncateAt.START) { 1026 if (mMaximumVisibleLineCount == 1) { 1027 float sum = 0; 1028 int i; 1029 1030 for (i = len; i > 0; i--) { 1031 float w = widths[i - 1 + lineStart - widthStart]; 1032 1033 if (w + sum + ellipsisWidth > avail) { 1034 break; 1035 } 1036 1037 sum += w; 1038 } 1039 1040 ellipsisStart = 0; 1041 ellipsisCount = i; 1042 } else { 1043 if (Log.isLoggable(TAG, Log.WARN)) { 1044 Log.w(TAG, "Start Ellipsis only supported with one line"); 1045 } 1046 } 1047 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE || 1048 where == TextUtils.TruncateAt.END_SMALL) { 1049 float sum = 0; 1050 int i; 1051 1052 for (i = 0; i < len; i++) { 1053 float w = widths[i + lineStart - widthStart]; 1054 1055 if (w + sum + ellipsisWidth > avail) { 1056 break; 1057 } 1058 1059 sum += w; 1060 } 1061 1062 ellipsisStart = i; 1063 ellipsisCount = len - i; 1064 if (forceEllipsis && ellipsisCount == 0 && len > 0) { 1065 ellipsisStart = len - 1; 1066 ellipsisCount = 1; 1067 } 1068 } else { 1069 // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line 1070 if (mMaximumVisibleLineCount == 1) { 1071 float lsum = 0, rsum = 0; 1072 int left = 0, right = len; 1073 1074 float ravail = (avail - ellipsisWidth) / 2; 1075 for (right = len; right > 0; right--) { 1076 float w = widths[right - 1 + lineStart - widthStart]; 1077 1078 if (w + rsum > ravail) { 1079 break; 1080 } 1081 1082 rsum += w; 1083 } 1084 1085 float lavail = avail - ellipsisWidth - rsum; 1086 for (left = 0; left < right; left++) { 1087 float w = widths[left + lineStart - widthStart]; 1088 1089 if (w + lsum > lavail) { 1090 break; 1091 } 1092 1093 lsum += w; 1094 } 1095 1096 ellipsisStart = left; 1097 ellipsisCount = right - left; 1098 } else { 1099 if (Log.isLoggable(TAG, Log.WARN)) { 1100 Log.w(TAG, "Middle Ellipsis only supported with one line"); 1101 } 1102 } 1103 } 1104 1105 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; 1106 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; 1107 } 1108 1109 // Override the base class so we can directly access our members, 1110 // rather than relying on member functions. 1111 // The logic mirrors that of Layout.getLineForVertical 1112 // FIXME: It may be faster to do a linear search for layouts without many lines. 1113 @Override 1114 public int getLineForVertical(int vertical) { 1115 int high = mLineCount; 1116 int low = -1; 1117 int guess; 1118 int[] lines = mLines; 1119 while (high - low > 1) { 1120 guess = (high + low) >> 1; 1121 if (lines[mColumns * guess + TOP] > vertical){ 1122 high = guess; 1123 } else { 1124 low = guess; 1125 } 1126 } 1127 if (low < 0) { 1128 return 0; 1129 } else { 1130 return low; 1131 } 1132 } 1133 1134 @Override 1135 public int getLineCount() { 1136 return mLineCount; 1137 } 1138 1139 @Override 1140 public int getLineTop(int line) { 1141 return mLines[mColumns * line + TOP]; 1142 } 1143 1144 @Override 1145 public int getLineDescent(int line) { 1146 return mLines[mColumns * line + 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(ByteBuffer buf, int offset); 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]; // hasTab 1312 // breaks, widths, and flags should all have the same length 1313 } 1314 1315 private int[] mLeftIndents; 1316 private int[] mRightIndents; 1317 } 1318