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