DynamicLayout.java revision fb0b2dc754fe54269dc8aac86b6e8c1458f96013
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 17package android.text; 18 19import android.graphics.Paint; 20import android.graphics.Rect; 21import android.text.style.ReplacementSpan; 22import android.text.style.UpdateLayout; 23import android.text.style.WrapTogetherSpan; 24import android.util.ArraySet; 25 26import com.android.internal.annotations.VisibleForTesting; 27import com.android.internal.util.ArrayUtils; 28import com.android.internal.util.GrowingArrayUtils; 29 30import 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 */ 40public 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 */ 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 */ 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 */ 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 */ 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 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 .setAddLastLineLineSpacing(!islast); 308 309 reflowed.generate(b, false /*includepad*/, true /*trackpad*/); 310 int n = reflowed.getLineCount(); 311 // If the new layout has a blank line at the end, but it is not 312 // the very end of the buffer, then we already have a line that 313 // starts there, so disregard the blank line. 314 315 if (where + after != len && reflowed.getLineStart(n - 1) == where + after) 316 n--; 317 318 // remove affected lines from old layout 319 mInts.deleteAt(startline, endline - startline); 320 mObjects.deleteAt(startline, endline - startline); 321 322 // adjust offsets in layout for new height and offsets 323 324 int ht = reflowed.getLineTop(n); 325 int toppad = 0, botpad = 0; 326 327 if (mIncludePad && startline == 0) { 328 toppad = reflowed.getTopPadding(); 329 mTopPadding = toppad; 330 ht -= toppad; 331 } 332 if (mIncludePad && islast) { 333 botpad = reflowed.getBottomPadding(); 334 mBottomPadding = botpad; 335 ht += botpad; 336 } 337 338 mInts.adjustValuesBelow(startline, START, after - before); 339 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht); 340 341 // insert new layout 342 343 int[] ints; 344 345 if (mEllipsize) { 346 ints = new int[COLUMNS_ELLIPSIZE]; 347 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 348 } else { 349 ints = new int[COLUMNS_NORMAL]; 350 } 351 352 Directions[] objects = new Directions[1]; 353 354 for (int i = 0; i < n; i++) { 355 final int start = reflowed.getLineStart(i); 356 ints[START] = start; 357 ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT; 358 ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0; 359 360 int top = reflowed.getLineTop(i) + startv; 361 if (i > 0) 362 top -= toppad; 363 ints[TOP] = top; 364 365 int desc = reflowed.getLineDescent(i); 366 if (i == n - 1) 367 desc += botpad; 368 369 ints[DESCENT] = desc; 370 ints[EXTRA] = reflowed.getLineExtra(i); 371 objects[0] = reflowed.getLineDirections(i); 372 373 final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1); 374 ints[HYPHEN] = reflowed.getHyphen(i) & HYPHEN_MASK; 375 ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |= 376 contentMayProtrudeFromLineTopOrBottom(text, start, end) ? 377 MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0; 378 379 if (mEllipsize) { 380 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); 381 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); 382 } 383 384 mInts.insertAt(startline + i, ints); 385 mObjects.insertAt(startline + i, objects); 386 } 387 388 updateBlocks(startline, endline - 1, n); 389 390 b.finish(); 391 synchronized (sLock) { 392 sStaticLayout = reflowed; 393 sBuilder = b; 394 } 395 } 396 397 private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) { 398 if (text instanceof Spanned) { 399 final Spanned spanned = (Spanned) text; 400 if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) { 401 return true; 402 } 403 } 404 // Spans other than ReplacementSpan can be ignored because line top and bottom are 405 // disjunction of all tops and bottoms, although it's not optimal. 406 final Paint paint = getPaint(); 407 paint.getTextBounds(text, start, end, mTempRect); 408 final Paint.FontMetricsInt fm = paint.getFontMetricsInt(); 409 return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom; 410 } 411 412 /** 413 * Create the initial block structure, cutting the text into blocks of at least 414 * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs. 415 */ 416 private void createBlocks() { 417 int offset = BLOCK_MINIMUM_CHARACTER_LENGTH; 418 mNumberOfBlocks = 0; 419 final CharSequence text = mDisplay; 420 421 while (true) { 422 offset = TextUtils.indexOf(text, '\n', offset); 423 if (offset < 0) { 424 addBlockAtOffset(text.length()); 425 break; 426 } else { 427 addBlockAtOffset(offset); 428 offset += BLOCK_MINIMUM_CHARACTER_LENGTH; 429 } 430 } 431 432 // mBlockIndices and mBlockEndLines should have the same length 433 mBlockIndices = new int[mBlockEndLines.length]; 434 for (int i = 0; i < mBlockEndLines.length; i++) { 435 mBlockIndices[i] = INVALID_BLOCK_INDEX; 436 } 437 } 438 439 /** 440 * @hide 441 */ 442 public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() { 443 return mBlocksAlwaysNeedToBeRedrawn; 444 } 445 446 private void updateAlwaysNeedsToBeRedrawn(int blockIndex) { 447 int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1); 448 int endLine = mBlockEndLines[blockIndex]; 449 for (int i = startLine; i <= endLine; i++) { 450 if (getContentMayProtrudeFromTopOrBottom(i)) { 451 if (mBlocksAlwaysNeedToBeRedrawn == null) { 452 mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>(); 453 } 454 mBlocksAlwaysNeedToBeRedrawn.add(blockIndex); 455 return; 456 } 457 } 458 if (mBlocksAlwaysNeedToBeRedrawn != null) { 459 mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex); 460 } 461 } 462 463 /** 464 * Create a new block, ending at the specified character offset. 465 * A block will actually be created only if has at least one line, i.e. this offset is 466 * not on the end line of the previous block. 467 */ 468 private void addBlockAtOffset(int offset) { 469 final int line = getLineForOffset(offset); 470 if (mBlockEndLines == null) { 471 // Initial creation of the array, no test on previous block ending line 472 mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1); 473 mBlockEndLines[mNumberOfBlocks] = line; 474 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks); 475 mNumberOfBlocks++; 476 return; 477 } 478 479 final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1]; 480 if (line > previousBlockEndLine) { 481 mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line); 482 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks); 483 mNumberOfBlocks++; 484 } 485 } 486 487 /** 488 * This method is called every time the layout is reflowed after an edition. 489 * It updates the internal block data structure. The text is split in blocks 490 * of contiguous lines, with at least one block for the entire text. 491 * When a range of lines is edited, new blocks (from 0 to 3 depending on the 492 * overlap structure) will replace the set of overlapping blocks. 493 * Blocks are listed in order and are represented by their ending line number. 494 * An index is associated to each block (which will be used by display lists), 495 * this class simply invalidates the index of blocks overlapping a modification. 496 * 497 * @param startLine the first line of the range of modified lines 498 * @param endLine the last line of the range, possibly equal to startLine, lower 499 * than getLineCount() 500 * @param newLineCount the number of lines that will replace the range, possibly 0 501 * 502 * @hide 503 */ 504 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 505 public void updateBlocks(int startLine, int endLine, int newLineCount) { 506 if (mBlockEndLines == null) { 507 createBlocks(); 508 return; 509 } 510 511 int firstBlock = -1; 512 int lastBlock = -1; 513 for (int i = 0; i < mNumberOfBlocks; i++) { 514 if (mBlockEndLines[i] >= startLine) { 515 firstBlock = i; 516 break; 517 } 518 } 519 for (int i = firstBlock; i < mNumberOfBlocks; i++) { 520 if (mBlockEndLines[i] >= endLine) { 521 lastBlock = i; 522 break; 523 } 524 } 525 final int lastBlockEndLine = mBlockEndLines[lastBlock]; 526 527 boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 : 528 mBlockEndLines[firstBlock - 1] + 1); 529 boolean createBlock = newLineCount > 0; 530 boolean createBlockAfter = endLine < mBlockEndLines[lastBlock]; 531 532 int numAddedBlocks = 0; 533 if (createBlockBefore) numAddedBlocks++; 534 if (createBlock) numAddedBlocks++; 535 if (createBlockAfter) numAddedBlocks++; 536 537 final int numRemovedBlocks = lastBlock - firstBlock + 1; 538 final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks; 539 540 if (newNumberOfBlocks == 0) { 541 // Even when text is empty, there is actually one line and hence one block 542 mBlockEndLines[0] = 0; 543 mBlockIndices[0] = INVALID_BLOCK_INDEX; 544 mNumberOfBlocks = 1; 545 return; 546 } 547 548 if (newNumberOfBlocks > mBlockEndLines.length) { 549 int[] blockEndLines = ArrayUtils.newUnpaddedIntArray( 550 Math.max(mBlockEndLines.length * 2, newNumberOfBlocks)); 551 int[] blockIndices = new int[blockEndLines.length]; 552 System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock); 553 System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock); 554 System.arraycopy(mBlockEndLines, lastBlock + 1, 555 blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 556 System.arraycopy(mBlockIndices, lastBlock + 1, 557 blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 558 mBlockEndLines = blockEndLines; 559 mBlockIndices = blockIndices; 560 } else if (numAddedBlocks + numRemovedBlocks != 0) { 561 System.arraycopy(mBlockEndLines, lastBlock + 1, 562 mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 563 System.arraycopy(mBlockIndices, lastBlock + 1, 564 mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 565 } 566 567 if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) { 568 final ArraySet<Integer> set = new ArraySet<>(); 569 for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) { 570 Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i); 571 if (block > firstBlock) { 572 block += numAddedBlocks - numRemovedBlocks; 573 } 574 set.add(block); 575 } 576 mBlocksAlwaysNeedToBeRedrawn = set; 577 } 578 579 mNumberOfBlocks = newNumberOfBlocks; 580 int newFirstChangedBlock; 581 final int deltaLines = newLineCount - (endLine - startLine + 1); 582 if (deltaLines != 0) { 583 // Display list whose index is >= mIndexFirstChangedBlock is valid 584 // but it needs to update its drawing location. 585 newFirstChangedBlock = firstBlock + numAddedBlocks; 586 for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) { 587 mBlockEndLines[i] += deltaLines; 588 } 589 } else { 590 newFirstChangedBlock = mNumberOfBlocks; 591 } 592 mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock); 593 594 int blockIndex = firstBlock; 595 if (createBlockBefore) { 596 mBlockEndLines[blockIndex] = startLine - 1; 597 updateAlwaysNeedsToBeRedrawn(blockIndex); 598 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 599 blockIndex++; 600 } 601 602 if (createBlock) { 603 mBlockEndLines[blockIndex] = startLine + newLineCount - 1; 604 updateAlwaysNeedsToBeRedrawn(blockIndex); 605 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 606 blockIndex++; 607 } 608 609 if (createBlockAfter) { 610 mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines; 611 updateAlwaysNeedsToBeRedrawn(blockIndex); 612 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 613 } 614 } 615 616 /** 617 * This method is used for test purposes only. 618 * @hide 619 */ 620 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 621 public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks, 622 int totalLines) { 623 mBlockEndLines = new int[blockEndLines.length]; 624 mBlockIndices = new int[blockIndices.length]; 625 System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length); 626 System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length); 627 mNumberOfBlocks = numberOfBlocks; 628 while (mInts.size() < totalLines) { 629 mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]); 630 } 631 } 632 633 /** 634 * @hide 635 */ 636 public int[] getBlockEndLines() { 637 return mBlockEndLines; 638 } 639 640 /** 641 * @hide 642 */ 643 public int[] getBlockIndices() { 644 return mBlockIndices; 645 } 646 647 /** 648 * @hide 649 */ 650 public int getBlockIndex(int index) { 651 return mBlockIndices[index]; 652 } 653 654 /** 655 * @hide 656 * @param index 657 */ 658 public void setBlockIndex(int index, int blockIndex) { 659 mBlockIndices[index] = blockIndex; 660 } 661 662 /** 663 * @hide 664 */ 665 public int getNumberOfBlocks() { 666 return mNumberOfBlocks; 667 } 668 669 /** 670 * @hide 671 */ 672 public int getIndexFirstChangedBlock() { 673 return mIndexFirstChangedBlock; 674 } 675 676 /** 677 * @hide 678 */ 679 public void setIndexFirstChangedBlock(int i) { 680 mIndexFirstChangedBlock = i; 681 } 682 683 @Override 684 public int getLineCount() { 685 return mInts.size() - 1; 686 } 687 688 @Override 689 public int getLineTop(int line) { 690 return mInts.getValue(line, TOP); 691 } 692 693 @Override 694 public int getLineDescent(int line) { 695 return mInts.getValue(line, DESCENT); 696 } 697 698 /** 699 * @hide 700 */ 701 @Override 702 public int getLineExtra(int line) { 703 return mInts.getValue(line, EXTRA); 704 } 705 706 @Override 707 public int getLineStart(int line) { 708 return mInts.getValue(line, START) & START_MASK; 709 } 710 711 @Override 712 public boolean getLineContainsTab(int line) { 713 return (mInts.getValue(line, TAB) & TAB_MASK) != 0; 714 } 715 716 @Override 717 public int getParagraphDirection(int line) { 718 return mInts.getValue(line, DIR) >> DIR_SHIFT; 719 } 720 721 @Override 722 public final Directions getLineDirections(int line) { 723 return mObjects.getValue(line, 0); 724 } 725 726 @Override 727 public int getTopPadding() { 728 return mTopPadding; 729 } 730 731 @Override 732 public int getBottomPadding() { 733 return mBottomPadding; 734 } 735 736 /** 737 * @hide 738 */ 739 @Override 740 public int getHyphen(int line) { 741 return mInts.getValue(line, HYPHEN) & HYPHEN_MASK; 742 } 743 744 private boolean getContentMayProtrudeFromTopOrBottom(int line) { 745 return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM) 746 & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0; 747 } 748 749 @Override 750 public int getEllipsizedWidth() { 751 return mEllipsizedWidth; 752 } 753 754 private static class ChangeWatcher implements TextWatcher, SpanWatcher { 755 public ChangeWatcher(DynamicLayout layout) { 756 mLayout = new WeakReference<DynamicLayout>(layout); 757 } 758 759 private void reflow(CharSequence s, int where, int before, int after) { 760 DynamicLayout ml = mLayout.get(); 761 762 if (ml != null) 763 ml.reflow(s, where, before, after); 764 else if (s instanceof Spannable) 765 ((Spannable) s).removeSpan(this); 766 } 767 768 public void beforeTextChanged(CharSequence s, int where, int before, int after) { 769 // Intentionally empty 770 } 771 772 public void onTextChanged(CharSequence s, int where, int before, int after) { 773 reflow(s, where, before, after); 774 } 775 776 public void afterTextChanged(Editable s) { 777 // Intentionally empty 778 } 779 780 public void onSpanAdded(Spannable s, Object o, int start, int end) { 781 if (o instanceof UpdateLayout) 782 reflow(s, start, end - start, end - start); 783 } 784 785 public void onSpanRemoved(Spannable s, Object o, int start, int end) { 786 if (o instanceof UpdateLayout) 787 reflow(s, start, end - start, end - start); 788 } 789 790 public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { 791 if (o instanceof UpdateLayout) { 792 reflow(s, start, end - start, end - start); 793 reflow(s, nstart, nend - nstart, nend - nstart); 794 } 795 } 796 797 private WeakReference<DynamicLayout> mLayout; 798 } 799 800 @Override 801 public int getEllipsisStart(int line) { 802 if (mEllipsizeAt == null) { 803 return 0; 804 } 805 806 return mInts.getValue(line, ELLIPSIS_START); 807 } 808 809 @Override 810 public int getEllipsisCount(int line) { 811 if (mEllipsizeAt == null) { 812 return 0; 813 } 814 815 return mInts.getValue(line, ELLIPSIS_COUNT); 816 } 817 818 private CharSequence mBase; 819 private CharSequence mDisplay; 820 private ChangeWatcher mWatcher; 821 private boolean mIncludePad; 822 private boolean mEllipsize; 823 private int mEllipsizedWidth; 824 private TextUtils.TruncateAt mEllipsizeAt; 825 private int mBreakStrategy; 826 private int mHyphenationFrequency; 827 private int mJustificationMode; 828 829 private PackedIntVector mInts; 830 private PackedObjectVector<Directions> mObjects; 831 832 /** 833 * Value used in mBlockIndices when a block has been created or recycled and indicating that its 834 * display list needs to be re-created. 835 * @hide 836 */ 837 public static final int INVALID_BLOCK_INDEX = -1; 838 // Stores the line numbers of the last line of each block (inclusive) 839 private int[] mBlockEndLines; 840 // The indices of this block's display list in TextView's internal display list array or 841 // INVALID_BLOCK_INDEX if this block has been invalidated during an edition 842 private int[] mBlockIndices; 843 // Set of blocks that always need to be redrawn. 844 private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn; 845 // Number of items actually currently being used in the above 2 arrays 846 private int mNumberOfBlocks; 847 // The first index of the blocks whose locations are changed 848 private int mIndexFirstChangedBlock; 849 850 private int mTopPadding, mBottomPadding; 851 852 private Rect mTempRect = new Rect(); 853 854 private static StaticLayout sStaticLayout = null; 855 private static StaticLayout.Builder sBuilder = null; 856 857 private static final Object[] sLock = new Object[0]; 858 859 // START, DIR, and TAB share the same entry. 860 private static final int START = 0; 861 private static final int DIR = START; 862 private static final int TAB = START; 863 private static final int TOP = 1; 864 private static final int DESCENT = 2; 865 private static final int EXTRA = 3; 866 // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry. 867 private static final int HYPHEN = 4; 868 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN; 869 private static final int COLUMNS_NORMAL = 5; 870 871 private static final int ELLIPSIS_START = 5; 872 private static final int ELLIPSIS_COUNT = 6; 873 private static final int COLUMNS_ELLIPSIZE = 7; 874 875 private static final int START_MASK = 0x1FFFFFFF; 876 private static final int DIR_SHIFT = 30; 877 private static final int TAB_MASK = 0x20000000; 878 private static final int HYPHEN_MASK = 0xFF; 879 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100; 880 881 private static final int ELLIPSIS_UNDEFINED = 0x80000000; 882} 883