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