StaticLayout.java revision 34a126e51aaf22e32c7af808ec6b5a0c41ae3311
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; 29 30/** 31 * StaticLayout is a Layout for text that will not be edited after it 32 * is laid out. Use {@link DynamicLayout} for text that may change. 33 * <p>This is used by widgets to control text layout. You should not need 34 * to use this class directly unless you are implementing your own widget 35 * or custom display object, or would be tempted to call 36 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, 37 * float, float, android.graphics.Paint) 38 * Canvas.drawText()} directly.</p> 39 */ 40public class StaticLayout extends Layout { 41 42 static final String TAG = "StaticLayout"; 43 44 public StaticLayout(CharSequence source, TextPaint paint, 45 int width, 46 Alignment align, float spacingmult, float spacingadd, 47 boolean includepad) { 48 this(source, 0, source.length(), paint, width, align, 49 spacingmult, spacingadd, includepad); 50 } 51 52 /** 53 * @hide 54 */ 55 public StaticLayout(CharSequence source, TextPaint paint, 56 int width, Alignment align, TextDirectionHeuristic textDir, 57 float spacingmult, float spacingadd, 58 boolean includepad) { 59 this(source, 0, source.length(), paint, width, align, textDir, 60 spacingmult, spacingadd, includepad); 61 } 62 63 public StaticLayout(CharSequence source, int bufstart, int bufend, 64 TextPaint paint, int outerwidth, 65 Alignment align, 66 float spacingmult, float spacingadd, 67 boolean includepad) { 68 this(source, bufstart, bufend, paint, outerwidth, align, 69 spacingmult, spacingadd, includepad, null, 0); 70 } 71 72 /** 73 * @hide 74 */ 75 public StaticLayout(CharSequence source, int bufstart, int bufend, 76 TextPaint paint, int outerwidth, 77 Alignment align, TextDirectionHeuristic textDir, 78 float spacingmult, float spacingadd, 79 boolean includepad) { 80 this(source, bufstart, bufend, paint, outerwidth, align, textDir, 81 spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE); 82} 83 84 public StaticLayout(CharSequence source, int bufstart, int bufend, 85 TextPaint paint, int outerwidth, 86 Alignment align, 87 float spacingmult, float spacingadd, 88 boolean includepad, 89 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 90 this(source, bufstart, bufend, paint, outerwidth, align, 91 TextDirectionHeuristics.FIRSTSTRONG_LTR, 92 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE); 93 } 94 95 /** 96 * @hide 97 */ 98 public StaticLayout(CharSequence source, int bufstart, int bufend, 99 TextPaint paint, int outerwidth, 100 Alignment align, TextDirectionHeuristic textDir, 101 float spacingmult, float spacingadd, 102 boolean includepad, 103 TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) { 104 super((ellipsize == null) 105 ? source 106 : (source instanceof Spanned) 107 ? new SpannedEllipsizer(source) 108 : new Ellipsizer(source), 109 paint, outerwidth, align, textDir, spacingmult, spacingadd); 110 111 /* 112 * This is annoying, but we can't refer to the layout until 113 * superclass construction is finished, and the superclass 114 * constructor wants the reference to the display text. 115 * 116 * This will break if the superclass constructor ever actually 117 * cares about the content instead of just holding the reference. 118 */ 119 if (ellipsize != null) { 120 Ellipsizer e = (Ellipsizer) getText(); 121 122 e.mLayout = this; 123 e.mWidth = ellipsizedWidth; 124 e.mMethod = ellipsize; 125 mEllipsizedWidth = ellipsizedWidth; 126 127 mColumns = COLUMNS_ELLIPSIZE; 128 } else { 129 mColumns = COLUMNS_NORMAL; 130 mEllipsizedWidth = outerwidth; 131 } 132 133 mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; 134 mLineDirections = new Directions[ 135 ArrayUtils.idealIntArraySize(2 * mColumns)]; 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 mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; 153 mLineDirections = new Directions[ 154 ArrayUtils.idealIntArraySize(2 * mColumns)]; 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 mLineCount = 0; 165 166 int v = 0; 167 boolean needMultiply = (spacingmult != 1 || spacingadd != 0); 168 169 Paint.FontMetricsInt fm = mFontMetricsInt; 170 int[] chooseHtv = null; 171 172 MeasuredText measured = mMeasured; 173 174 Spanned spanned = null; 175 if (source instanceof Spanned) 176 spanned = (Spanned) source; 177 178 int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX 179 180 int paraEnd; 181 for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) { 182 paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd); 183 if (paraEnd < 0) 184 paraEnd = bufEnd; 185 else 186 paraEnd++; 187 188 int firstWidthLineLimit = mLineCount + 1; 189 int firstWidth = outerWidth; 190 int restWidth = outerWidth; 191 192 LineHeightSpan[] chooseHt = null; 193 194 if (spanned != null) { 195 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd, 196 LeadingMarginSpan.class); 197 for (int i = 0; i < sp.length; i++) { 198 LeadingMarginSpan lms = sp[i]; 199 firstWidth -= sp[i].getLeadingMargin(true); 200 restWidth -= sp[i].getLeadingMargin(false); 201 202 // LeadingMarginSpan2 is odd. The count affects all 203 // leading margin spans, not just this particular one, 204 // and start from the top of the span, not the top of the 205 // paragraph. 206 if (lms instanceof LeadingMarginSpan2) { 207 LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms; 208 int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2)); 209 firstWidthLineLimit = 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 = new int[ArrayUtils.idealIntArraySize( 219 chooseHt.length)]; 220 } 221 222 for (int i = 0; i < chooseHt.length; i++) { 223 int o = spanned.getSpanStart(chooseHt[i]); 224 225 if (o < paraStart) { 226 // starts in this layout, before the 227 // current paragraph 228 229 chooseHtv[i] = getLineTop(getLineForOffset(o)); 230 } else { 231 // starts in this paragraph 232 233 chooseHtv[i] = v; 234 } 235 } 236 } 237 } 238 239 measured.setPara(source, paraStart, paraEnd, textDir); 240 char[] chs = measured.mChars; 241 float[] widths = measured.mWidths; 242 byte[] chdirs = measured.mLevels; 243 int dir = measured.mDir; 244 boolean easy = measured.mEasy; 245 246 int width = firstWidth; 247 248 float w = 0; 249 int here = paraStart; 250 251 int ok = paraStart; 252 float okWidth = w; 253 int okAscent = 0, okDescent = 0, okTop = 0, okBottom = 0; 254 255 int fit = paraStart; 256 float fitWidth = w; 257 int fitAscent = 0, fitDescent = 0, fitTop = 0, fitBottom = 0; 258 259 boolean hasTabOrEmoji = false; 260 boolean hasTab = false; 261 TabStops tabStops = null; 262 263 for (int spanStart = paraStart, spanEnd = spanStart, nextSpanStart; 264 spanStart < paraEnd; spanStart = nextSpanStart) { 265 266 if (spanStart == spanEnd) { 267 if (spanned == null) 268 spanEnd = paraEnd; 269 else 270 spanEnd = spanned.nextSpanTransition(spanStart, paraEnd, 271 MetricAffectingSpan.class); 272 273 int spanLen = spanEnd - spanStart; 274 if (spanned == null) { 275 measured.addStyleRun(paint, spanLen, fm); 276 } else { 277 MetricAffectingSpan[] spans = 278 spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); 279 spans = TextUtils.removeEmptySpans(spans, spanned, 280 MetricAffectingSpan.class); 281 measured.addStyleRun(paint, spans, spanLen, fm); 282 } 283 } 284 285 nextSpanStart = spanEnd; 286 287 int fmTop = fm.top; 288 int fmBottom = fm.bottom; 289 int fmAscent = fm.ascent; 290 int fmDescent = fm.descent; 291 292 for (int j = spanStart; j < spanEnd; j++) { 293 char c = chs[j - paraStart]; 294 295 if (c == CHAR_NEW_LINE) { 296 // intentionally left empty 297 } else if (c == CHAR_TAB) { 298 if (hasTab == false) { 299 hasTab = true; 300 hasTabOrEmoji = true; 301 if (spanned != null) { 302 // First tab this para, check for tabstops 303 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, 304 paraEnd, TabStopSpan.class); 305 if (spans.length > 0) { 306 tabStops = new TabStops(TAB_INCREMENT, spans); 307 } 308 } 309 } 310 if (tabStops != null) { 311 w = tabStops.nextTab(w); 312 } else { 313 w = TabStops.nextDefaultStop(w, TAB_INCREMENT); 314 } 315 } else if (c >= CHAR_FIRST_HIGH_SURROGATE && c <= CHAR_LAST_LOW_SURROGATE 316 && j + 1 < spanEnd) { 317 int emoji = Character.codePointAt(chs, j - paraStart); 318 319 if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { 320 Bitmap bm = EMOJI_FACTORY.getBitmapFromAndroidPua(emoji); 321 322 if (bm != null) { 323 Paint whichPaint; 324 325 if (spanned == null) { 326 whichPaint = paint; 327 } else { 328 whichPaint = mWorkPaint; 329 } 330 331 float wid = bm.getWidth() * -whichPaint.ascent() / bm.getHeight(); 332 333 w += wid; 334 hasTabOrEmoji = true; 335 j++; 336 } else { 337 w += widths[j - paraStart]; 338 } 339 } else { 340 w += widths[j - paraStart]; 341 } 342 } else { 343 w += widths[j - paraStart]; 344 } 345 346 // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width); 347 348 if (w <= width) { 349 fitWidth = w; 350 fit = j + 1; 351 352 if (fmTop < fitTop) 353 fitTop = fmTop; 354 if (fmAscent < fitAscent) 355 fitAscent = fmAscent; 356 if (fmDescent > fitDescent) 357 fitDescent = fmDescent; 358 if (fmBottom > fitBottom) 359 fitBottom = fmBottom; 360 361 /* 362 * From the Unicode Line Breaking Algorithm: 363 * (at least approximately) 364 * 365 * .,:; are class IS: breakpoints 366 * except when adjacent to digits 367 * / is class SY: a breakpoint 368 * except when followed by a digit. 369 * - is class HY: a breakpoint 370 * except when followed by a digit. 371 * 372 * Ideographs are class ID: breakpoints when adjacent, 373 * except for NS (non-starters), which can be broken 374 * after but not before. 375 */ 376 377 if (c == CHAR_SPACE || c == CHAR_TAB || 378 ((c == CHAR_DOT || c == CHAR_COMMA || 379 c == CHAR_COLON || c == CHAR_SEMICOLON) && 380 (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) && 381 (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || 382 ((c == CHAR_SLASH || c == CHAR_HYPHEN) && 383 (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || 384 (c >= CHAR_FIRST_CJK && isIdeographic(c, true) && 385 j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) { 386 okWidth = w; 387 ok = j + 1; 388 389 if (fitTop < okTop) 390 okTop = fitTop; 391 if (fitAscent < okAscent) 392 okAscent = fitAscent; 393 if (fitDescent > okDescent) 394 okDescent = fitDescent; 395 if (fitBottom > okBottom) 396 okBottom = fitBottom; 397 } 398 } else { 399 final boolean moreChars = (j + 1 < spanEnd); 400 int endPos; 401 int above, below, top, bottom; 402 float currentTextWidth; 403 404 if (ok != here) { 405 // If it is a space that makes the length exceed width, cut here 406 if (c == CHAR_SPACE) ok = j + 1; 407 408 while (ok < spanEnd && chs[ok - paraStart] == CHAR_SPACE) { 409 ok++; 410 } 411 412 endPos = ok; 413 above = okAscent; 414 below = okDescent; 415 top = okTop; 416 bottom = okBottom; 417 currentTextWidth = okWidth; 418 } else if (fit != here) { 419 endPos = fit; 420 above = fitAscent; 421 below = fitDescent; 422 top = fitTop; 423 bottom = fitBottom; 424 currentTextWidth = fitWidth; 425 } else { 426 endPos = here + 1; 427 above = fm.ascent; 428 below = fm.descent; 429 top = fm.top; 430 bottom = fm.bottom; 431 currentTextWidth = widths[here - paraStart]; 432 } 433 434 v = out(source, here, endPos, 435 above, below, top, bottom, 436 v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, hasTabOrEmoji, 437 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad, 438 chs, widths, paraStart, ellipsize, ellipsizedWidth, 439 currentTextWidth, paint, moreChars); 440 here = endPos; 441 442 if (here < spanStart) { 443 // didn't output all the text for this span 444 // we've measured the raw widths, though, so 445 // just reset the start point 446 j = nextSpanStart = here; 447 } else { 448 j = here - 1; // continue looping 449 } 450 451 ok = fit = here; 452 w = 0; 453 fitAscent = fitDescent = fitTop = fitBottom = 0; 454 okAscent = okDescent = okTop = okBottom = 0; 455 456 if (--firstWidthLineLimit <= 0) { 457 width = restWidth; 458 } 459 } 460 if (mLineCount >= mMaximumVisibleLineCount) { 461 break; 462 } 463 } 464 } 465 466 if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) { 467 if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) { 468 paint.getFontMetricsInt(fm); 469 470 fitTop = fm.top; 471 fitBottom = fm.bottom; 472 fitAscent = fm.ascent; 473 fitDescent = fm.descent; 474 } 475 476 // Log.e("text", "output rest " + here + " to " + end); 477 478 v = out(source, 479 here, paraEnd, fitAscent, fitDescent, 480 fitTop, fitBottom, 481 v, 482 spacingmult, spacingadd, chooseHt, 483 chooseHtv, fm, hasTabOrEmoji, 484 needMultiply, chdirs, dir, easy, bufEnd, 485 includepad, trackpad, chs, 486 widths, paraStart, ellipsize, 487 ellipsizedWidth, w, paint, paraEnd != bufEnd); 488 } 489 490 paraStart = paraEnd; 491 492 if (paraEnd == bufEnd) 493 break; 494 } 495 496 if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) && 497 mLineCount < mMaximumVisibleLineCount) { 498 // Log.e("text", "output last " + bufEnd); 499 500 paint.getFontMetricsInt(fm); 501 502 v = out(source, 503 bufEnd, bufEnd, fm.ascent, fm.descent, 504 fm.top, fm.bottom, 505 v, 506 spacingmult, spacingadd, null, 507 null, fm, false, 508 needMultiply, null, DEFAULT_DIR, true, bufEnd, 509 includepad, trackpad, null, 510 null, bufStart, ellipsize, 511 ellipsizedWidth, 0, paint, false); 512 } 513 } 514 515 /** 516 * Returns true if the specified character is one of those specified 517 * as being Ideographic (class ID) by the Unicode Line Breaking Algorithm 518 * (http://www.unicode.org/unicode/reports/tr14/), and is therefore OK 519 * to break between a pair of. 520 * 521 * @param includeNonStarters also return true for category NS 522 * (non-starters), which can be broken 523 * after but not before. 524 */ 525 private static final boolean isIdeographic(char c, boolean includeNonStarters) { 526 if (c >= '\u2E80' && c <= '\u2FFF') { 527 return true; // CJK, KANGXI RADICALS, DESCRIPTION SYMBOLS 528 } 529 if (c == '\u3000') { 530 return true; // IDEOGRAPHIC SPACE 531 } 532 if (c >= '\u3040' && c <= '\u309F') { 533 if (!includeNonStarters) { 534 switch (c) { 535 case '\u3041': // # HIRAGANA LETTER SMALL A 536 case '\u3043': // # HIRAGANA LETTER SMALL I 537 case '\u3045': // # HIRAGANA LETTER SMALL U 538 case '\u3047': // # HIRAGANA LETTER SMALL E 539 case '\u3049': // # HIRAGANA LETTER SMALL O 540 case '\u3063': // # HIRAGANA LETTER SMALL TU 541 case '\u3083': // # HIRAGANA LETTER SMALL YA 542 case '\u3085': // # HIRAGANA LETTER SMALL YU 543 case '\u3087': // # HIRAGANA LETTER SMALL YO 544 case '\u308E': // # HIRAGANA LETTER SMALL WA 545 case '\u3095': // # HIRAGANA LETTER SMALL KA 546 case '\u3096': // # HIRAGANA LETTER SMALL KE 547 case '\u309B': // # KATAKANA-HIRAGANA VOICED SOUND MARK 548 case '\u309C': // # KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK 549 case '\u309D': // # HIRAGANA ITERATION MARK 550 case '\u309E': // # HIRAGANA VOICED ITERATION MARK 551 return false; 552 } 553 } 554 return true; // Hiragana (except small characters) 555 } 556 if (c >= '\u30A0' && c <= '\u30FF') { 557 if (!includeNonStarters) { 558 switch (c) { 559 case '\u30A0': // # KATAKANA-HIRAGANA DOUBLE HYPHEN 560 case '\u30A1': // # KATAKANA LETTER SMALL A 561 case '\u30A3': // # KATAKANA LETTER SMALL I 562 case '\u30A5': // # KATAKANA LETTER SMALL U 563 case '\u30A7': // # KATAKANA LETTER SMALL E 564 case '\u30A9': // # KATAKANA LETTER SMALL O 565 case '\u30C3': // # KATAKANA LETTER SMALL TU 566 case '\u30E3': // # KATAKANA LETTER SMALL YA 567 case '\u30E5': // # KATAKANA LETTER SMALL YU 568 case '\u30E7': // # KATAKANA LETTER SMALL YO 569 case '\u30EE': // # KATAKANA LETTER SMALL WA 570 case '\u30F5': // # KATAKANA LETTER SMALL KA 571 case '\u30F6': // # KATAKANA LETTER SMALL KE 572 case '\u30FB': // # KATAKANA MIDDLE DOT 573 case '\u30FC': // # KATAKANA-HIRAGANA PROLONGED SOUND MARK 574 case '\u30FD': // # KATAKANA ITERATION MARK 575 case '\u30FE': // # KATAKANA VOICED ITERATION MARK 576 return false; 577 } 578 } 579 return true; // Katakana (except small characters) 580 } 581 if (c >= '\u3400' && c <= '\u4DB5') { 582 return true; // CJK UNIFIED IDEOGRAPHS EXTENSION A 583 } 584 if (c >= '\u4E00' && c <= '\u9FBB') { 585 return true; // CJK UNIFIED IDEOGRAPHS 586 } 587 if (c >= '\uF900' && c <= '\uFAD9') { 588 return true; // CJK COMPATIBILITY IDEOGRAPHS 589 } 590 if (c >= '\uA000' && c <= '\uA48F') { 591 return true; // YI SYLLABLES 592 } 593 if (c >= '\uA490' && c <= '\uA4CF') { 594 return true; // YI RADICALS 595 } 596 if (c >= '\uFE62' && c <= '\uFE66') { 597 return true; // SMALL PLUS SIGN to SMALL EQUALS SIGN 598 } 599 if (c >= '\uFF10' && c <= '\uFF19') { 600 return true; // WIDE DIGITS 601 } 602 603 return false; 604 } 605 606 private int out(CharSequence text, int start, int end, 607 int above, int below, int top, int bottom, int v, 608 float spacingmult, float spacingadd, 609 LineHeightSpan[] chooseHt, int[] chooseHtv, 610 Paint.FontMetricsInt fm, boolean hasTabOrEmoji, 611 boolean needMultiply, byte[] chdirs, int dir, 612 boolean easy, int bufEnd, boolean includePad, 613 boolean trackPad, char[] chs, 614 float[] widths, int widthStart, TextUtils.TruncateAt ellipsize, 615 float ellipsisWidth, float textWidth, 616 TextPaint paint, boolean moreChars) { 617 int j = mLineCount; 618 int off = j * mColumns; 619 int want = off + mColumns + TOP; 620 int[] lines = mLines; 621 622 if (want >= lines.length) { 623 int nlen = ArrayUtils.idealIntArraySize(want + 1); 624 int[] grow = new int[nlen]; 625 System.arraycopy(lines, 0, grow, 0, lines.length); 626 mLines = grow; 627 lines = grow; 628 629 Directions[] grow2 = new Directions[nlen]; 630 System.arraycopy(mLineDirections, 0, grow2, 0, 631 mLineDirections.length); 632 mLineDirections = grow2; 633 } 634 635 if (chooseHt != null) { 636 fm.ascent = above; 637 fm.descent = below; 638 fm.top = top; 639 fm.bottom = bottom; 640 641 for (int i = 0; i < chooseHt.length; i++) { 642 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { 643 ((LineHeightSpan.WithDensity) chooseHt[i]). 644 chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); 645 646 } else { 647 chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); 648 } 649 } 650 651 above = fm.ascent; 652 below = fm.descent; 653 top = fm.top; 654 bottom = fm.bottom; 655 } 656 657 if (j == 0) { 658 if (trackPad) { 659 mTopPadding = top - above; 660 } 661 662 if (includePad) { 663 above = top; 664 } 665 } 666 if (end == bufEnd) { 667 if (trackPad) { 668 mBottomPadding = bottom - below; 669 } 670 671 if (includePad) { 672 below = bottom; 673 } 674 } 675 676 int extra; 677 678 if (needMultiply) { 679 double ex = (below - above) * (spacingmult - 1) + spacingadd; 680 if (ex >= 0) { 681 extra = (int)(ex + EXTRA_ROUNDING); 682 } else { 683 extra = -(int)(-ex + EXTRA_ROUNDING); 684 } 685 } else { 686 extra = 0; 687 } 688 689 lines[off + START] = start; 690 lines[off + TOP] = v; 691 lines[off + DESCENT] = below + extra; 692 693 v += (below - above) + extra; 694 lines[off + mColumns + START] = end; 695 lines[off + mColumns + TOP] = v; 696 697 if (hasTabOrEmoji) 698 lines[off + TAB] |= TAB_MASK; 699 700 lines[off + DIR] |= dir << DIR_SHIFT; 701 Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; 702 // easy means all chars < the first RTL, so no emoji, no nothing 703 // XXX a run with no text or all spaces is easy but might be an empty 704 // RTL paragraph. Make sure easy is false if this is the case. 705 if (easy) { 706 mLineDirections[j] = linedirs; 707 } else { 708 mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs, 709 start - widthStart, end - start); 710 } 711 712 if (ellipsize != null) { 713 // If there is only one line, then do any type of ellipsis except when it is MARQUEE 714 // if there are multiple lines, just allow END ellipsis on the last line 715 boolean firstLine = (j == 0); 716 boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); 717 boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount); 718 719 boolean doEllipsis = 720 (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) && 721 ellipsize != TextUtils.TruncateAt.MARQUEE) || 722 (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) && 723 ellipsize == TextUtils.TruncateAt.END); 724 if (doEllipsis) { 725 calculateEllipsis(start, end, widths, widthStart, 726 ellipsisWidth, ellipsize, j, 727 textWidth, paint, forceEllipsis); 728 } 729 } 730 731 mLineCount++; 732 return v; 733 } 734 735 private void calculateEllipsis(int lineStart, int lineEnd, 736 float[] widths, int widthStart, 737 float avail, TextUtils.TruncateAt where, 738 int line, float textWidth, TextPaint paint, 739 boolean forceEllipsis) { 740 if (textWidth <= avail && !forceEllipsis) { 741 // Everything fits! 742 mLines[mColumns * line + ELLIPSIS_START] = 0; 743 mLines[mColumns * line + ELLIPSIS_COUNT] = 0; 744 return; 745 } 746 747 float ellipsisWidth = paint.measureText( 748 (where == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL); 749 int ellipsisStart = 0; 750 int ellipsisCount = 0; 751 int len = lineEnd - lineStart; 752 753 // We only support start ellipsis on a single line 754 if (where == TextUtils.TruncateAt.START) { 755 if (mMaximumVisibleLineCount == 1) { 756 float sum = 0; 757 int i; 758 759 for (i = len; i >= 0; i--) { 760 float w = widths[i - 1 + lineStart - widthStart]; 761 762 if (w + sum + ellipsisWidth > avail) { 763 break; 764 } 765 766 sum += w; 767 } 768 769 ellipsisStart = 0; 770 ellipsisCount = i; 771 } else { 772 if (Log.isLoggable(TAG, Log.WARN)) { 773 Log.w(TAG, "Start Ellipsis only supported with one line"); 774 } 775 } 776 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE || 777 where == TextUtils.TruncateAt.END_SMALL) { 778 float sum = 0; 779 int i; 780 781 for (i = 0; i < len; i++) { 782 float w = widths[i + lineStart - widthStart]; 783 784 if (w + sum + ellipsisWidth > avail) { 785 break; 786 } 787 788 sum += w; 789 } 790 791 ellipsisStart = i; 792 ellipsisCount = len - i; 793 if (forceEllipsis && ellipsisCount == 0 && len > 0) { 794 ellipsisStart = len - 1; 795 ellipsisCount = 1; 796 } 797 } else { 798 // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line 799 if (mMaximumVisibleLineCount == 1) { 800 float lsum = 0, rsum = 0; 801 int left = 0, right = len; 802 803 float ravail = (avail - ellipsisWidth) / 2; 804 for (right = len; right >= 0; right--) { 805 float w = widths[right - 1 + lineStart - widthStart]; 806 807 if (w + rsum > ravail) { 808 break; 809 } 810 811 rsum += w; 812 } 813 814 float lavail = avail - ellipsisWidth - rsum; 815 for (left = 0; left < right; left++) { 816 float w = widths[left + lineStart - widthStart]; 817 818 if (w + lsum > lavail) { 819 break; 820 } 821 822 lsum += w; 823 } 824 825 ellipsisStart = left; 826 ellipsisCount = right - left; 827 } else { 828 if (Log.isLoggable(TAG, Log.WARN)) { 829 Log.w(TAG, "Middle Ellipsis only supported with one line"); 830 } 831 } 832 } 833 834 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; 835 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; 836 } 837 838 // Override the base class so we can directly access our members, 839 // rather than relying on member functions. 840 // The logic mirrors that of Layout.getLineForVertical 841 // FIXME: It may be faster to do a linear search for layouts without many lines. 842 @Override 843 public int getLineForVertical(int vertical) { 844 int high = mLineCount; 845 int low = -1; 846 int guess; 847 int[] lines = mLines; 848 while (high - low > 1) { 849 guess = (high + low) >> 1; 850 if (lines[mColumns * guess + TOP] > vertical){ 851 high = guess; 852 } else { 853 low = guess; 854 } 855 } 856 if (low < 0) { 857 return 0; 858 } else { 859 return low; 860 } 861 } 862 863 @Override 864 public int getLineCount() { 865 return mLineCount; 866 } 867 868 @Override 869 public int getLineTop(int line) { 870 int top = mLines[mColumns * line + TOP]; 871 if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount && 872 line != mLineCount) { 873 top += getBottomPadding(); 874 } 875 return top; 876 } 877 878 @Override 879 public int getLineDescent(int line) { 880 int descent = mLines[mColumns * line + DESCENT]; 881 if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended 882 line != mLineCount) { 883 descent += getBottomPadding(); 884 } 885 return descent; 886 } 887 888 @Override 889 public int getLineStart(int line) { 890 return mLines[mColumns * line + START] & START_MASK; 891 } 892 893 @Override 894 public int getParagraphDirection(int line) { 895 return mLines[mColumns * line + DIR] >> DIR_SHIFT; 896 } 897 898 @Override 899 public boolean getLineContainsTab(int line) { 900 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0; 901 } 902 903 @Override 904 public final Directions getLineDirections(int line) { 905 return mLineDirections[line]; 906 } 907 908 @Override 909 public int getTopPadding() { 910 return mTopPadding; 911 } 912 913 @Override 914 public int getBottomPadding() { 915 return mBottomPadding; 916 } 917 918 @Override 919 public int getEllipsisCount(int line) { 920 if (mColumns < COLUMNS_ELLIPSIZE) { 921 return 0; 922 } 923 924 return mLines[mColumns * line + ELLIPSIS_COUNT]; 925 } 926 927 @Override 928 public int getEllipsisStart(int line) { 929 if (mColumns < COLUMNS_ELLIPSIZE) { 930 return 0; 931 } 932 933 return mLines[mColumns * line + ELLIPSIS_START]; 934 } 935 936 @Override 937 public int getEllipsizedWidth() { 938 return mEllipsizedWidth; 939 } 940 941 void prepare() { 942 mMeasured = MeasuredText.obtain(); 943 } 944 945 void finish() { 946 mMeasured = MeasuredText.recycle(mMeasured); 947 } 948 949 private int mLineCount; 950 private int mTopPadding, mBottomPadding; 951 private int mColumns; 952 private int mEllipsizedWidth; 953 954 private static final int COLUMNS_NORMAL = 3; 955 private static final int COLUMNS_ELLIPSIZE = 5; 956 private static final int START = 0; 957 private static final int DIR = START; 958 private static final int TAB = START; 959 private static final int TOP = 1; 960 private static final int DESCENT = 2; 961 private static final int ELLIPSIS_START = 3; 962 private static final int ELLIPSIS_COUNT = 4; 963 964 private int[] mLines; 965 private Directions[] mLineDirections; 966 private int mMaximumVisibleLineCount = Integer.MAX_VALUE; 967 968 private static final int START_MASK = 0x1FFFFFFF; 969 private static final int DIR_SHIFT = 30; 970 private static final int TAB_MASK = 0x20000000; 971 972 private static final int TAB_INCREMENT = 20; // same as Layout, but that's private 973 974 private static final char CHAR_FIRST_CJK = '\u2E80'; 975 976 private static final char CHAR_NEW_LINE = '\n'; 977 private static final char CHAR_TAB = '\t'; 978 private static final char CHAR_SPACE = ' '; 979 private static final char CHAR_DOT = '.'; 980 private static final char CHAR_COMMA = ','; 981 private static final char CHAR_COLON = ':'; 982 private static final char CHAR_SEMICOLON = ';'; 983 private static final char CHAR_SLASH = '/'; 984 private static final char CHAR_HYPHEN = '-'; 985 986 private static final double EXTRA_ROUNDING = 0.5; 987 988 private static final String ELLIPSIS_NORMAL = "\u2026"; // this is "..." 989 private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // this is ".." 990 991 private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800; 992 private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF; 993 994 /* 995 * This is reused across calls to generate() 996 */ 997 private MeasuredText mMeasured; 998 private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); 999} 1000