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, textDir, spacingmult, 141 spacingadd, includepad, includepad, ellipsizedWidth, 142 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 TextDirectionHeuristic textDir, float spacingmult, 161 float spacingadd, boolean includepad, 162 boolean trackpad, float ellipsizedWidth, 163 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() * -whichPaint.ascent() / bm.getHeight(); 332 333 w += wid; 334 hasTabOrEmoji = true; 335 j++; 336 } else { 337 w += widths[j - paraStart]; 338 } 339 } else { 340 w += widths[j - paraStart]; 341 } 342 } else { 343 w += widths[j - paraStart]; 344 } 345 346 // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width); 347 348 if (w <= width) { 349 fitWidth = w; 350 fit = j + 1; 351 352 if (fmTop < fitTop) 353 fitTop = fmTop; 354 if (fmAscent < fitAscent) 355 fitAscent = fmAscent; 356 if (fmDescent > fitDescent) 357 fitDescent = fmDescent; 358 if (fmBottom > fitBottom) 359 fitBottom = fmBottom; 360 361 /* 362 * From the Unicode Line Breaking Algorithm: 363 * (at least approximately) 364 * 365 * .,:; are class IS: breakpoints 366 * except when adjacent to digits 367 * / is class SY: a breakpoint 368 * except when followed by a digit. 369 * - is class HY: a breakpoint 370 * except when followed by a digit. 371 * 372 * Ideographs are class ID: breakpoints when adjacent, 373 * except for NS (non-starters), which can be broken 374 * after but not before. 375 */ 376 377 if (c == CHAR_SPACE || c == CHAR_TAB || 378 ((c == CHAR_DOT || c == CHAR_COMMA || 379 c == CHAR_COLON || c == CHAR_SEMICOLON) && 380 (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) && 381 (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || 382 ((c == CHAR_SLASH || c == CHAR_HYPHEN) && 383 (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) || 384 (c >= CHAR_FIRST_CJK && isIdeographic(c, true) && 385 j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) { 386 okWidth = w; 387 ok = j + 1; 388 389 if (fitTop < okTop) 390 okTop = fitTop; 391 if (fitAscent < okAscent) 392 okAscent = fitAscent; 393 if (fitDescent > okDescent) 394 okDescent = fitDescent; 395 if (fitBottom > okBottom) 396 okBottom = fitBottom; 397 } 398 } else { 399 final boolean moreChars = (j + 1 < spanEnd); 400 int endPos; 401 int above, below, top, bottom; 402 float currentTextWidth; 403 404 if (ok != here) { 405 // If it is a space that makes the length exceed width, cut here 406 if (c == CHAR_SPACE) ok = j + 1; 407 408 while (ok < spanEnd && chs[ok - paraStart] == CHAR_SPACE) { 409 ok++; 410 } 411 412 endPos = ok; 413 above = okAscent; 414 below = okDescent; 415 top = okTop; 416 bottom = okBottom; 417 currentTextWidth = okWidth; 418 } else if (fit != here) { 419 endPos = fit; 420 above = fitAscent; 421 below = fitDescent; 422 top = fitTop; 423 bottom = fitBottom; 424 currentTextWidth = fitWidth; 425 } else { 426 endPos = here + 1; 427 above = fm.ascent; 428 below = fm.descent; 429 top = fm.top; 430 bottom = fm.bottom; 431 currentTextWidth = widths[here - paraStart]; 432 } 433 434 v = out(source, here, endPos, 435 above, below, top, bottom, 436 v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, hasTabOrEmoji, 437 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad, 438 chs, widths, paraStart, ellipsize, ellipsizedWidth, 439 currentTextWidth, paint, moreChars); 440 here = endPos; 441 442 if (here < spanStart) { 443 // didn't output all the text for this span 444 // we've measured the raw widths, though, so 445 // just reset the start point 446 j = nextSpanStart = here; 447 } else { 448 j = here - 1; // continue looping 449 } 450 451 ok = fit = here; 452 w = 0; 453 fitAscent = fitDescent = fitTop = fitBottom = 0; 454 okAscent = okDescent = okTop = okBottom = 0; 455 456 if (--firstWidthLineLimit <= 0) { 457 width = restWidth; 458 } 459 } 460 if (mLineCount >= mMaximumVisibleLineCount) { 461 break; 462 } 463 } 464 } 465 466 if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) { 467 if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) { 468 paint.getFontMetricsInt(fm); 469 470 fitTop = fm.top; 471 fitBottom = fm.bottom; 472 fitAscent = fm.ascent; 473 fitDescent = fm.descent; 474 } 475 476 // Log.e("text", "output rest " + here + " to " + end); 477 478 v = out(source, 479 here, paraEnd, fitAscent, fitDescent, 480 fitTop, fitBottom, 481 v, 482 spacingmult, spacingadd, chooseHt, 483 chooseHtv, fm, hasTabOrEmoji, 484 needMultiply, chdirs, dir, easy, bufEnd, 485 includepad, trackpad, chs, 486 widths, paraStart, ellipsize, 487 ellipsizedWidth, w, paint, paraEnd != bufEnd); 488 } 489 490 paraStart = paraEnd; 491 492 if (paraEnd == bufEnd) 493 break; 494 } 495 496 if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) && 497 mLineCount < mMaximumVisibleLineCount) { 498 // Log.e("text", "output last " + bufEnd); 499 500 paint.getFontMetricsInt(fm); 501 502 v = out(source, 503 bufEnd, bufEnd, fm.ascent, fm.descent, 504 fm.top, fm.bottom, 505 v, 506 spacingmult, spacingadd, null, 507 null, fm, false, 508 needMultiply, null, DEFAULT_DIR, true, bufEnd, 509 includepad, trackpad, null, 510 null, bufStart, ellipsize, 511 ellipsizedWidth, 0, paint, false); 512 } 513 } 514 515 /** 516 * Returns true if the specified character is one of those specified 517 * as being Ideographic (class ID) by the Unicode Line Breaking Algorithm 518 * (http://www.unicode.org/unicode/reports/tr14/), and is therefore OK 519 * to break between a pair of. 520 * 521 * @param includeNonStarters also return true for category NS 522 * (non-starters), which can be broken 523 * after but not before. 524 */ 525 private static final boolean isIdeographic(char c, boolean includeNonStarters) { 526 if (c >= '\u2E80' && c <= '\u2FFF') { 527 return true; // CJK, KANGXI RADICALS, DESCRIPTION SYMBOLS 528 } 529 if (c == '\u3000') { 530 return true; // IDEOGRAPHIC SPACE 531 } 532 if (c >= '\u3040' && c <= '\u309F') { 533 if (!includeNonStarters) { 534 switch (c) { 535 case '\u3041': // # HIRAGANA LETTER SMALL A 536 case '\u3043': // # HIRAGANA LETTER SMALL I 537 case '\u3045': // # HIRAGANA LETTER SMALL U 538 case '\u3047': // # HIRAGANA LETTER SMALL E 539 case '\u3049': // # HIRAGANA LETTER SMALL O 540 case '\u3063': // # HIRAGANA LETTER SMALL TU 541 case '\u3083': // # HIRAGANA LETTER SMALL YA 542 case '\u3085': // # HIRAGANA LETTER SMALL YU 543 case '\u3087': // # HIRAGANA LETTER SMALL YO 544 case '\u308E': // # HIRAGANA LETTER SMALL WA 545 case '\u3095': // # HIRAGANA LETTER SMALL KA 546 case '\u3096': // # HIRAGANA LETTER SMALL KE 547 case '\u309B': // # KATAKANA-HIRAGANA VOICED SOUND MARK 548 case '\u309C': // # KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK 549 case '\u309D': // # HIRAGANA ITERATION MARK 550 case '\u309E': // # HIRAGANA VOICED ITERATION MARK 551 return false; 552 } 553 } 554 return true; // Hiragana (except small characters) 555 } 556 if (c >= '\u30A0' && c <= '\u30FF') { 557 if (!includeNonStarters) { 558 switch (c) { 559 case '\u30A0': // # KATAKANA-HIRAGANA DOUBLE HYPHEN 560 case '\u30A1': // # KATAKANA LETTER SMALL A 561 case '\u30A3': // # KATAKANA LETTER SMALL I 562 case '\u30A5': // # KATAKANA LETTER SMALL U 563 case '\u30A7': // # KATAKANA LETTER SMALL E 564 case '\u30A9': // # KATAKANA LETTER SMALL O 565 case '\u30C3': // # KATAKANA LETTER SMALL TU 566 case '\u30E3': // # KATAKANA LETTER SMALL YA 567 case '\u30E5': // # KATAKANA LETTER SMALL YU 568 case '\u30E7': // # KATAKANA LETTER SMALL YO 569 case '\u30EE': // # KATAKANA LETTER SMALL WA 570 case '\u30F5': // # KATAKANA LETTER SMALL KA 571 case '\u30F6': // # KATAKANA LETTER SMALL KE 572 case '\u30FB': // # KATAKANA MIDDLE DOT 573 case '\u30FC': // # KATAKANA-HIRAGANA PROLONGED SOUND MARK 574 case '\u30FD': // # KATAKANA ITERATION MARK 575 case '\u30FE': // # KATAKANA VOICED ITERATION MARK 576 return false; 577 } 578 } 579 return true; // Katakana (except small characters) 580 } 581 if (c >= '\u3400' && c <= '\u4DB5') { 582 return true; // CJK UNIFIED IDEOGRAPHS EXTENSION A 583 } 584 if (c >= '\u4E00' && c <= '\u9FBB') { 585 return true; // CJK UNIFIED IDEOGRAPHS 586 } 587 if (c >= '\uF900' && c <= '\uFAD9') { 588 return true; // CJK COMPATIBILITY IDEOGRAPHS 589 } 590 if (c >= '\uA000' && c <= '\uA48F') { 591 return true; // YI SYLLABLES 592 } 593 if (c >= '\uA490' && c <= '\uA4CF') { 594 return true; // YI RADICALS 595 } 596 if (c >= '\uFE62' && c <= '\uFE66') { 597 return true; // SMALL PLUS SIGN to SMALL EQUALS SIGN 598 } 599 if (c >= '\uFF10' && c <= '\uFF19') { 600 return true; // WIDE DIGITS 601 } 602 603 return false; 604 } 605 606 private int out(CharSequence text, int start, int end, 607 int above, int below, int top, int bottom, int v, 608 float spacingmult, float spacingadd, 609 LineHeightSpan[] chooseHt, int[] chooseHtv, 610 Paint.FontMetricsInt fm, boolean hasTabOrEmoji, 611 boolean needMultiply, byte[] chdirs, int dir, 612 boolean easy, int bufEnd, boolean includePad, 613 boolean trackPad, char[] chs, 614 float[] widths, int widthStart, TextUtils.TruncateAt ellipsize, 615 float ellipsisWidth, float textWidth, 616 TextPaint paint, boolean moreChars) { 617 int j = mLineCount; 618 int off = j * mColumns; 619 int want = off + mColumns + TOP; 620 int[] lines = mLines; 621 622 if (want >= lines.length) { 623 int nlen = ArrayUtils.idealIntArraySize(want + 1); 624 int[] grow = new int[nlen]; 625 System.arraycopy(lines, 0, grow, 0, lines.length); 626 mLines = grow; 627 lines = grow; 628 629 Directions[] grow2 = new Directions[nlen]; 630 System.arraycopy(mLineDirections, 0, grow2, 0, 631 mLineDirections.length); 632 mLineDirections = grow2; 633 } 634 635 if (chooseHt != null) { 636 fm.ascent = above; 637 fm.descent = below; 638 fm.top = top; 639 fm.bottom = bottom; 640 641 for (int i = 0; i < chooseHt.length; i++) { 642 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) { 643 ((LineHeightSpan.WithDensity) chooseHt[i]). 644 chooseHeight(text, start, end, chooseHtv[i], v, fm, paint); 645 646 } else { 647 chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); 648 } 649 } 650 651 above = fm.ascent; 652 below = fm.descent; 653 top = fm.top; 654 bottom = fm.bottom; 655 } 656 657 if (j == 0) { 658 if (trackPad) { 659 mTopPadding = top - above; 660 } 661 662 if (includePad) { 663 above = top; 664 } 665 } 666 if (end == bufEnd) { 667 if (trackPad) { 668 mBottomPadding = bottom - below; 669 } 670 671 if (includePad) { 672 below = bottom; 673 } 674 } 675 676 int extra; 677 678 if (needMultiply) { 679 double ex = (below - above) * (spacingmult - 1) + spacingadd; 680 if (ex >= 0) { 681 extra = (int)(ex + EXTRA_ROUNDING); 682 } else { 683 extra = -(int)(-ex + EXTRA_ROUNDING); 684 } 685 } else { 686 extra = 0; 687 } 688 689 lines[off + START] = start; 690 lines[off + TOP] = v; 691 lines[off + DESCENT] = below + extra; 692 693 v += (below - above) + extra; 694 lines[off + mColumns + START] = end; 695 lines[off + mColumns + TOP] = v; 696 697 if (hasTabOrEmoji) 698 lines[off + TAB] |= TAB_MASK; 699 700 lines[off + DIR] |= dir << DIR_SHIFT; 701 Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; 702 // easy means all chars < the first RTL, so no emoji, no nothing 703 // XXX a run with no text or all spaces is easy but might be an empty 704 // RTL paragraph. Make sure easy is false if this is the case. 705 if (easy) { 706 mLineDirections[j] = linedirs; 707 } else { 708 mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs, 709 start - widthStart, end - start); 710 } 711 712 if (ellipsize != null) { 713 // If there is only one line, then do any type of ellipsis except when it is MARQUEE 714 // if there are multiple lines, just allow END ellipsis on the last line 715 boolean firstLine = (j == 0); 716 boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount); 717 boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount); 718 719 boolean doEllipsis = (firstLine && !moreChars && 720 ellipsize != TextUtils.TruncateAt.MARQUEE) || 721 (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) && 722 ellipsize == TextUtils.TruncateAt.END); 723 if (doEllipsis) { 724 calculateEllipsis(start, end, widths, widthStart, 725 ellipsisWidth, ellipsize, j, 726 textWidth, paint, forceEllipsis); 727 } 728 } 729 730 mLineCount++; 731 return v; 732 } 733 734 private void calculateEllipsis(int lineStart, int lineEnd, 735 float[] widths, int widthStart, 736 float avail, TextUtils.TruncateAt where, 737 int line, float textWidth, TextPaint paint, 738 boolean forceEllipsis) { 739 if (textWidth <= avail && !forceEllipsis) { 740 // Everything fits! 741 mLines[mColumns * line + ELLIPSIS_START] = 0; 742 mLines[mColumns * line + ELLIPSIS_COUNT] = 0; 743 return; 744 } 745 746 float ellipsisWidth = paint.measureText( 747 (where == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL); 748 int ellipsisStart = 0; 749 int ellipsisCount = 0; 750 int len = lineEnd - lineStart; 751 752 // We only support start ellipsis on a single line 753 if (where == TextUtils.TruncateAt.START) { 754 if (mMaximumVisibleLineCount == 1) { 755 float sum = 0; 756 int i; 757 758 for (i = len; i >= 0; i--) { 759 float w = widths[i - 1 + lineStart - widthStart]; 760 761 if (w + sum + ellipsisWidth > avail) { 762 break; 763 } 764 765 sum += w; 766 } 767 768 ellipsisStart = 0; 769 ellipsisCount = i; 770 } else { 771 if (Log.isLoggable(TAG, Log.WARN)) { 772 Log.w(TAG, "Start Ellipsis only supported with one line"); 773 } 774 } 775 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE || 776 where == TextUtils.TruncateAt.END_SMALL) { 777 float sum = 0; 778 int i; 779 780 for (i = 0; i < len; i++) { 781 float w = widths[i + lineStart - widthStart]; 782 783 if (w + sum + ellipsisWidth > avail) { 784 break; 785 } 786 787 sum += w; 788 } 789 790 ellipsisStart = i; 791 ellipsisCount = len - i; 792 if (forceEllipsis && ellipsisCount == 0 && len > 0) { 793 ellipsisStart = len - 1; 794 ellipsisCount = 1; 795 } 796 } else { 797 // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line 798 if (mMaximumVisibleLineCount == 1) { 799 float lsum = 0, rsum = 0; 800 int left = 0, right = len; 801 802 float ravail = (avail - ellipsisWidth) / 2; 803 for (right = len; right >= 0; right--) { 804 float w = widths[right - 1 + lineStart - widthStart]; 805 806 if (w + rsum > ravail) { 807 break; 808 } 809 810 rsum += w; 811 } 812 813 float lavail = avail - ellipsisWidth - rsum; 814 for (left = 0; left < right; left++) { 815 float w = widths[left + lineStart - widthStart]; 816 817 if (w + lsum > lavail) { 818 break; 819 } 820 821 lsum += w; 822 } 823 824 ellipsisStart = left; 825 ellipsisCount = right - left; 826 } else { 827 if (Log.isLoggable(TAG, Log.WARN)) { 828 Log.w(TAG, "Middle Ellipsis only supported with one line"); 829 } 830 } 831 } 832 833 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart; 834 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount; 835 } 836 837 // Override the base class so we can directly access our members, 838 // rather than relying on member functions. 839 // The logic mirrors that of Layout.getLineForVertical 840 // FIXME: It may be faster to do a linear search for layouts without many lines. 841 @Override 842 public int getLineForVertical(int vertical) { 843 int high = mLineCount; 844 int low = -1; 845 int guess; 846 int[] lines = mLines; 847 while (high - low > 1) { 848 guess = (high + low) >> 1; 849 if (lines[mColumns * guess + TOP] > vertical){ 850 high = guess; 851 } else { 852 low = guess; 853 } 854 } 855 if (low < 0) { 856 return 0; 857 } else { 858 return low; 859 } 860 } 861 862 @Override 863 public int getLineCount() { 864 return mLineCount; 865 } 866 867 @Override 868 public int getLineTop(int line) { 869 int top = mLines[mColumns * line + TOP]; 870 if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount && 871 line != mLineCount) { 872 top += getBottomPadding(); 873 } 874 return top; 875 } 876 877 @Override 878 public int getLineDescent(int line) { 879 int descent = mLines[mColumns * line + DESCENT]; 880 if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended 881 line != mLineCount) { 882 descent += getBottomPadding(); 883 } 884 return descent; 885 } 886 887 @Override 888 public int getLineStart(int line) { 889 return mLines[mColumns * line + START] & START_MASK; 890 } 891 892 @Override 893 public int getParagraphDirection(int line) { 894 return mLines[mColumns * line + DIR] >> DIR_SHIFT; 895 } 896 897 @Override 898 public boolean getLineContainsTab(int line) { 899 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0; 900 } 901 902 @Override 903 public final Directions getLineDirections(int line) { 904 return mLineDirections[line]; 905 } 906 907 @Override 908 public int getTopPadding() { 909 return mTopPadding; 910 } 911 912 @Override 913 public int getBottomPadding() { 914 return mBottomPadding; 915 } 916 917 @Override 918 public int getEllipsisCount(int line) { 919 if (mColumns < COLUMNS_ELLIPSIZE) { 920 return 0; 921 } 922 923 return mLines[mColumns * line + ELLIPSIS_COUNT]; 924 } 925 926 @Override 927 public int getEllipsisStart(int line) { 928 if (mColumns < COLUMNS_ELLIPSIZE) { 929 return 0; 930 } 931 932 return mLines[mColumns * line + ELLIPSIS_START]; 933 } 934 935 @Override 936 public int getEllipsizedWidth() { 937 return mEllipsizedWidth; 938 } 939 940 void prepare() { 941 mMeasured = MeasuredText.obtain(); 942 } 943 944 void finish() { 945 mMeasured = MeasuredText.recycle(mMeasured); 946 } 947 948 private int mLineCount; 949 private int mTopPadding, mBottomPadding; 950 private int mColumns; 951 private int mEllipsizedWidth; 952 953 private static final int COLUMNS_NORMAL = 3; 954 private static final int COLUMNS_ELLIPSIZE = 5; 955 private static final int START = 0; 956 private static final int DIR = START; 957 private static final int TAB = START; 958 private static final int TOP = 1; 959 private static final int DESCENT = 2; 960 private static final int ELLIPSIS_START = 3; 961 private static final int ELLIPSIS_COUNT = 4; 962 963 private int[] mLines; 964 private Directions[] mLineDirections; 965 private int mMaximumVisibleLineCount = Integer.MAX_VALUE; 966 967 private static final int START_MASK = 0x1FFFFFFF; 968 private static final int DIR_SHIFT = 30; 969 private static final int TAB_MASK = 0x20000000; 970 971 private static final int TAB_INCREMENT = 20; // same as Layout, but that's private 972 973 private static final char CHAR_FIRST_CJK = '\u2E80'; 974 975 private static final char CHAR_NEW_LINE = '\n'; 976 private static final char CHAR_TAB = '\t'; 977 private static final char CHAR_SPACE = ' '; 978 private static final char CHAR_DOT = '.'; 979 private static final char CHAR_COMMA = ','; 980 private static final char CHAR_COLON = ':'; 981 private static final char CHAR_SEMICOLON = ';'; 982 private static final char CHAR_SLASH = '/'; 983 private static final char CHAR_HYPHEN = '-'; 984 985 private static final double EXTRA_ROUNDING = 0.5; 986 987 private static final String ELLIPSIS_NORMAL = "\u2026"; // this is "..." 988 private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // this is ".." 989 990 private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800; 991 private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF; 992 993 /* 994 * This is reused across calls to generate() 995 */ 996 private MeasuredText mMeasured; 997 private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); 998} 999