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