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