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