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