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