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