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 * This method is package private and not private so that it can be tested. 495 * 496 * @param startLine the first line of the range of modified lines 497 * @param endLine the last line of the range, possibly equal to startLine, lower 498 * than getLineCount() 499 * @param newLineCount the number of lines that will replace the range, possibly 0 500 * 501 * @hide 502 */ 503 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) updateBlocks(int startLine, int endLine, int newLineCount)504 public void updateBlocks(int startLine, int endLine, int newLineCount) { 505 if (mBlockEndLines == null) { 506 createBlocks(); 507 return; 508 } 509 510 int firstBlock = -1; 511 int lastBlock = -1; 512 for (int i = 0; i < mNumberOfBlocks; i++) { 513 if (mBlockEndLines[i] >= startLine) { 514 firstBlock = i; 515 break; 516 } 517 } 518 for (int i = firstBlock; i < mNumberOfBlocks; i++) { 519 if (mBlockEndLines[i] >= endLine) { 520 lastBlock = i; 521 break; 522 } 523 } 524 final int lastBlockEndLine = mBlockEndLines[lastBlock]; 525 526 boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 : 527 mBlockEndLines[firstBlock - 1] + 1); 528 boolean createBlock = newLineCount > 0; 529 boolean createBlockAfter = endLine < mBlockEndLines[lastBlock]; 530 531 int numAddedBlocks = 0; 532 if (createBlockBefore) numAddedBlocks++; 533 if (createBlock) numAddedBlocks++; 534 if (createBlockAfter) numAddedBlocks++; 535 536 final int numRemovedBlocks = lastBlock - firstBlock + 1; 537 final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks; 538 539 if (newNumberOfBlocks == 0) { 540 // Even when text is empty, there is actually one line and hence one block 541 mBlockEndLines[0] = 0; 542 mBlockIndices[0] = INVALID_BLOCK_INDEX; 543 mNumberOfBlocks = 1; 544 return; 545 } 546 547 if (newNumberOfBlocks > mBlockEndLines.length) { 548 int[] blockEndLines = ArrayUtils.newUnpaddedIntArray( 549 Math.max(mBlockEndLines.length * 2, newNumberOfBlocks)); 550 int[] blockIndices = new int[blockEndLines.length]; 551 System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock); 552 System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock); 553 System.arraycopy(mBlockEndLines, lastBlock + 1, 554 blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 555 System.arraycopy(mBlockIndices, lastBlock + 1, 556 blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 557 mBlockEndLines = blockEndLines; 558 mBlockIndices = blockIndices; 559 } else if (numAddedBlocks + numRemovedBlocks != 0) { 560 System.arraycopy(mBlockEndLines, lastBlock + 1, 561 mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 562 System.arraycopy(mBlockIndices, lastBlock + 1, 563 mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 564 } 565 566 if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) { 567 final ArraySet<Integer> set = new ArraySet<>(); 568 for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) { 569 Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i); 570 if (block > firstBlock) { 571 block += numAddedBlocks - numRemovedBlocks; 572 } 573 set.add(block); 574 } 575 mBlocksAlwaysNeedToBeRedrawn = set; 576 } 577 578 mNumberOfBlocks = newNumberOfBlocks; 579 int newFirstChangedBlock; 580 final int deltaLines = newLineCount - (endLine - startLine + 1); 581 if (deltaLines != 0) { 582 // Display list whose index is >= mIndexFirstChangedBlock is valid 583 // but it needs to update its drawing location. 584 newFirstChangedBlock = firstBlock + numAddedBlocks; 585 for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) { 586 mBlockEndLines[i] += deltaLines; 587 } 588 } else { 589 newFirstChangedBlock = mNumberOfBlocks; 590 } 591 mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock); 592 593 int blockIndex = firstBlock; 594 if (createBlockBefore) { 595 mBlockEndLines[blockIndex] = startLine - 1; 596 updateAlwaysNeedsToBeRedrawn(blockIndex); 597 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 598 blockIndex++; 599 } 600 601 if (createBlock) { 602 mBlockEndLines[blockIndex] = startLine + newLineCount - 1; 603 updateAlwaysNeedsToBeRedrawn(blockIndex); 604 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 605 blockIndex++; 606 } 607 608 if (createBlockAfter) { 609 mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines; 610 updateAlwaysNeedsToBeRedrawn(blockIndex); 611 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 612 } 613 } 614 615 /** 616 * This package private method is used for test purposes only 617 * @hide 618 */ 619 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks)620 public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks) { 621 mBlockEndLines = new int[blockEndLines.length]; 622 mBlockIndices = new int[blockIndices.length]; 623 System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length); 624 System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length); 625 mNumberOfBlocks = numberOfBlocks; 626 } 627 628 /** 629 * @hide 630 */ getBlockEndLines()631 public int[] getBlockEndLines() { 632 return mBlockEndLines; 633 } 634 635 /** 636 * @hide 637 */ getBlockIndices()638 public int[] getBlockIndices() { 639 return mBlockIndices; 640 } 641 642 /** 643 * @hide 644 */ getBlockIndex(int index)645 public int getBlockIndex(int index) { 646 return mBlockIndices[index]; 647 } 648 649 /** 650 * @hide 651 * @param index 652 */ setBlockIndex(int index, int blockIndex)653 public void setBlockIndex(int index, int blockIndex) { 654 mBlockIndices[index] = blockIndex; 655 } 656 657 /** 658 * @hide 659 */ getNumberOfBlocks()660 public int getNumberOfBlocks() { 661 return mNumberOfBlocks; 662 } 663 664 /** 665 * @hide 666 */ getIndexFirstChangedBlock()667 public int getIndexFirstChangedBlock() { 668 return mIndexFirstChangedBlock; 669 } 670 671 /** 672 * @hide 673 */ setIndexFirstChangedBlock(int i)674 public void setIndexFirstChangedBlock(int i) { 675 mIndexFirstChangedBlock = i; 676 } 677 678 @Override getLineCount()679 public int getLineCount() { 680 return mInts.size() - 1; 681 } 682 683 @Override getLineTop(int line)684 public int getLineTop(int line) { 685 return mInts.getValue(line, TOP); 686 } 687 688 @Override getLineDescent(int line)689 public int getLineDescent(int line) { 690 return mInts.getValue(line, DESCENT); 691 } 692 693 @Override getLineStart(int line)694 public int getLineStart(int line) { 695 return mInts.getValue(line, START) & START_MASK; 696 } 697 698 @Override getLineContainsTab(int line)699 public boolean getLineContainsTab(int line) { 700 return (mInts.getValue(line, TAB) & TAB_MASK) != 0; 701 } 702 703 @Override getParagraphDirection(int line)704 public int getParagraphDirection(int line) { 705 return mInts.getValue(line, DIR) >> DIR_SHIFT; 706 } 707 708 @Override getLineDirections(int line)709 public final Directions getLineDirections(int line) { 710 return mObjects.getValue(line, 0); 711 } 712 713 @Override getTopPadding()714 public int getTopPadding() { 715 return mTopPadding; 716 } 717 718 @Override getBottomPadding()719 public int getBottomPadding() { 720 return mBottomPadding; 721 } 722 723 /** 724 * @hide 725 */ 726 @Override getHyphen(int line)727 public int getHyphen(int line) { 728 return mInts.getValue(line, HYPHEN) & HYPHEN_MASK; 729 } 730 getContentMayProtrudeFromTopOrBottom(int line)731 private boolean getContentMayProtrudeFromTopOrBottom(int line) { 732 return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM) 733 & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0; 734 } 735 736 @Override getEllipsizedWidth()737 public int getEllipsizedWidth() { 738 return mEllipsizedWidth; 739 } 740 741 private static class ChangeWatcher implements TextWatcher, SpanWatcher { ChangeWatcher(DynamicLayout layout)742 public ChangeWatcher(DynamicLayout layout) { 743 mLayout = new WeakReference<DynamicLayout>(layout); 744 } 745 reflow(CharSequence s, int where, int before, int after)746 private void reflow(CharSequence s, int where, int before, int after) { 747 DynamicLayout ml = mLayout.get(); 748 749 if (ml != null) 750 ml.reflow(s, where, before, after); 751 else if (s instanceof Spannable) 752 ((Spannable) s).removeSpan(this); 753 } 754 beforeTextChanged(CharSequence s, int where, int before, int after)755 public void beforeTextChanged(CharSequence s, int where, int before, int after) { 756 // Intentionally empty 757 } 758 onTextChanged(CharSequence s, int where, int before, int after)759 public void onTextChanged(CharSequence s, int where, int before, int after) { 760 reflow(s, where, before, after); 761 } 762 afterTextChanged(Editable s)763 public void afterTextChanged(Editable s) { 764 // Intentionally empty 765 } 766 onSpanAdded(Spannable s, Object o, int start, int end)767 public void onSpanAdded(Spannable s, Object o, int start, int end) { 768 if (o instanceof UpdateLayout) 769 reflow(s, start, end - start, end - start); 770 } 771 onSpanRemoved(Spannable s, Object o, int start, int end)772 public void onSpanRemoved(Spannable s, Object o, int start, int end) { 773 if (o instanceof UpdateLayout) 774 reflow(s, start, end - start, end - start); 775 } 776 onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend)777 public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { 778 if (o instanceof UpdateLayout) { 779 reflow(s, start, end - start, end - start); 780 reflow(s, nstart, nend - nstart, nend - nstart); 781 } 782 } 783 784 private WeakReference<DynamicLayout> mLayout; 785 } 786 787 @Override getEllipsisStart(int line)788 public int getEllipsisStart(int line) { 789 if (mEllipsizeAt == null) { 790 return 0; 791 } 792 793 return mInts.getValue(line, ELLIPSIS_START); 794 } 795 796 @Override getEllipsisCount(int line)797 public int getEllipsisCount(int line) { 798 if (mEllipsizeAt == null) { 799 return 0; 800 } 801 802 return mInts.getValue(line, ELLIPSIS_COUNT); 803 } 804 805 private CharSequence mBase; 806 private CharSequence mDisplay; 807 private ChangeWatcher mWatcher; 808 private boolean mIncludePad; 809 private boolean mEllipsize; 810 private int mEllipsizedWidth; 811 private TextUtils.TruncateAt mEllipsizeAt; 812 private int mBreakStrategy; 813 private int mHyphenationFrequency; 814 private int mJustificationMode; 815 816 private PackedIntVector mInts; 817 private PackedObjectVector<Directions> mObjects; 818 819 /** 820 * Value used in mBlockIndices when a block has been created or recycled and indicating that its 821 * display list needs to be re-created. 822 * @hide 823 */ 824 public static final int INVALID_BLOCK_INDEX = -1; 825 // Stores the line numbers of the last line of each block (inclusive) 826 private int[] mBlockEndLines; 827 // The indices of this block's display list in TextView's internal display list array or 828 // INVALID_BLOCK_INDEX if this block has been invalidated during an edition 829 private int[] mBlockIndices; 830 // Set of blocks that always need to be redrawn. 831 private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn; 832 // Number of items actually currently being used in the above 2 arrays 833 private int mNumberOfBlocks; 834 // The first index of the blocks whose locations are changed 835 private int mIndexFirstChangedBlock; 836 837 private int mTopPadding, mBottomPadding; 838 839 private Rect mTempRect = new Rect(); 840 841 private static StaticLayout sStaticLayout = null; 842 private static StaticLayout.Builder sBuilder = null; 843 844 private static final Object[] sLock = new Object[0]; 845 846 // START, DIR, and TAB share the same entry. 847 private static final int START = 0; 848 private static final int DIR = START; 849 private static final int TAB = START; 850 private static final int TOP = 1; 851 private static final int DESCENT = 2; 852 // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry. 853 private static final int HYPHEN = 3; 854 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN; 855 private static final int COLUMNS_NORMAL = 4; 856 857 private static final int ELLIPSIS_START = 4; 858 private static final int ELLIPSIS_COUNT = 5; 859 private static final int COLUMNS_ELLIPSIZE = 6; 860 861 private static final int START_MASK = 0x1FFFFFFF; 862 private static final int DIR_SHIFT = 30; 863 private static final int TAB_MASK = 0x20000000; 864 private static final int HYPHEN_MASK = 0xFF; 865 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100; 866 867 private static final int ELLIPSIS_UNDEFINED = 0x80000000; 868 } 869