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