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