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.Paint; 20 import android.graphics.Rect; 21 import android.text.style.ReplacementSpan; 22 import android.text.style.UpdateLayout; 23 import android.text.style.WrapTogetherSpan; 24 import android.util.ArraySet; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 import com.android.internal.util.ArrayUtils; 28 import com.android.internal.util.GrowingArrayUtils; 29 30 import java.lang.ref.WeakReference; 31 32 /** 33 * DynamicLayout is a text layout that updates itself as the text is edited. 34 * <p>This is used by widgets to control text layout. You should not need 35 * to use this class directly unless you are implementing your own widget 36 * or custom display object, or need to call 37 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) 38 * Canvas.drawText()} directly.</p> 39 */ 40 public class DynamicLayout extends Layout 41 { 42 private static final int PRIORITY = 128; 43 private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400; 44 45 /** 46 * Make a layout for the specified text that will be updated as 47 * the text is changed. 48 */ DynamicLayout(CharSequence base, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)49 public DynamicLayout(CharSequence base, 50 TextPaint paint, 51 int width, Alignment align, 52 float spacingmult, float spacingadd, 53 boolean includepad) { 54 this(base, base, paint, width, align, spacingmult, spacingadd, 55 includepad); 56 } 57 58 /** 59 * Make a layout for the transformed text (password transformation 60 * being the primary example of a transformation) 61 * that will be updated as the base text is changed. 62 */ DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)63 public DynamicLayout(CharSequence base, CharSequence display, 64 TextPaint paint, 65 int width, Alignment align, 66 float spacingmult, float spacingadd, 67 boolean includepad) { 68 this(base, display, paint, width, align, spacingmult, spacingadd, 69 includepad, null, 0); 70 } 71 72 /** 73 * Make a layout for the transformed text (password transformation 74 * being the primary example of a transformation) 75 * that will be updated as the base text is changed. 76 * If ellipsize is non-null, the Layout will ellipsize the text 77 * down to ellipsizedWidth. 78 */ DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)79 public DynamicLayout(CharSequence base, CharSequence display, 80 TextPaint paint, 81 int width, Alignment align, 82 float spacingmult, float spacingadd, 83 boolean includepad, 84 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 85 this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, 86 spacingmult, spacingadd, includepad, 87 StaticLayout.BREAK_STRATEGY_SIMPLE, StaticLayout.HYPHENATION_FREQUENCY_NONE, 88 Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth); 89 } 90 91 /** 92 * Make a layout for the transformed text (password transformation 93 * being the primary example of a transformation) 94 * that will be updated as the base text is changed. 95 * If ellipsize is non-null, the Layout will ellipsize the text 96 * down to ellipsizedWidth. 97 * * 98 * *@hide 99 */ DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad, int breakStrategy, int hyphenationFrequency, int justificationMode, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)100 public DynamicLayout(CharSequence base, CharSequence display, 101 TextPaint paint, 102 int width, Alignment align, TextDirectionHeuristic textDir, 103 float spacingmult, float spacingadd, 104 boolean includepad, int breakStrategy, int hyphenationFrequency, 105 int justificationMode, TextUtils.TruncateAt ellipsize, 106 int ellipsizedWidth) { 107 super((ellipsize == null) 108 ? display 109 : (display instanceof Spanned) 110 ? new SpannedEllipsizer(display) 111 : new Ellipsizer(display), 112 paint, width, align, textDir, spacingmult, spacingadd); 113 114 mBase = base; 115 mDisplay = display; 116 117 if (ellipsize != null) { 118 mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); 119 mEllipsizedWidth = ellipsizedWidth; 120 mEllipsizeAt = ellipsize; 121 } else { 122 mInts = new PackedIntVector(COLUMNS_NORMAL); 123 mEllipsizedWidth = width; 124 mEllipsizeAt = null; 125 } 126 127 mObjects = new PackedObjectVector<Directions>(1); 128 129 mIncludePad = includepad; 130 mBreakStrategy = breakStrategy; 131 mJustificationMode = justificationMode; 132 mHyphenationFrequency = hyphenationFrequency; 133 134 /* 135 * This is annoying, but we can't refer to the layout until 136 * superclass construction is finished, and the superclass 137 * constructor wants the reference to the display text. 138 * 139 * This will break if the superclass constructor ever actually 140 * cares about the content instead of just holding the reference. 141 */ 142 if (ellipsize != null) { 143 Ellipsizer e = (Ellipsizer) getText(); 144 145 e.mLayout = this; 146 e.mWidth = ellipsizedWidth; 147 e.mMethod = ellipsize; 148 mEllipsize = true; 149 } 150 151 // Initial state is a single line with 0 characters (0 to 0), 152 // with top at 0 and bottom at whatever is natural, and 153 // undefined ellipsis. 154 155 int[] start; 156 157 if (ellipsize != null) { 158 start = new int[COLUMNS_ELLIPSIZE]; 159 start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 160 } else { 161 start = new int[COLUMNS_NORMAL]; 162 } 163 164 Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT }; 165 166 Paint.FontMetricsInt fm = paint.getFontMetricsInt(); 167 int asc = fm.ascent; 168 int desc = fm.descent; 169 170 start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT; 171 start[TOP] = 0; 172 start[DESCENT] = desc; 173 mInts.insertAt(0, start); 174 175 start[TOP] = desc - asc; 176 mInts.insertAt(1, start); 177 178 mObjects.insertAt(0, dirs); 179 180 // Update from 0 characters to whatever the real text is 181 reflow(base, 0, 0, base.length()); 182 183 if (base instanceof Spannable) { 184 if (mWatcher == null) 185 mWatcher = new ChangeWatcher(this); 186 187 // Strip out any watchers for other DynamicLayouts. 188 Spannable sp = (Spannable) base; 189 ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class); 190 for (int i = 0; i < spans.length; i++) 191 sp.removeSpan(spans[i]); 192 193 sp.setSpan(mWatcher, 0, base.length(), 194 Spannable.SPAN_INCLUSIVE_INCLUSIVE | 195 (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT)); 196 } 197 } 198 reflow(CharSequence s, int where, int before, int after)199 private void reflow(CharSequence s, int where, int before, int after) { 200 if (s != mBase) 201 return; 202 203 CharSequence text = mDisplay; 204 int len = text.length(); 205 206 // seek back to the start of the paragraph 207 208 int find = TextUtils.lastIndexOf(text, '\n', where - 1); 209 if (find < 0) 210 find = 0; 211 else 212 find = find + 1; 213 214 { 215 int diff = where - find; 216 before += diff; 217 after += diff; 218 where -= diff; 219 } 220 221 // seek forward to the end of the paragraph 222 223 int look = TextUtils.indexOf(text, '\n', where + after); 224 if (look < 0) 225 look = len; 226 else 227 look++; // we want the index after the \n 228 229 int change = look - (where + after); 230 before += change; 231 after += change; 232 233 // seek further out to cover anything that is forced to wrap together 234 235 if (text instanceof Spanned) { 236 Spanned sp = (Spanned) text; 237 boolean again; 238 239 do { 240 again = false; 241 242 Object[] force = sp.getSpans(where, where + after, 243 WrapTogetherSpan.class); 244 245 for (int i = 0; i < force.length; i++) { 246 int st = sp.getSpanStart(force[i]); 247 int en = sp.getSpanEnd(force[i]); 248 249 if (st < where) { 250 again = true; 251 252 int diff = where - st; 253 before += diff; 254 after += diff; 255 where -= diff; 256 } 257 258 if (en > where + after) { 259 again = true; 260 261 int diff = en - (where + after); 262 before += diff; 263 after += diff; 264 } 265 } 266 } while (again); 267 } 268 269 // find affected region of old layout 270 271 int startline = getLineForOffset(where); 272 int startv = getLineTop(startline); 273 274 int endline = getLineForOffset(where + before); 275 if (where + after == len) 276 endline = getLineCount(); 277 int endv = getLineTop(endline); 278 boolean islast = (endline == getLineCount()); 279 280 // generate new layout for affected text 281 282 StaticLayout reflowed; 283 StaticLayout.Builder b; 284 285 synchronized (sLock) { 286 reflowed = sStaticLayout; 287 b = sBuilder; 288 sStaticLayout = null; 289 sBuilder = null; 290 } 291 292 if (reflowed == null) { 293 reflowed = new StaticLayout(null); 294 b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth()); 295 } 296 297 b.setText(text, where, where + after) 298 .setPaint(getPaint()) 299 .setWidth(getWidth()) 300 .setTextDirection(getTextDirectionHeuristic()) 301 .setLineSpacing(getSpacingAdd(), getSpacingMultiplier()) 302 .setEllipsizedWidth(mEllipsizedWidth) 303 .setEllipsize(mEllipsizeAt) 304 .setBreakStrategy(mBreakStrategy) 305 .setHyphenationFrequency(mHyphenationFrequency) 306 .setJustificationMode(mJustificationMode); 307 reflowed.generate(b, false, true); 308 int n = reflowed.getLineCount(); 309 // If the new layout has a blank line at the end, but it is not 310 // the very end of the buffer, then we already have a line that 311 // starts there, so disregard the blank line. 312 313 if (where + after != len && reflowed.getLineStart(n - 1) == where + after) 314 n--; 315 316 // remove affected lines from old layout 317 mInts.deleteAt(startline, endline - startline); 318 mObjects.deleteAt(startline, endline - startline); 319 320 // adjust offsets in layout for new height and offsets 321 322 int ht = reflowed.getLineTop(n); 323 int toppad = 0, botpad = 0; 324 325 if (mIncludePad && startline == 0) { 326 toppad = reflowed.getTopPadding(); 327 mTopPadding = toppad; 328 ht -= toppad; 329 } 330 if (mIncludePad && islast) { 331 botpad = reflowed.getBottomPadding(); 332 mBottomPadding = botpad; 333 ht += botpad; 334 } 335 336 mInts.adjustValuesBelow(startline, START, after - before); 337 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht); 338 339 // insert new layout 340 341 int[] ints; 342 343 if (mEllipsize) { 344 ints = new int[COLUMNS_ELLIPSIZE]; 345 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 346 } else { 347 ints = new int[COLUMNS_NORMAL]; 348 } 349 350 Directions[] objects = new Directions[1]; 351 352 for (int i = 0; i < n; i++) { 353 final int start = reflowed.getLineStart(i); 354 ints[START] = start; 355 ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT; 356 ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0; 357 358 int top = reflowed.getLineTop(i) + startv; 359 if (i > 0) 360 top -= toppad; 361 ints[TOP] = top; 362 363 int desc = reflowed.getLineDescent(i); 364 if (i == n - 1) 365 desc += botpad; 366 367 ints[DESCENT] = desc; 368 objects[0] = reflowed.getLineDirections(i); 369 370 final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1); 371 ints[HYPHEN] = reflowed.getHyphen(i) & HYPHEN_MASK; 372 ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |= 373 contentMayProtrudeFromLineTopOrBottom(text, start, end) ? 374 MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0; 375 376 if (mEllipsize) { 377 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); 378 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); 379 } 380 381 mInts.insertAt(startline + i, ints); 382 mObjects.insertAt(startline + i, objects); 383 } 384 385 updateBlocks(startline, endline - 1, n); 386 387 b.finish(); 388 synchronized (sLock) { 389 sStaticLayout = reflowed; 390 sBuilder = b; 391 } 392 } 393 contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end)394 private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) { 395 if (text instanceof Spanned) { 396 final Spanned spanned = (Spanned) text; 397 if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) { 398 return true; 399 } 400 } 401 // Spans other than ReplacementSpan can be ignored because line top and bottom are 402 // disjunction of all tops and bottoms, although it's not optimal. 403 final Paint paint = getPaint(); 404 paint.getTextBounds(text, start, end, mTempRect); 405 final Paint.FontMetricsInt fm = paint.getFontMetricsInt(); 406 return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom; 407 } 408 409 /** 410 * Create the initial block structure, cutting the text into blocks of at least 411 * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs. 412 */ createBlocks()413 private void createBlocks() { 414 int offset = BLOCK_MINIMUM_CHARACTER_LENGTH; 415 mNumberOfBlocks = 0; 416 final CharSequence text = mDisplay; 417 418 while (true) { 419 offset = TextUtils.indexOf(text, '\n', offset); 420 if (offset < 0) { 421 addBlockAtOffset(text.length()); 422 break; 423 } else { 424 addBlockAtOffset(offset); 425 offset += BLOCK_MINIMUM_CHARACTER_LENGTH; 426 } 427 } 428 429 // mBlockIndices and mBlockEndLines should have the same length 430 mBlockIndices = new int[mBlockEndLines.length]; 431 for (int i = 0; i < mBlockEndLines.length; i++) { 432 mBlockIndices[i] = INVALID_BLOCK_INDEX; 433 } 434 } 435 436 /** 437 * @hide 438 */ getBlocksAlwaysNeedToBeRedrawn()439 public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() { 440 return mBlocksAlwaysNeedToBeRedrawn; 441 } 442 updateAlwaysNeedsToBeRedrawn(int blockIndex)443 private void updateAlwaysNeedsToBeRedrawn(int blockIndex) { 444 int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1); 445 int endLine = mBlockEndLines[blockIndex]; 446 for (int i = startLine; i <= endLine; i++) { 447 if (getContentMayProtrudeFromTopOrBottom(i)) { 448 if (mBlocksAlwaysNeedToBeRedrawn == null) { 449 mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>(); 450 } 451 mBlocksAlwaysNeedToBeRedrawn.add(blockIndex); 452 return; 453 } 454 } 455 if (mBlocksAlwaysNeedToBeRedrawn != null) { 456 mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex); 457 } 458 } 459 460 /** 461 * Create a new block, ending at the specified character offset. 462 * A block will actually be created only if has at least one line, i.e. this offset is 463 * not on the end line of the previous block. 464 */ addBlockAtOffset(int offset)465 private void addBlockAtOffset(int offset) { 466 final int line = getLineForOffset(offset); 467 if (mBlockEndLines == null) { 468 // Initial creation of the array, no test on previous block ending line 469 mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1); 470 mBlockEndLines[mNumberOfBlocks] = line; 471 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks); 472 mNumberOfBlocks++; 473 return; 474 } 475 476 final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1]; 477 if (line > previousBlockEndLine) { 478 mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line); 479 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks); 480 mNumberOfBlocks++; 481 } 482 } 483 484 /** 485 * This method is called every time the layout is reflowed after an edition. 486 * It updates the internal block data structure. The text is split in blocks 487 * of contiguous lines, with at least one block for the entire text. 488 * When a range of lines is edited, new blocks (from 0 to 3 depending on the 489 * overlap structure) will replace the set of overlapping blocks. 490 * Blocks are listed in order and are represented by their ending line number. 491 * An index is associated to each block (which will be used by display lists), 492 * this class simply invalidates the index of blocks overlapping a modification. 493 * 494 * @param startLine the first line of the range of modified lines 495 * @param endLine the last line of the range, possibly equal to startLine, lower 496 * than getLineCount() 497 * @param newLineCount the number of lines that will replace the range, possibly 0 498 * 499 * @hide 500 */ 501 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) updateBlocks(int startLine, int endLine, int newLineCount)502 public void updateBlocks(int startLine, int endLine, int newLineCount) { 503 if (mBlockEndLines == null) { 504 createBlocks(); 505 return; 506 } 507 508 int firstBlock = -1; 509 int lastBlock = -1; 510 for (int i = 0; i < mNumberOfBlocks; i++) { 511 if (mBlockEndLines[i] >= startLine) { 512 firstBlock = i; 513 break; 514 } 515 } 516 for (int i = firstBlock; i < mNumberOfBlocks; i++) { 517 if (mBlockEndLines[i] >= endLine) { 518 lastBlock = i; 519 break; 520 } 521 } 522 final int lastBlockEndLine = mBlockEndLines[lastBlock]; 523 524 boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 : 525 mBlockEndLines[firstBlock - 1] + 1); 526 boolean createBlock = newLineCount > 0; 527 boolean createBlockAfter = endLine < mBlockEndLines[lastBlock]; 528 529 int numAddedBlocks = 0; 530 if (createBlockBefore) numAddedBlocks++; 531 if (createBlock) numAddedBlocks++; 532 if (createBlockAfter) numAddedBlocks++; 533 534 final int numRemovedBlocks = lastBlock - firstBlock + 1; 535 final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks; 536 537 if (newNumberOfBlocks == 0) { 538 // Even when text is empty, there is actually one line and hence one block 539 mBlockEndLines[0] = 0; 540 mBlockIndices[0] = INVALID_BLOCK_INDEX; 541 mNumberOfBlocks = 1; 542 return; 543 } 544 545 if (newNumberOfBlocks > mBlockEndLines.length) { 546 int[] blockEndLines = ArrayUtils.newUnpaddedIntArray( 547 Math.max(mBlockEndLines.length * 2, newNumberOfBlocks)); 548 int[] blockIndices = new int[blockEndLines.length]; 549 System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock); 550 System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock); 551 System.arraycopy(mBlockEndLines, lastBlock + 1, 552 blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 553 System.arraycopy(mBlockIndices, lastBlock + 1, 554 blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 555 mBlockEndLines = blockEndLines; 556 mBlockIndices = blockIndices; 557 } else if (numAddedBlocks + numRemovedBlocks != 0) { 558 System.arraycopy(mBlockEndLines, lastBlock + 1, 559 mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 560 System.arraycopy(mBlockIndices, lastBlock + 1, 561 mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 562 } 563 564 if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) { 565 final ArraySet<Integer> set = new ArraySet<>(); 566 for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) { 567 Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i); 568 if (block > firstBlock) { 569 block += numAddedBlocks - numRemovedBlocks; 570 } 571 set.add(block); 572 } 573 mBlocksAlwaysNeedToBeRedrawn = set; 574 } 575 576 mNumberOfBlocks = newNumberOfBlocks; 577 int newFirstChangedBlock; 578 final int deltaLines = newLineCount - (endLine - startLine + 1); 579 if (deltaLines != 0) { 580 // Display list whose index is >= mIndexFirstChangedBlock is valid 581 // but it needs to update its drawing location. 582 newFirstChangedBlock = firstBlock + numAddedBlocks; 583 for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) { 584 mBlockEndLines[i] += deltaLines; 585 } 586 } else { 587 newFirstChangedBlock = mNumberOfBlocks; 588 } 589 mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock); 590 591 int blockIndex = firstBlock; 592 if (createBlockBefore) { 593 mBlockEndLines[blockIndex] = startLine - 1; 594 updateAlwaysNeedsToBeRedrawn(blockIndex); 595 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 596 blockIndex++; 597 } 598 599 if (createBlock) { 600 mBlockEndLines[blockIndex] = startLine + newLineCount - 1; 601 updateAlwaysNeedsToBeRedrawn(blockIndex); 602 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 603 blockIndex++; 604 } 605 606 if (createBlockAfter) { 607 mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines; 608 updateAlwaysNeedsToBeRedrawn(blockIndex); 609 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 610 } 611 } 612 613 /** 614 * This method is used for test purposes only. 615 * @hide 616 */ 617 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks, int totalLines)618 public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks, 619 int totalLines) { 620 mBlockEndLines = new int[blockEndLines.length]; 621 mBlockIndices = new int[blockIndices.length]; 622 System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length); 623 System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length); 624 mNumberOfBlocks = numberOfBlocks; 625 while (mInts.size() < totalLines) { 626 mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]); 627 } 628 } 629 630 /** 631 * @hide 632 */ getBlockEndLines()633 public int[] getBlockEndLines() { 634 return mBlockEndLines; 635 } 636 637 /** 638 * @hide 639 */ getBlockIndices()640 public int[] getBlockIndices() { 641 return mBlockIndices; 642 } 643 644 /** 645 * @hide 646 */ getBlockIndex(int index)647 public int getBlockIndex(int index) { 648 return mBlockIndices[index]; 649 } 650 651 /** 652 * @hide 653 * @param index 654 */ setBlockIndex(int index, int blockIndex)655 public void setBlockIndex(int index, int blockIndex) { 656 mBlockIndices[index] = blockIndex; 657 } 658 659 /** 660 * @hide 661 */ getNumberOfBlocks()662 public int getNumberOfBlocks() { 663 return mNumberOfBlocks; 664 } 665 666 /** 667 * @hide 668 */ getIndexFirstChangedBlock()669 public int getIndexFirstChangedBlock() { 670 return mIndexFirstChangedBlock; 671 } 672 673 /** 674 * @hide 675 */ setIndexFirstChangedBlock(int i)676 public void setIndexFirstChangedBlock(int i) { 677 mIndexFirstChangedBlock = i; 678 } 679 680 @Override getLineCount()681 public int getLineCount() { 682 return mInts.size() - 1; 683 } 684 685 @Override getLineTop(int line)686 public int getLineTop(int line) { 687 return mInts.getValue(line, TOP); 688 } 689 690 @Override getLineDescent(int line)691 public int getLineDescent(int line) { 692 return mInts.getValue(line, DESCENT); 693 } 694 695 @Override getLineStart(int line)696 public int getLineStart(int line) { 697 return mInts.getValue(line, START) & START_MASK; 698 } 699 700 @Override getLineContainsTab(int line)701 public boolean getLineContainsTab(int line) { 702 return (mInts.getValue(line, TAB) & TAB_MASK) != 0; 703 } 704 705 @Override getParagraphDirection(int line)706 public int getParagraphDirection(int line) { 707 return mInts.getValue(line, DIR) >> DIR_SHIFT; 708 } 709 710 @Override getLineDirections(int line)711 public final Directions getLineDirections(int line) { 712 return mObjects.getValue(line, 0); 713 } 714 715 @Override getTopPadding()716 public int getTopPadding() { 717 return mTopPadding; 718 } 719 720 @Override getBottomPadding()721 public int getBottomPadding() { 722 return mBottomPadding; 723 } 724 725 /** 726 * @hide 727 */ 728 @Override getHyphen(int line)729 public int getHyphen(int line) { 730 return mInts.getValue(line, HYPHEN) & HYPHEN_MASK; 731 } 732 getContentMayProtrudeFromTopOrBottom(int line)733 private boolean getContentMayProtrudeFromTopOrBottom(int line) { 734 return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM) 735 & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0; 736 } 737 738 @Override getEllipsizedWidth()739 public int getEllipsizedWidth() { 740 return mEllipsizedWidth; 741 } 742 743 private static class ChangeWatcher implements TextWatcher, SpanWatcher { ChangeWatcher(DynamicLayout layout)744 public ChangeWatcher(DynamicLayout layout) { 745 mLayout = new WeakReference<DynamicLayout>(layout); 746 } 747 reflow(CharSequence s, int where, int before, int after)748 private void reflow(CharSequence s, int where, int before, int after) { 749 DynamicLayout ml = mLayout.get(); 750 751 if (ml != null) 752 ml.reflow(s, where, before, after); 753 else if (s instanceof Spannable) 754 ((Spannable) s).removeSpan(this); 755 } 756 beforeTextChanged(CharSequence s, int where, int before, int after)757 public void beforeTextChanged(CharSequence s, int where, int before, int after) { 758 // Intentionally empty 759 } 760 onTextChanged(CharSequence s, int where, int before, int after)761 public void onTextChanged(CharSequence s, int where, int before, int after) { 762 reflow(s, where, before, after); 763 } 764 afterTextChanged(Editable s)765 public void afterTextChanged(Editable s) { 766 // Intentionally empty 767 } 768 onSpanAdded(Spannable s, Object o, int start, int end)769 public void onSpanAdded(Spannable s, Object o, int start, int end) { 770 if (o instanceof UpdateLayout) 771 reflow(s, start, end - start, end - start); 772 } 773 onSpanRemoved(Spannable s, Object o, int start, int end)774 public void onSpanRemoved(Spannable s, Object o, int start, int end) { 775 if (o instanceof UpdateLayout) 776 reflow(s, start, end - start, end - start); 777 } 778 onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend)779 public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { 780 if (o instanceof UpdateLayout) { 781 reflow(s, start, end - start, end - start); 782 reflow(s, nstart, nend - nstart, nend - nstart); 783 } 784 } 785 786 private WeakReference<DynamicLayout> mLayout; 787 } 788 789 @Override getEllipsisStart(int line)790 public int getEllipsisStart(int line) { 791 if (mEllipsizeAt == null) { 792 return 0; 793 } 794 795 return mInts.getValue(line, ELLIPSIS_START); 796 } 797 798 @Override getEllipsisCount(int line)799 public int getEllipsisCount(int line) { 800 if (mEllipsizeAt == null) { 801 return 0; 802 } 803 804 return mInts.getValue(line, ELLIPSIS_COUNT); 805 } 806 807 private CharSequence mBase; 808 private CharSequence mDisplay; 809 private ChangeWatcher mWatcher; 810 private boolean mIncludePad; 811 private boolean mEllipsize; 812 private int mEllipsizedWidth; 813 private TextUtils.TruncateAt mEllipsizeAt; 814 private int mBreakStrategy; 815 private int mHyphenationFrequency; 816 private int mJustificationMode; 817 818 private PackedIntVector mInts; 819 private PackedObjectVector<Directions> mObjects; 820 821 /** 822 * Value used in mBlockIndices when a block has been created or recycled and indicating that its 823 * display list needs to be re-created. 824 * @hide 825 */ 826 public static final int INVALID_BLOCK_INDEX = -1; 827 // Stores the line numbers of the last line of each block (inclusive) 828 private int[] mBlockEndLines; 829 // The indices of this block's display list in TextView's internal display list array or 830 // INVALID_BLOCK_INDEX if this block has been invalidated during an edition 831 private int[] mBlockIndices; 832 // Set of blocks that always need to be redrawn. 833 private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn; 834 // Number of items actually currently being used in the above 2 arrays 835 private int mNumberOfBlocks; 836 // The first index of the blocks whose locations are changed 837 private int mIndexFirstChangedBlock; 838 839 private int mTopPadding, mBottomPadding; 840 841 private Rect mTempRect = new Rect(); 842 843 private static StaticLayout sStaticLayout = null; 844 private static StaticLayout.Builder sBuilder = null; 845 846 private static final Object[] sLock = new Object[0]; 847 848 // START, DIR, and TAB share the same entry. 849 private static final int START = 0; 850 private static final int DIR = START; 851 private static final int TAB = START; 852 private static final int TOP = 1; 853 private static final int DESCENT = 2; 854 // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry. 855 private static final int HYPHEN = 3; 856 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN; 857 private static final int COLUMNS_NORMAL = 4; 858 859 private static final int ELLIPSIS_START = 4; 860 private static final int ELLIPSIS_COUNT = 5; 861 private static final int COLUMNS_ELLIPSIZE = 6; 862 863 private static final int START_MASK = 0x1FFFFFFF; 864 private static final int DIR_SHIFT = 30; 865 private static final int TAB_MASK = 0x20000000; 866 private static final int HYPHEN_MASK = 0xFF; 867 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100; 868 869 private static final int ELLIPSIS_UNDEFINED = 0x80000000; 870 } 871