StaticLayout.java revision 4c02e831728daf9374b52e2fe3fdbf7ce982bfc4
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.graphics.Bitmap; 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; 27 28import com.android.internal.util.ArrayUtils; 29import com.android.internal.util.GrowingArrayUtils; 30 31import java.util.Arrays; 32 33/** 34 * StaticLayout is a Layout for text that will not be edited after it 35 * is laid out. Use {@link DynamicLayout} for text that may change. 36 * <p>This is used by widgets to control text layout. You should not need 37 * to use this class directly unless you are implementing your own widget 38 * or custom display object, or would be tempted to call 39 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, 40 * float, float, android.graphics.Paint) 41 * Canvas.drawText()} directly.</p> 42 */ 43public class StaticLayout extends Layout { 44 45 static final String TAG = "StaticLayout"; 46 47 public StaticLayout(CharSequence source, TextPaint paint, 48 int width, 49 Alignment align, float spacingmult, float spacingadd, 50 boolean includepad) { 51 this(source, 0, source.length(), paint, width, align, 52 spacingmult, spacingadd, includepad); 53 } 54 55 /** 56 * @hide 57 */ 58 public StaticLayout(CharSequence source, TextPaint paint, 59 int width, Alignment align, TextDirectionHeuristic textDir, 60 float spacingmult, float spacingadd, 61 boolean includepad) { 62 this(source, 0, source.length(), paint, width, align, textDir, 63 spacingmult, spacingadd, includepad); 64 } 65 66 public StaticLayout(CharSequence source, int bufstart, int bufend, 67 TextPaint paint, int outerwidth, 68 Alignment align, 69 float spacingmult, float spacingadd, 70 boolean includepad) { 71 this(source, bufstart, bufend, paint, outerwidth, align, 72 spacingmult, spacingadd, includepad, null, 0); 73 } 74 75 /** 76 * @hide 77 */ 78 public StaticLayout(CharSequence source, int bufstart, int bufend, 79 TextPaint paint, int outerwidth, 80 Alignment align, TextDirectionHeuristic textDir, 81 float spacingmult, float spacingadd, 82 boolean includepad) { 83 this(source, bufstart, bufend, paint, outerwidth, align, textDir, 84 spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE); 85} 86 87 public StaticLayout(CharSequence source, int bufstart, int bufend, 88 TextPaint paint, int outerwidth, 89 Alignment align, 90 float spacingmult, float spacingadd, 91 boolean includepad, 92 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 93 this(source, bufstart, bufend, paint, outerwidth, align, 94 TextDirectionHeuristics.FIRSTSTRONG_LTR, 95 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE); 96 } 97 98 /** 99 * @hide 100 */ 101 public StaticLayout(CharSequence source, int bufstart, int bufend, 102 TextPaint paint, int outerwidth, 103 Alignment align, TextDirectionHeuristic textDir, 104 float spacingmult, float spacingadd, 105 boolean includepad, 106 TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) { 107 super((ellipsize == null) 108 ? source 109 : (source instanceof Spanned) 110 ? new SpannedEllipsizer(source) 111 : new Ellipsizer(source), 112 paint, outerwidth, align, textDir, spacingmult, spacingadd); 113 114 /* 115 * This is annoying, but we can't refer to the layout until 116 * superclass construction is finished, and the superclass 117 * constructor wants the reference to the display text. 118 * 119 * This will break if the superclass constructor ever actually 120 * cares about the content instead of just holding the reference. 121 */ 122 if (ellipsize != null) { 123 Ellipsizer e = (Ellipsizer) getText(); 124 125 e.mLayout = this; 126 e.mWidth = ellipsizedWidth; 127 e.mMethod = ellipsize; 128 mEllipsizedWidth = ellipsizedWidth; 129 130 mColumns = COLUMNS_ELLIPSIZE; 131 } else { 132 mColumns = COLUMNS_NORMAL; 133 mEllipsizedWidth = outerwidth; 134 } 135 136 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); 137 mLines = new int[mLineDirections.length]; 138 mMaximumVisibleLineCount = maxLines; 139 140 mMeasured = MeasuredText.obtain(); 141 142 generate(source, bufstart, bufend, paint, outerwidth, textDir, spacingmult, 143 spacingadd, includepad, includepad, ellipsizedWidth, 144 ellipsize); 145 146 mMeasured = MeasuredText.recycle(mMeasured); 147 mFontMetricsInt = null; 148 } 149 150 /* package */ StaticLayout(CharSequence text) { 151 super(text, null, 0, null, 0, 0); 152 153 mColumns = COLUMNS_ELLIPSIZE; 154 mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2 * mColumns); 155 mLines = new int[mLineDirections.length]; 156 // FIXME This is never recycled 157 mMeasured = MeasuredText.obtain(); 158 } 159 160 /* package */ void generate(CharSequence source, int bufStart, int bufEnd, 161 TextPaint paint, int outerWidth, 162 TextDirectionHeuristic textDir, float spacingmult, 163 float spacingadd, boolean includepad, 164 boolean trackpad, float ellipsizedWidth, 165 TextUtils.TruncateAt ellipsize) { 166 LineBreaks lineBreaks = new LineBreaks(); 167 // store span end locations 168 int[] spanEndCache = new int[4]; 169 // store fontMetrics per span range 170 // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range) 171 int[] fmCache = new int[4 * 4]; 172 final String localeLanguageTag = paint.getTextLocale().toLanguageTag(); 173 174 mLineCount = 0; 175 176 int v = 0; 177 boolean needMultiply = (spacingmult != 1 || spacingadd != 0); 178 179 Paint.FontMetricsInt fm = mFontMetricsInt; 180 int[] chooseHtv = null; 181 182 MeasuredText measured = mMeasured; 183 184 Spanned spanned = null; 185 if (source instanceof Spanned) 186 spanned = (Spanned) source; 187 188 int paraEnd; 189 for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) { 190 paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd); 191 if (paraEnd < 0) 192 paraEnd = bufEnd; 193 else 194 paraEnd++; 195 196 int firstWidthLineCount = 1; 197 int firstWidth = outerWidth; 198 int restWidth = outerWidth; 199 200 LineHeightSpan[] chooseHt = null; 201 202 if (spanned != null) { 203 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd, 204 LeadingMarginSpan.class); 205 for (int i = 0; i < sp.length; i++) { 206 LeadingMarginSpan lms = sp[i]; 207 firstWidth -= sp[i].getLeadingMargin(true); 208 restWidth -= sp[i].getLeadingMargin(false); 209 210 // LeadingMarginSpan2 is odd. The count affects all 211 // leading margin spans, not just this particular one 212 if (lms instanceof LeadingMarginSpan2) { 213 LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms; 214 firstWidthLineCount = Math.max(firstWidthLineCount, 215 lms2.getLeadingMarginLineCount()); 216 } 217 } 218 219 chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class); 220 221 if (chooseHt.length != 0) { 222 if (chooseHtv == null || 223 chooseHtv.length < chooseHt.length) { 224 chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length); 225 } 226 227 for (int i = 0; i < chooseHt.length; i++) { 228 int o = spanned.getSpanStart(chooseHt[i]); 229 230 if (o < paraStart) { 231 // starts in this layout, before the 232 // current paragraph 233 234 chooseHtv[i] = getLineTop(getLineForOffset(o)); 235 } else { 236 // starts in this paragraph 237 238 chooseHtv[i] = v; 239 } 240 } 241 } 242 } 243 244 measured.setPara(source, paraStart, paraEnd, textDir); 245 char[] chs = measured.mChars; 246 float[] widths = measured.mWidths; 247 byte[] chdirs = measured.mLevels; 248 int dir = measured.mDir; 249 boolean easy = measured.mEasy; 250 251 // measurement has to be done before performing line breaking 252 // but we don't want to recompute fontmetrics or span ranges the 253 // second time, so we cache those and then use those stored values 254 int fmCacheCount = 0; 255 int spanEndCacheCount = 0; 256 for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { 257 if (fmCacheCount * 4 >= fmCache.length) { 258 int[] grow = new int[fmCacheCount * 4 * 2]; 259 System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4); 260 fmCache = grow; 261 } 262 263 if (spanEndCacheCount >= spanEndCache.length) { 264 int[] grow = new int[spanEndCacheCount * 2]; 265 System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount); 266 spanEndCache = grow; 267 } 268 269 if (spanned == null) { 270 spanEnd = paraEnd; 271 int spanLen = spanEnd - spanStart; 272 measured.addStyleRun(paint, spanLen, fm); 273 } else { 274 spanEnd = spanned.nextSpanTransition(spanStart, paraEnd, 275 MetricAffectingSpan.class); 276 int spanLen = spanEnd - spanStart; 277 MetricAffectingSpan[] spans = 278 spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); 279 spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class); 280 measured.addStyleRun(paint, spans, spanLen, fm); 281 } 282 283 // the order of storage here (top, bottom, ascent, descent) has to match the code below 284 // where these values are retrieved 285 fmCache[fmCacheCount * 4 + 0] = fm.top; 286 fmCache[fmCacheCount * 4 + 1] = fm.bottom; 287 fmCache[fmCacheCount * 4 + 2] = fm.ascent; 288 fmCache[fmCacheCount * 4 + 3] = fm.descent; 289 fmCacheCount++; 290 291 spanEndCache[spanEndCacheCount] = spanEnd; 292 spanEndCacheCount++; 293 } 294 295 // tab stop locations 296 int[] variableTabStops = null; 297 if (spanned != null) { 298 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, 299 paraEnd, TabStopSpan.class); 300 if (spans.length > 0) { 301 int[] stops = new int[spans.length]; 302 for (int i = 0; i < spans.length; i++) { 303 stops[i] = spans[i].getTabStop(); 304 } 305 Arrays.sort(stops, 0, stops.length); 306 variableTabStops = stops; 307 } 308 } 309 310 int breakCount = nComputeLineBreaks(localeLanguageTag, chs, widths, paraEnd - paraStart, firstWidth, 311 firstWidthLineCount, restWidth, variableTabStops, TAB_INCREMENT, false, lineBreaks, 312 lineBreaks.breaks, lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length); 313 314 int[] breaks = lineBreaks.breaks; 315 float[] lineWidths = lineBreaks.widths; 316 boolean[] flags = lineBreaks.flags; 317 318 // here is the offset of the starting character of the line we are currently measuring 319 int here = paraStart; 320 321 int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0; 322 int fmCacheIndex = 0; 323 int spanEndCacheIndex = 0; 324 int breakIndex = 0; 325 for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { 326 // retrieve end of span 327 spanEnd = spanEndCache[spanEndCacheIndex++]; 328 329 // retrieve cached metrics, order matches above 330 fm.top = fmCache[fmCacheIndex * 4 + 0]; 331 fm.bottom = fmCache[fmCacheIndex * 4 + 1]; 332 fm.ascent = fmCache[fmCacheIndex * 4 + 2]; 333 fm.descent = fmCache[fmCacheIndex * 4 + 3]; 334 fmCacheIndex++; 335 336 if (fm.top < fmTop) { 337 fmTop = fm.top; 338 } 339 if (fm.ascent < fmAscent) { 340 fmAscent = fm.ascent; 341 } 342 if (fm.descent > fmDescent) { 343 fmDescent = fm.descent; 344 } 345 if (fm.bottom > fmBottom) { 346 fmBottom = fm.bottom; 347 } 348 349 // skip breaks ending before current span range 350 while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) { 351 breakIndex++; 352 } 353 354 while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) { 355 int endPos = paraStart + breaks[breakIndex]; 356 357 boolean moreChars = (endPos < paraEnd); // XXX is this the right way to calculate this? 358 359 v = out(source, here, endPos, 360 fmAscent, fmDescent, fmTop, fmBottom, 361 v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, flags[breakIndex], 362 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad, 363 chs, widths, paraStart, ellipsize, ellipsizedWidth, 364 lineWidths[breakIndex], paint, moreChars); 365 366 if (endPos < spanEnd) { 367 // preserve metrics for current span 368 fmTop = fm.top; 369 fmBottom = fm.bottom; 370 fmAscent = fm.ascent; 371 fmDescent = fm.descent; 372 } else { 373 fmTop = fmBottom = fmAscent = fmDescent = 0; 374 } 375 376 here = endPos; 377 breakIndex++; 378 379 if (mLineCount >= mMaximumVisibleLineCount) { 380 return; 381 } 382 } 383 } 384 385 if (paraEnd == bufEnd) 386 break; 387 } 388 389 if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) && 390 mLineCount < mMaximumVisibleLineCount) { 391 // Log.e("text", "output last " + bufEnd); 392 393 measured.setPara(source, bufEnd, bufEnd, textDir); 394 395 paint.getFontMetricsInt(fm); 396 397 v = out(source, 398 bufEnd, bufEnd, fm.ascent, fm.descent, 399 fm.top, fm.bottom, 400 v, 401 spacingmult, spacingadd, null, 402 null, fm, false, 403 needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd, 404 includepad, trackpad, null, 405 null, bufStart, ellipsize, 406 ellipsizedWidth, 0, paint, false); 407 } 408 } 409 410 private int out(CharSequence text, int start, int end, 411 int above, int below, int top, int bottom, int v, 412 float spacingmult, float spacingadd, 413 LineHeightSpan[] chooseHt, int[] chooseHtv, 414 Paint.FontMetricsInt fm, boolean hasTabOrEmoji, 415 boolean needMultiply, byte[] chdirs, int dir, 416 boolean easy, int bufEnd, boolean includePad, 417 boolean trackPad, char[] chs, 418 float[] widths, int widthStart, TextUtils.TruncateAt ellipsize, 419 float ellipsisWidth, float textWidth, 420 TextPaint paint, boolean moreChars) { 421 int j = mLineCount; 422 int off = j * mColumns; 423 int want = off + mColumns + TOP; 424 int[] lines = mLines; 425 426 if (want >= lines.length) { 427 Directions[] grow2 = ArrayUtils.newUnpaddedArray( 428 Directions.class, GrowingArrayUtils.growSize(want)); 429 System.arraycopy(mLineDirections, 0, grow2, 0, 430 mLineDirections.length); 431 mLineDirections = grow2; 432 433 int[] grow = new int[grow2.length]; 434 System.arraycopy(lines, 0, grow, 0, lines.length); 435 mLines = grow; 436 lines = grow; 437 } 438 439 if (chooseHt != null) { 440 fm.ascent = above; 441 fm.descent = below; 442 fm.top = top; 443 fm.bottom = bottom; 444 445 for (int i = 0; i < chooseHt.length; i++) { 446 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { 447 ((LineHeightSpan.WithDensity) chooseHt[i]). 448 chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); 449 450 } else { 451 chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); 452 } 453 } 454 455 above = fm.ascent; 456 below = fm.descent; 457 top = fm.top; 458 bottom = fm.bottom; 459 } 460 461 boolean firstLine = (j == 0); 462 boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); 463 boolean lastLine = currentLineIsTheLastVisibleOne || (end == bufEnd); 464 465 if (firstLine) { 466 if (trackPad) { 467 mTopPadding = top - above; 468 } 469 470 if (includePad) { 471 above = top; 472 } 473 } 474 475 int extra; 476 477 if (lastLine) { 478 if (trackPad) { 479 mBottomPadding = bottom - below; 480 } 481 482 if (includePad) { 483 below = bottom; 484 } 485 } 486 487 488 if (needMultiply && !lastLine) { 489 double ex = (below - above) * (spacingmult - 1) + spacingadd; 490 if (ex >= 0) { 491 extra = (int)(ex + EXTRA_ROUNDING); 492 } else { 493 extra = -(int)(-ex + EXTRA_ROUNDING); 494 } 495 } else { 496 extra = 0; 497 } 498 499 lines[off + START] = start; 500 lines[off + TOP] = v; 501 lines[off + DESCENT] = below + extra; 502 503 v += (below - above) + extra; 504 lines[off + mColumns + START] = end; 505 lines[off + mColumns + TOP] = v; 506 507 if (hasTabOrEmoji) 508 lines[off + TAB] |= TAB_MASK; 509 510 lines[off + DIR] |= dir << DIR_SHIFT; 511 Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; 512 // easy means all chars < the first RTL, so no emoji, no nothing 513 // XXX a run with no text or all spaces is easy but might be an empty 514 // RTL paragraph. Make sure easy is false if this is the case. 515 if (easy) { 516 mLineDirections[j] = linedirs; 517 } else { 518 mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs, 519 start - widthStart, end - start); 520 } 521 522 if (ellipsize != null) { 523 // If there is only one line, then do any type of ellipsis except when it is MARQUEE 524 // if there are multiple lines, just allow END ellipsis on the last line 525 boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount); 526 527 boolean doEllipsis = 528 (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) && 529 ellipsize != TextUtils.TruncateAt.MARQUEE) || 530 (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) && 531 ellipsize == TextUtils.TruncateAt.END); 532 if (doEllipsis) { 533 calculateEllipsis(start, end, widths, widthStart, 534 ellipsisWidth, ellipsize, j, 535 textWidth, paint, forceEllipsis); 536 } 537 } 538 539 mLineCount++; 540 return v; 541 } 542 543 private void calculateEllipsis(int lineStart, int lineEnd, 544 float[] widths, int widthStart, 545 float avail, TextUtils.TruncateAt where, 546 int line, float textWidth, TextPaint paint, 547 boolean forceEllipsis) { 548 if (textWidth <= avail && !forceEllipsis) { 549 // Everything fits! 550 mLines[mColumns * line + ELLIPSIS_START] = 0; 551 mLines[mColumns * line + ELLIPSIS_COUNT] = 0; 552 return; 553 } 554 555 float ellipsisWidth = paint.measureText( 556 (where == TextUtils.TruncateAt.END_SMALL) ? 557 ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL, 0, 1); 558 int ellipsisStart = 0; 559 int ellipsisCount = 0; 560 int len = lineEnd - lineStart; 561 562 // We only support start ellipsis on a single line 563 if (where == TextUtils.TruncateAt.START) { 564 if (mMaximumVisibleLineCount == 1) { 565 float sum = 0; 566 int i; 567 568 for (i = len; i >= 0; i--) { 569 float w = widths[i - 1 + lineStart - widthStart]; 570 571 if (w + sum + ellipsisWidth > avail) { 572 break; 573 } 574 575 sum += w; 576 } 577 578 ellipsisStart = 0; 579 ellipsisCount = i; 580 } else { 581 if (Log.isLoggable(TAG, Log.WARN)) { 582 Log.w(TAG, "Start Ellipsis only supported with one line"); 583 } 584 } 585 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE || 586 where == TextUtils.TruncateAt.END_SMALL) { 587 float sum = 0; 588 int i; 589 590 for (i = 0; i < len; i++) { 591 float w = widths[i + lineStart - widthStart]; 592 593 if (w + sum + ellipsisWidth > avail) { 594 break; 595 } 596 597 sum += w; 598 } 599 600 ellipsisStart = i; 601 ellipsisCount = len - i; 602 if (forceEllipsis && ellipsisCount == 0 && len > 0) { 603 ellipsisStart = len - 1; 604 ellipsisCount = 1; 605 } 606 } else { 607 // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line 608 if (mMaximumVisibleLineCount == 1) { 609 float lsum = 0, rsum = 0; 610 int left = 0, right = len; 611 612 float ravail = (avail - ellipsisWidth) / 2; 613 for (right = len; right > 0; right--) { 614 float w = widths[right - 1 + lineStart - widthStart]; 615 616 if (w + rsum > ravail) { 617 break; 618 } 619 620 rsum += w; 621 } 622 623 float lavail = avail - ellipsisWidth - rsum; 624 for (left = 0; left < right; left++) { 625 float w = widths[left + lineStart - widthStart]; 626 627 if (w + lsum > lavail) { 628 break; 629 } 630 631 lsum += w; 632 } 633 634 ellipsisStart = left; 635 ellipsisCount = right - left; 636 } else { 637 if (Log.isLoggable(TAG, Log.WARN)) { 638 Log.w(TAG, "Middle Ellipsis only supported with one line"); 639 } 640 } 641 } 642 643 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; 644 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; 645 } 646 647 // Override the base class so we can directly access our members, 648 // rather than relying on member functions. 649 // The logic mirrors that of Layout.getLineForVertical 650 // FIXME: It may be faster to do a linear search for layouts without many lines. 651 @Override 652 public int getLineForVertical(int vertical) { 653 int high = mLineCount; 654 int low = -1; 655 int guess; 656 int[] lines = mLines; 657 while (high - low > 1) { 658 guess = (high + low) >> 1; 659 if (lines[mColumns * guess + TOP] > vertical){ 660 high = guess; 661 } else { 662 low = guess; 663 } 664 } 665 if (low < 0) { 666 return 0; 667 } else { 668 return low; 669 } 670 } 671 672 @Override 673 public int getLineCount() { 674 return mLineCount; 675 } 676 677 @Override 678 public int getLineTop(int line) { 679 int top = mLines[mColumns * line + TOP]; 680 if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount && 681 line != mLineCount) { 682 top += getBottomPadding(); 683 } 684 return top; 685 } 686 687 @Override 688 public int getLineDescent(int line) { 689 int descent = mLines[mColumns * line + DESCENT]; 690 if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended 691 line != mLineCount) { 692 descent += getBottomPadding(); 693 } 694 return descent; 695 } 696 697 @Override 698 public int getLineStart(int line) { 699 return mLines[mColumns * line + START] & START_MASK; 700 } 701 702 @Override 703 public int getParagraphDirection(int line) { 704 return mLines[mColumns * line + DIR] >> DIR_SHIFT; 705 } 706 707 @Override 708 public boolean getLineContainsTab(int line) { 709 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0; 710 } 711 712 @Override 713 public final Directions getLineDirections(int line) { 714 return mLineDirections[line]; 715 } 716 717 @Override 718 public int getTopPadding() { 719 return mTopPadding; 720 } 721 722 @Override 723 public int getBottomPadding() { 724 return mBottomPadding; 725 } 726 727 @Override 728 public int getEllipsisCount(int line) { 729 if (mColumns < COLUMNS_ELLIPSIZE) { 730 return 0; 731 } 732 733 return mLines[mColumns * line + ELLIPSIS_COUNT]; 734 } 735 736 @Override 737 public int getEllipsisStart(int line) { 738 if (mColumns < COLUMNS_ELLIPSIZE) { 739 return 0; 740 } 741 742 return mLines[mColumns * line + ELLIPSIS_START]; 743 } 744 745 @Override 746 public int getEllipsizedWidth() { 747 return mEllipsizedWidth; 748 } 749 750 void prepare() { 751 mMeasured = MeasuredText.obtain(); 752 } 753 754 void finish() { 755 mMeasured = MeasuredText.recycle(mMeasured); 756 } 757 758 // populates LineBreaks and returns the number of breaks found 759 // 760 // the arrays inside the LineBreaks objects are passed in as well 761 // to reduce the number of JNI calls in the common case where the 762 // arrays do not have to be resized 763 private static native int nComputeLineBreaks(String locale, char[] text, float[] widths, 764 int length, float firstWidth, int firstWidthLineCount, float restWidth, 765 int[] variableTabStops, int defaultTabStop, boolean optimize, LineBreaks recycle, 766 int[] recycleBreaks, float[] recycleWidths, boolean[] recycleFlags, int recycleLength); 767 768 private int mLineCount; 769 private int mTopPadding, mBottomPadding; 770 private int mColumns; 771 private int mEllipsizedWidth; 772 773 private static final int COLUMNS_NORMAL = 3; 774 private static final int COLUMNS_ELLIPSIZE = 5; 775 private static final int START = 0; 776 private static final int DIR = START; 777 private static final int TAB = START; 778 private static final int TOP = 1; 779 private static final int DESCENT = 2; 780 private static final int ELLIPSIS_START = 3; 781 private static final int ELLIPSIS_COUNT = 4; 782 783 private int[] mLines; 784 private Directions[] mLineDirections; 785 private int mMaximumVisibleLineCount = Integer.MAX_VALUE; 786 787 private static final int START_MASK = 0x1FFFFFFF; 788 private static final int DIR_SHIFT = 30; 789 private static final int TAB_MASK = 0x20000000; 790 791 private static final int TAB_INCREMENT = 20; // same as Layout, but that's private 792 793 private static final char CHAR_NEW_LINE = '\n'; 794 795 private static final double EXTRA_ROUNDING = 0.5; 796 797 /* 798 * This is reused across calls to generate() 799 */ 800 private MeasuredText mMeasured; 801 private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); 802 803 // This is used to return three arrays from a single JNI call when 804 // performing line breaking 805 /*package*/ static class LineBreaks { 806 private static final int INITIAL_SIZE = 16; 807 public int[] breaks = new int[INITIAL_SIZE]; 808 public float[] widths = new float[INITIAL_SIZE]; 809 public boolean[] flags = new boolean[INITIAL_SIZE]; // hasTabOrEmoji 810 // breaks, widths, and flags should all have the same length 811 } 812 813} 814