StaticLayout.java revision 74d31ef2b2c42b54fa1f7cf94ea955ea67ab69a0
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.LineHeightSpan; 25import android.text.style.MetricAffectingSpan; 26import android.text.style.TabStopSpan; 27import android.text.style.LeadingMarginSpan.LeadingMarginSpan2; 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 40StaticLayout 41extends Layout 42{ 43 public StaticLayout(CharSequence source, TextPaint paint, 44 int width, 45 Alignment align, float spacingmult, float spacingadd, 46 boolean includepad) { 47 this(source, 0, source.length(), paint, width, align, 48 spacingmult, spacingadd, includepad); 49 } 50 51 public StaticLayout(CharSequence source, int bufstart, int bufend, 52 TextPaint paint, int outerwidth, 53 Alignment align, 54 float spacingmult, float spacingadd, 55 boolean includepad) { 56 this(source, bufstart, bufend, paint, outerwidth, align, 57 spacingmult, spacingadd, includepad, null, 0); 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 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 66 super((ellipsize == null) 67 ? source 68 : (source instanceof Spanned) 69 ? new SpannedEllipsizer(source) 70 : new Ellipsizer(source), 71 paint, outerwidth, align, spacingmult, spacingadd); 72 73 /* 74 * This is annoying, but we can't refer to the layout until 75 * superclass construction is finished, and the superclass 76 * constructor wants the reference to the display text. 77 * 78 * This will break if the superclass constructor ever actually 79 * cares about the content instead of just holding the reference. 80 */ 81 if (ellipsize != null) { 82 Ellipsizer e = (Ellipsizer) getText(); 83 84 e.mLayout = this; 85 e.mWidth = ellipsizedWidth; 86 e.mMethod = ellipsize; 87 mEllipsizedWidth = ellipsizedWidth; 88 89 mColumns = COLUMNS_ELLIPSIZE; 90 } else { 91 mColumns = COLUMNS_NORMAL; 92 mEllipsizedWidth = outerwidth; 93 } 94 95 mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; 96 mLineDirections = new Directions[ 97 ArrayUtils.idealIntArraySize(2 * mColumns)]; 98 99 mMeasured = MeasuredText.obtain(); 100 101 generate(source, bufstart, bufend, paint, outerwidth, align, 102 spacingmult, spacingadd, includepad, includepad, 103 ellipsize != null, ellipsizedWidth, ellipsize); 104 105 mMeasured = MeasuredText.recycle(mMeasured); 106 mFontMetricsInt = null; 107 } 108 109 /* package */ StaticLayout(boolean ellipsize) { 110 super(null, null, 0, null, 0, 0); 111 112 mColumns = COLUMNS_ELLIPSIZE; 113 mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)]; 114 mLineDirections = new Directions[ 115 ArrayUtils.idealIntArraySize(2 * mColumns)]; 116 mMeasured = MeasuredText.obtain(); 117 } 118 119 /* package */ void generate(CharSequence source, int bufstart, int bufend, 120 TextPaint paint, int outerwidth, 121 Alignment align, 122 float spacingmult, float spacingadd, 123 boolean includepad, boolean trackpad, 124 boolean breakOnlyAtSpaces, 125 float ellipsizedWidth, TextUtils.TruncateAt where) { 126 mLineCount = 0; 127 128 int v = 0; 129 boolean needMultiply = (spacingmult != 1 || spacingadd != 0); 130 131 Paint.FontMetricsInt fm = mFontMetricsInt; 132 int[] choosehtv = null; 133 134 MeasuredText measured = mMeasured; 135 136 Spanned spanned = null; 137 if (source instanceof Spanned) 138 spanned = (Spanned) source; 139 140 int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX 141 142 int paraEnd; 143 for (int paraStart = bufstart; paraStart <= bufend; paraStart = paraEnd) { 144 paraEnd = TextUtils.indexOf(source, '\n', paraStart, bufend); 145 if (paraEnd < 0) 146 paraEnd = bufend; 147 else 148 paraEnd++; 149 int paraLen = paraEnd - paraStart; 150 151 int firstWidthLineLimit = mLineCount + 1; 152 int firstwidth = outerwidth; 153 int restwidth = outerwidth; 154 155 LineHeightSpan[] chooseht = null; 156 157 if (spanned != null) { 158 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd, 159 LeadingMarginSpan.class); 160 for (int i = 0; i < sp.length; i++) { 161 LeadingMarginSpan lms = sp[i]; 162 firstwidth -= sp[i].getLeadingMargin(true); 163 restwidth -= sp[i].getLeadingMargin(false); 164 165 // LeadingMarginSpan2 is odd. The count affects all 166 // leading margin spans, not just this particular one, 167 // and start from the top of the span, not the top of the 168 // paragraph. 169 if (lms instanceof LeadingMarginSpan2) { 170 LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms; 171 int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2)); 172 firstWidthLineLimit = lmsFirstLine + 173 lms2.getLeadingMarginLineCount(); 174 } 175 } 176 177 chooseht = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class); 178 179 if (chooseht.length != 0) { 180 if (choosehtv == null || 181 choosehtv.length < chooseht.length) { 182 choosehtv = new int[ArrayUtils.idealIntArraySize( 183 chooseht.length)]; 184 } 185 186 for (int i = 0; i < chooseht.length; i++) { 187 int o = spanned.getSpanStart(chooseht[i]); 188 189 if (o < paraStart) { 190 // starts in this layout, before the 191 // current paragraph 192 193 choosehtv[i] = getLineTop(getLineForOffset(o)); 194 } else { 195 // starts in this paragraph 196 197 choosehtv[i] = v; 198 } 199 } 200 } 201 } 202 203 measured.setPara(source, paraStart, paraEnd, DIR_REQUEST_DEFAULT_LTR); 204 char[] chs = measured.mChars; 205 float[] widths = measured.mWidths; 206 byte[] chdirs = measured.mLevels; 207 int dir = measured.mDir; 208 boolean easy = measured.mEasy; 209 210 CharSequence sub = source; 211 212 int width = firstwidth; 213 214 float w = 0; 215 int here = paraStart; 216 217 int ok = paraStart; 218 float okwidth = w; 219 int okascent = 0, okdescent = 0, oktop = 0, okbottom = 0; 220 221 int fit = paraStart; 222 float fitwidth = w; 223 int fitascent = 0, fitdescent = 0, fittop = 0, fitbottom = 0; 224 225 boolean hasTabOrEmoji = false; 226 boolean hasTab = false; 227 TabStops tabStops = null; 228 229 for (int spanStart = paraStart, spanEnd = spanStart, nextSpanStart; 230 spanStart < paraEnd; spanStart = nextSpanStart) { 231 232 if (spanStart == spanEnd) { 233 if (spanned == null) 234 spanEnd = paraEnd; 235 else 236 spanEnd = spanned.nextSpanTransition(spanStart, paraEnd, 237 MetricAffectingSpan.class); 238 239 int spanLen = spanEnd - spanStart; 240 if (spanned == null) { 241 measured.addStyleRun(paint, spanLen, fm); 242 } else { 243 MetricAffectingSpan[] spans = 244 spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); 245 measured.addStyleRun(paint, spans, spanLen, fm); 246 } 247 } 248 249 nextSpanStart = spanEnd; 250 int startInPara = spanStart - paraStart; 251 int endInPara = spanEnd - paraStart; 252 253 int fmtop = fm.top; 254 int fmbottom = fm.bottom; 255 int fmascent = fm.ascent; 256 int fmdescent = fm.descent; 257 258 for (int j = spanStart; j < spanEnd; j++) { 259 char c = chs[j - paraStart]; 260 float before = w; 261 262 if (c == '\n') { 263 ; 264 } else if (c == '\t') { 265 if (hasTab == false) { 266 hasTab = true; 267 hasTabOrEmoji = true; 268 if (spanned != null) { 269 // First tab this para, check for tabstops 270 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, 271 paraEnd, TabStopSpan.class); 272 if (spans.length > 0) { 273 tabStops = new TabStops(TAB_INCREMENT, spans); 274 } 275 } 276 } 277 if (tabStops != null) { 278 w = tabStops.nextTab(w); 279 } else { 280 w = TabStops.nextDefaultStop(w, TAB_INCREMENT); 281 } 282 } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < spanEnd) { 283 int emoji = Character.codePointAt(chs, j - paraStart); 284 285 if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) { 286 Bitmap bm = EMOJI_FACTORY. 287 getBitmapFromAndroidPua(emoji); 288 289 if (bm != null) { 290 Paint whichPaint; 291 292 if (spanned == null) { 293 whichPaint = paint; 294 } else { 295 whichPaint = mWorkPaint; 296 } 297 298 float wid = bm.getWidth() * 299 -whichPaint.ascent() / 300 bm.getHeight(); 301 302 w += wid; 303 hasTabOrEmoji = true; 304 j++; 305 } else { 306 w += widths[j - paraStart]; 307 } 308 } else { 309 w += widths[j - paraStart]; 310 } 311 } else { 312 w += widths[j - paraStart]; 313 } 314 315 // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width); 316 317 if (w <= width) { 318 fitwidth = w; 319 fit = j + 1; 320 321 if (fmtop < fittop) 322 fittop = fmtop; 323 if (fmascent < fitascent) 324 fitascent = fmascent; 325 if (fmdescent > fitdescent) 326 fitdescent = fmdescent; 327 if (fmbottom > fitbottom) 328 fitbottom = fmbottom; 329 330 /* 331 * From the Unicode Line Breaking Algorithm: 332 * (at least approximately) 333 * 334 * .,:; are class IS: breakpoints 335 * except when adjacent to digits 336 * / is class SY: a breakpoint 337 * except when followed by a digit. 338 * - is class HY: a breakpoint 339 * except when followed by a digit. 340 * 341 * Ideographs are class ID: breakpoints when adjacent, 342 * except for NS (non-starters), which can be broken 343 * after but not before. 344 */ 345 346 if (c == ' ' || c == '\t' || 347 ((c == '.' || c == ',' || c == ':' || c == ';') && 348 (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) && 349 (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || 350 ((c == '/' || c == '-') && 351 (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || 352 (c >= FIRST_CJK && isIdeographic(c, true) && 353 j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) { 354 okwidth = w; 355 ok = j + 1; 356 357 if (fittop < oktop) 358 oktop = fittop; 359 if (fitascent < okascent) 360 okascent = fitascent; 361 if (fitdescent > okdescent) 362 okdescent = fitdescent; 363 if (fitbottom > okbottom) 364 okbottom = fitbottom; 365 } 366 } else if (breakOnlyAtSpaces) { 367 if (ok != here) { 368 // Log.e("text", "output ok " + here + " to " +ok); 369 370 while (ok < spanEnd && chs[ok - paraStart] == ' ') { 371 ok++; 372 } 373 374 v = out(source, 375 here, ok, 376 okascent, okdescent, oktop, okbottom, 377 v, 378 spacingmult, spacingadd, chooseht, 379 choosehtv, fm, hasTabOrEmoji, 380 needMultiply, paraStart, chdirs, dir, easy, 381 ok == bufend, includepad, trackpad, 382 chs, widths, here - paraStart, 383 where, ellipsizedWidth, okwidth, 384 paint); 385 386 here = ok; 387 } else { 388 // Act like it fit even though it didn't. 389 390 fitwidth = w; 391 fit = j + 1; 392 393 if (fmtop < fittop) 394 fittop = fmtop; 395 if (fmascent < fitascent) 396 fitascent = fmascent; 397 if (fmdescent > fitdescent) 398 fitdescent = fmdescent; 399 if (fmbottom > fitbottom) 400 fitbottom = fmbottom; 401 } 402 } else { 403 if (ok != here) { 404 // Log.e("text", "output ok " + here + " to " +ok); 405 406 while (ok < spanEnd && chs[ok - paraStart] == ' ') { 407 ok++; 408 } 409 410 v = out(source, 411 here, ok, 412 okascent, okdescent, oktop, okbottom, 413 v, 414 spacingmult, spacingadd, chooseht, 415 choosehtv, fm, hasTabOrEmoji, 416 needMultiply, paraStart, chdirs, dir, easy, 417 ok == bufend, includepad, trackpad, 418 chs, widths, here - paraStart, 419 where, ellipsizedWidth, okwidth, 420 paint); 421 422 here = ok; 423 } else if (fit != here) { 424 // Log.e("text", "output fit " + here + " to " +fit); 425 v = out(source, 426 here, fit, 427 fitascent, fitdescent, 428 fittop, fitbottom, 429 v, 430 spacingmult, spacingadd, chooseht, 431 choosehtv, fm, hasTabOrEmoji, 432 needMultiply, paraStart, chdirs, dir, easy, 433 fit == bufend, includepad, trackpad, 434 chs, widths, here - paraStart, 435 where, ellipsizedWidth, fitwidth, 436 paint); 437 438 here = fit; 439 } else { 440 // Log.e("text", "output one " + here + " to " +(here + 1)); 441 // XXX not sure why the existing fm wasn't ok. 442 // measureText(paint, mWorkPaint, 443 // source, here, here + 1, fm, tab, 444 // null); 445 446 v = out(source, 447 here, here+1, 448 fm.ascent, fm.descent, 449 fm.top, fm.bottom, 450 v, 451 spacingmult, spacingadd, chooseht, 452 choosehtv, fm, hasTabOrEmoji, 453 needMultiply, paraStart, chdirs, dir, easy, 454 here + 1 == bufend, includepad, 455 trackpad, 456 chs, widths, here - paraStart, 457 where, ellipsizedWidth, 458 widths[here - paraStart], paint); 459 460 here = here + 1; 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 public int getLineForVertical(int vertical) { 841 int high = mLineCount; 842 int low = -1; 843 int guess; 844 int[] lines = mLines; 845 while (high - low > 1) { 846 guess = (high + low) >> 1; 847 if (lines[mColumns * guess + TOP] > vertical){ 848 high = guess; 849 } else { 850 low = guess; 851 } 852 } 853 if (low < 0) { 854 return 0; 855 } else { 856 return low; 857 } 858 } 859 860 public int getLineCount() { 861 return mLineCount; 862 } 863 864 public int getLineTop(int line) { 865 return mLines[mColumns * line + TOP]; 866 } 867 868 public int getLineDescent(int line) { 869 return mLines[mColumns * line + DESCENT]; 870 } 871 872 public int getLineStart(int line) { 873 return mLines[mColumns * line + START] & START_MASK; 874 } 875 876 public int getParagraphDirection(int line) { 877 return mLines[mColumns * line + DIR] >> DIR_SHIFT; 878 } 879 880 public boolean getLineContainsTab(int line) { 881 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0; 882 } 883 884 public final Directions getLineDirections(int line) { 885 return mLineDirections[line]; 886 } 887 888 public int getTopPadding() { 889 return mTopPadding; 890 } 891 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 private int mLineCount; 920 private int mTopPadding, mBottomPadding; 921 private int mColumns; 922 private int mEllipsizedWidth; 923 924 private static final int COLUMNS_NORMAL = 3; 925 private static final int COLUMNS_ELLIPSIZE = 5; 926 private static final int START = 0; 927 private static final int DIR = START; 928 private static final int TAB = START; 929 private static final int TOP = 1; 930 private static final int DESCENT = 2; 931 private static final int ELLIPSIS_START = 3; 932 private static final int ELLIPSIS_COUNT = 4; 933 934 private int[] mLines; 935 private Directions[] mLineDirections; 936 937 private static final int START_MASK = 0x1FFFFFFF; 938 private static final int DIR_MASK = 0xC0000000; 939 private static final int DIR_SHIFT = 30; 940 private static final int TAB_MASK = 0x20000000; 941 942 private static final int TAB_INCREMENT = 20; // same as Layout, but that's private 943 944 /* 945 * This is reused across calls to generate() 946 */ 947 private MeasuredText mMeasured; 948 private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); 949} 950