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