DynamicLayout.java revision 1e130b2abc051081982b5a793a18a28376c945e4
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.text.style.UpdateLayout; 21import android.text.style.WrapTogetherSpan; 22 23import com.android.internal.util.ArrayUtils; 24 25import java.lang.ref.WeakReference; 26 27/** 28 * DynamicLayout is a text layout that updates itself as the text is edited. 29 * <p>This is used by widgets to control text layout. You should not need 30 * to use this class directly unless you are implementing your own widget 31 * or custom display object, or need to call 32 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) 33 * Canvas.drawText()} directly.</p> 34 */ 35public class DynamicLayout extends Layout 36{ 37 private static final int PRIORITY = 128; 38 39 /** 40 * Make a layout for the specified text that will be updated as 41 * the text is changed. 42 */ 43 public DynamicLayout(CharSequence base, 44 TextPaint paint, 45 int width, Alignment align, 46 float spacingmult, float spacingadd, 47 boolean includepad) { 48 this(base, base, paint, width, align, spacingmult, spacingadd, 49 includepad); 50 } 51 52 /** 53 * Make a layout for the transformed text (password transformation 54 * being the primary example of a transformation) 55 * that will be updated as the base text is changed. 56 */ 57 public DynamicLayout(CharSequence base, CharSequence display, 58 TextPaint paint, 59 int width, Alignment align, 60 float spacingmult, float spacingadd, 61 boolean includepad) { 62 this(base, display, paint, width, align, spacingmult, spacingadd, 63 includepad, null, 0); 64 } 65 66 /** 67 * Make a layout for the transformed text (password transformation 68 * being the primary example of a transformation) 69 * that will be updated as the base text is changed. 70 * If ellipsize is non-null, the Layout will ellipsize the text 71 * down to ellipsizedWidth. 72 */ 73 public DynamicLayout(CharSequence base, CharSequence display, 74 TextPaint paint, 75 int width, Alignment align, 76 float spacingmult, float spacingadd, 77 boolean includepad, 78 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 79 this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, 80 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth); 81 } 82 83 /** 84 * Make a layout for the transformed text (password transformation 85 * being the primary example of a transformation) 86 * that will be updated as the base text is changed. 87 * If ellipsize is non-null, the Layout will ellipsize the text 88 * down to ellipsizedWidth. 89 * * 90 * *@hide 91 */ 92 public DynamicLayout(CharSequence base, CharSequence display, 93 TextPaint paint, 94 int width, Alignment align, TextDirectionHeuristic textDir, 95 float spacingmult, float spacingadd, 96 boolean includepad, 97 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 98 super((ellipsize == null) 99 ? display 100 : (display instanceof Spanned) 101 ? new SpannedEllipsizer(display) 102 : new Ellipsizer(display), 103 paint, width, align, textDir, spacingmult, spacingadd); 104 105 mBase = base; 106 mDisplay = display; 107 108 if (ellipsize != null) { 109 mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); 110 mEllipsizedWidth = ellipsizedWidth; 111 mEllipsizeAt = ellipsize; 112 } else { 113 mInts = new PackedIntVector(COLUMNS_NORMAL); 114 mEllipsizedWidth = width; 115 mEllipsizeAt = null; 116 } 117 118 mObjects = new PackedObjectVector<Directions>(1); 119 120 mBlockEnds = new int[] { 0 }; 121 mBlockIndices = new int[] { INVALID_BLOCK_INDEX }; 122 mNumberOfBlocks = 1; 123 124 mIncludePad = includepad; 125 126 /* 127 * This is annoying, but we can't refer to the layout until 128 * superclass construction is finished, and the superclass 129 * constructor wants the reference to the display text. 130 * 131 * This will break if the superclass constructor ever actually 132 * cares about the content instead of just holding the reference. 133 */ 134 if (ellipsize != null) { 135 Ellipsizer e = (Ellipsizer) getText(); 136 137 e.mLayout = this; 138 e.mWidth = ellipsizedWidth; 139 e.mMethod = ellipsize; 140 mEllipsize = true; 141 } 142 143 // Initial state is a single line with 0 characters (0 to 0), 144 // with top at 0 and bottom at whatever is natural, and 145 // undefined ellipsis. 146 147 int[] start; 148 149 if (ellipsize != null) { 150 start = new int[COLUMNS_ELLIPSIZE]; 151 start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 152 } else { 153 start = new int[COLUMNS_NORMAL]; 154 } 155 156 Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT }; 157 158 Paint.FontMetricsInt fm = paint.getFontMetricsInt(); 159 int asc = fm.ascent; 160 int desc = fm.descent; 161 162 start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT; 163 start[TOP] = 0; 164 start[DESCENT] = desc; 165 mInts.insertAt(0, start); 166 167 start[TOP] = desc - asc; 168 mInts.insertAt(1, start); 169 170 mObjects.insertAt(0, dirs); 171 172 // Update from 0 characters to whatever the real text is 173 174 reflow(base, 0, 0, base.length()); 175 176 if (base instanceof Spannable) { 177 if (mWatcher == null) 178 mWatcher = new ChangeWatcher(this); 179 180 // Strip out any watchers for other DynamicLayouts. 181 Spannable sp = (Spannable) base; 182 ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class); 183 for (int i = 0; i < spans.length; i++) 184 sp.removeSpan(spans[i]); 185 186 sp.setSpan(mWatcher, 0, base.length(), 187 Spannable.SPAN_INCLUSIVE_INCLUSIVE | 188 (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT)); 189 } 190 } 191 192 private void reflow(CharSequence s, int where, int before, int after) { 193 if (s != mBase) 194 return; 195 196 CharSequence text = mDisplay; 197 int len = text.length(); 198 199 // seek back to the start of the paragraph 200 201 int find = TextUtils.lastIndexOf(text, '\n', where - 1); 202 if (find < 0) 203 find = 0; 204 else 205 find = find + 1; 206 207 { 208 int diff = where - find; 209 before += diff; 210 after += diff; 211 where -= diff; 212 } 213 214 // seek forward to the end of the paragraph 215 216 int look = TextUtils.indexOf(text, '\n', where + after); 217 if (look < 0) 218 look = len; 219 else 220 look++; // we want the index after the \n 221 222 int change = look - (where + after); 223 before += change; 224 after += change; 225 226 // seek further out to cover anything that is forced to wrap together 227 228 if (text instanceof Spanned) { 229 Spanned sp = (Spanned) text; 230 boolean again; 231 232 do { 233 again = false; 234 235 Object[] force = sp.getSpans(where, where + after, 236 WrapTogetherSpan.class); 237 238 for (int i = 0; i < force.length; i++) { 239 int st = sp.getSpanStart(force[i]); 240 int en = sp.getSpanEnd(force[i]); 241 242 if (st < where) { 243 again = true; 244 245 int diff = where - st; 246 before += diff; 247 after += diff; 248 where -= diff; 249 } 250 251 if (en > where + after) { 252 again = true; 253 254 int diff = en - (where + after); 255 before += diff; 256 after += diff; 257 } 258 } 259 } while (again); 260 } 261 262 // find affected region of old layout 263 264 int startline = getLineForOffset(where); 265 int startv = getLineTop(startline); 266 267 int endline = getLineForOffset(where + before); 268 if (where + after == len) 269 endline = getLineCount(); 270 int endv = getLineTop(endline); 271 boolean islast = (endline == getLineCount()); 272 273 // generate new layout for affected text 274 275 StaticLayout reflowed; 276 277 synchronized (sLock) { 278 reflowed = sStaticLayout; 279 sStaticLayout = null; 280 } 281 282 if (reflowed == null) { 283 reflowed = new StaticLayout(null); 284 } else { 285 reflowed.prepare(); 286 } 287 288 reflowed.generate(text, where, where + after, 289 getPaint(), getWidth(), getTextDirectionHeuristic(), getSpacingMultiplier(), 290 getSpacingAdd(), false, 291 true, mEllipsizedWidth, mEllipsizeAt); 292 int n = reflowed.getLineCount(); 293 294 // If the new layout has a blank line at the end, but it is not 295 // the very end of the buffer, then we already have a line that 296 // starts there, so disregard the blank line. 297 298 if (where + after != len && 299 reflowed.getLineStart(n - 1) == where + after) 300 n--; 301 302 // remove affected lines from old layout 303 mInts.deleteAt(startline, endline - startline); 304 mObjects.deleteAt(startline, endline - startline); 305 updateBlocks(startline, endline - 1, n); 306 307 // adjust offsets in layout for new height and offsets 308 309 int ht = reflowed.getLineTop(n); 310 int toppad = 0, botpad = 0; 311 312 if (mIncludePad && startline == 0) { 313 toppad = reflowed.getTopPadding(); 314 mTopPadding = toppad; 315 ht -= toppad; 316 } 317 if (mIncludePad && islast) { 318 botpad = reflowed.getBottomPadding(); 319 mBottomPadding = botpad; 320 ht += botpad; 321 } 322 323 mInts.adjustValuesBelow(startline, START, after - before); 324 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht); 325 326 // insert new layout 327 328 int[] ints; 329 330 if (mEllipsize) { 331 ints = new int[COLUMNS_ELLIPSIZE]; 332 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 333 } else { 334 ints = new int[COLUMNS_NORMAL]; 335 } 336 337 Directions[] objects = new Directions[1]; 338 339 for (int i = 0; i < n; i++) { 340 ints[START] = reflowed.getLineStart(i) | 341 (reflowed.getParagraphDirection(i) << DIR_SHIFT) | 342 (reflowed.getLineContainsTab(i) ? TAB_MASK : 0); 343 344 int top = reflowed.getLineTop(i) + startv; 345 if (i > 0) 346 top -= toppad; 347 ints[TOP] = top; 348 349 int desc = reflowed.getLineDescent(i); 350 if (i == n - 1) 351 desc += botpad; 352 353 ints[DESCENT] = desc; 354 objects[0] = reflowed.getLineDirections(i); 355 356 if (mEllipsize) { 357 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); 358 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); 359 } 360 361 mInts.insertAt(startline + i, ints); 362 mObjects.insertAt(startline + i, objects); 363 } 364 365 synchronized (sLock) { 366 sStaticLayout = reflowed; 367 reflowed.finish(); 368 } 369 } 370 371 /** 372 * This method is called every time the layout is reflowed after an edition. 373 * It updates the internal block data structure. The text is split in blocks 374 * of contiguous lines, with at least one block for the entire text. 375 * When a range of lines is edited, new blocks (from 0 to 3 depending on the 376 * overlap structure) will replace the set of overlapping blocks. 377 * Blocks are listed in order and are represented by their ending line number. 378 * An index is associated to each block (which will be used by display lists), 379 * this class simply invalidates the index of blocks overlapping a modification. 380 * 381 * This method is package private and not private so that it can be tested. 382 * 383 * @param startLine the first line of the range of modified lines 384 * @param endLine the last line of the range, possibly equal to startLine, lower 385 * than getLineCount() 386 * @param newLineCount the number of lines that will replace the range, possibly 0 387 * 388 * @hide 389 */ 390 void updateBlocks(int startLine, int endLine, int newLineCount) { 391 int firstBlock = -1; 392 int lastBlock = -1; 393 for (int i = 0; i < mNumberOfBlocks; i++) { 394 if (mBlockEnds[i] >= startLine) { 395 firstBlock = i; 396 break; 397 } 398 } 399 for (int i = firstBlock; i < mNumberOfBlocks; i++) { 400 if (mBlockEnds[i] >= endLine) { 401 lastBlock = i; 402 break; 403 } 404 } 405 final int lastBlockEndLine = mBlockEnds[lastBlock]; 406 407 boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 : 408 mBlockEnds[firstBlock - 1] + 1); 409 boolean createBlock = newLineCount > 0; 410 boolean createBlockAfter = endLine < mBlockEnds[lastBlock]; 411 412 int numAddedBlocks = 0; 413 if (createBlockBefore) numAddedBlocks++; 414 if (createBlock) numAddedBlocks++; 415 if (createBlockAfter) numAddedBlocks++; 416 417 final int numRemovedBlocks = lastBlock - firstBlock + 1; 418 final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks; 419 420 if (newNumberOfBlocks == 0) { 421 // Even when text is empty, there is actually one line and hence one block 422 mBlockEnds[0] = 0; 423 mBlockIndices[0] = INVALID_BLOCK_INDEX; 424 mNumberOfBlocks = 1; 425 return; 426 } 427 428 if (newNumberOfBlocks > mBlockEnds.length) { 429 final int newSize = ArrayUtils.idealIntArraySize(newNumberOfBlocks); 430 int[] blockEnds = new int[newSize]; 431 int[] blockIndices = new int[newSize]; 432 System.arraycopy(mBlockEnds, 0, blockEnds, 0, firstBlock); 433 System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock); 434 System.arraycopy(mBlockEnds, lastBlock + 1, 435 blockEnds, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 436 System.arraycopy(mBlockIndices, lastBlock + 1, 437 blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 438 mBlockEnds = blockEnds; 439 mBlockIndices = blockIndices; 440 } else { 441 System.arraycopy(mBlockEnds, lastBlock + 1, 442 mBlockEnds, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 443 System.arraycopy(mBlockIndices, lastBlock + 1, 444 mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1); 445 } 446 447 mNumberOfBlocks = newNumberOfBlocks; 448 final int deltaLines = newLineCount - (endLine - startLine + 1); 449 for (int i = firstBlock + numAddedBlocks; i < mNumberOfBlocks; i++) { 450 mBlockEnds[i] += deltaLines; 451 } 452 453 int blockIndex = firstBlock; 454 if (createBlockBefore) { 455 mBlockEnds[blockIndex] = startLine - 1; 456 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 457 blockIndex++; 458 } 459 460 if (createBlock) { 461 mBlockEnds[blockIndex] = startLine + newLineCount - 1; 462 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 463 blockIndex++; 464 } 465 466 if (createBlockAfter) { 467 mBlockEnds[blockIndex] = lastBlockEndLine + deltaLines; 468 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX; 469 } 470 } 471 472 /** 473 * This package private method is used for test purposes only 474 * @hide 475 */ 476 void setBlocksDataForTest(int[] blockEnds, int[] blockIndices, int numberOfBlocks) { 477 mBlockEnds = new int[blockEnds.length]; 478 mBlockIndices = new int[blockIndices.length]; 479 System.arraycopy(blockEnds, 0, mBlockEnds, 0, blockEnds.length); 480 System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length); 481 mNumberOfBlocks = numberOfBlocks; 482 } 483 484 /** 485 * @hide 486 */ 487 public int[] getBlockEnds() { 488 return mBlockEnds; 489 } 490 491 /** 492 * @hide 493 */ 494 public int[] getBlockIndices() { 495 return mBlockIndices; 496 } 497 498 /** 499 * @hide 500 */ 501 public int getNumberOfBlocks() { 502 return mNumberOfBlocks; 503 } 504 505 @Override 506 public int getLineCount() { 507 return mInts.size() - 1; 508 } 509 510 @Override 511 public int getLineTop(int line) { 512 return mInts.getValue(line, TOP); 513 } 514 515 @Override 516 public int getLineDescent(int line) { 517 return mInts.getValue(line, DESCENT); 518 } 519 520 @Override 521 public int getLineStart(int line) { 522 return mInts.getValue(line, START) & START_MASK; 523 } 524 525 @Override 526 public boolean getLineContainsTab(int line) { 527 return (mInts.getValue(line, TAB) & TAB_MASK) != 0; 528 } 529 530 @Override 531 public int getParagraphDirection(int line) { 532 return mInts.getValue(line, DIR) >> DIR_SHIFT; 533 } 534 535 @Override 536 public final Directions getLineDirections(int line) { 537 return mObjects.getValue(line, 0); 538 } 539 540 @Override 541 public int getTopPadding() { 542 return mTopPadding; 543 } 544 545 @Override 546 public int getBottomPadding() { 547 return mBottomPadding; 548 } 549 550 @Override 551 public int getEllipsizedWidth() { 552 return mEllipsizedWidth; 553 } 554 555 private static class ChangeWatcher implements TextWatcher, SpanWatcher { 556 public ChangeWatcher(DynamicLayout layout) { 557 mLayout = new WeakReference<DynamicLayout>(layout); 558 } 559 560 private void reflow(CharSequence s, int where, int before, int after) { 561 DynamicLayout ml = mLayout.get(); 562 563 if (ml != null) 564 ml.reflow(s, where, before, after); 565 else if (s instanceof Spannable) 566 ((Spannable) s).removeSpan(this); 567 } 568 569 public void beforeTextChanged(CharSequence s, int where, int before, int after) { 570 // Intentionally empty 571 } 572 573 public void onTextChanged(CharSequence s, int where, int before, int after) { 574 reflow(s, where, before, after); 575 } 576 577 public void afterTextChanged(Editable s) { 578 // Intentionally empty 579 } 580 581 public void onSpanAdded(Spannable s, Object o, int start, int end) { 582 if (o instanceof UpdateLayout) 583 reflow(s, start, end - start, end - start); 584 } 585 586 public void onSpanRemoved(Spannable s, Object o, int start, int end) { 587 if (o instanceof UpdateLayout) 588 reflow(s, start, end - start, end - start); 589 } 590 591 public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { 592 if (o instanceof UpdateLayout) { 593 reflow(s, start, end - start, end - start); 594 reflow(s, nstart, nend - nstart, nend - nstart); 595 } 596 } 597 598 private WeakReference<DynamicLayout> mLayout; 599 } 600 601 @Override 602 public int getEllipsisStart(int line) { 603 if (mEllipsizeAt == null) { 604 return 0; 605 } 606 607 return mInts.getValue(line, ELLIPSIS_START); 608 } 609 610 @Override 611 public int getEllipsisCount(int line) { 612 if (mEllipsizeAt == null) { 613 return 0; 614 } 615 616 return mInts.getValue(line, ELLIPSIS_COUNT); 617 } 618 619 private CharSequence mBase; 620 private CharSequence mDisplay; 621 private ChangeWatcher mWatcher; 622 private boolean mIncludePad; 623 private boolean mEllipsize; 624 private int mEllipsizedWidth; 625 private TextUtils.TruncateAt mEllipsizeAt; 626 627 private PackedIntVector mInts; 628 private PackedObjectVector<Directions> mObjects; 629 630 /** 631 * Value used in mBlockIndices when a block has been created or recycled and indicating that its 632 * display list needs to be re-created. 633 * @hide 634 */ 635 public static final int INVALID_BLOCK_INDEX = -1; 636 // Stores the line numbers of the last line of each block 637 private int[] mBlockEnds; 638 // The indices of this block's display list in TextView's internal display list array or 639 // INVALID_BLOCK_INDEX if this block has been invalidated during an edition 640 private int[] mBlockIndices; 641 // Number of items actually currently being used in the above 2 arrays 642 private int mNumberOfBlocks; 643 644 private int mTopPadding, mBottomPadding; 645 646 private static StaticLayout sStaticLayout = new StaticLayout(null); 647 648 private static final Object[] sLock = new Object[0]; 649 650 private static final int START = 0; 651 private static final int DIR = START; 652 private static final int TAB = START; 653 private static final int TOP = 1; 654 private static final int DESCENT = 2; 655 private static final int COLUMNS_NORMAL = 3; 656 657 private static final int ELLIPSIS_START = 3; 658 private static final int ELLIPSIS_COUNT = 4; 659 private static final int COLUMNS_ELLIPSIZE = 5; 660 661 private static final int START_MASK = 0x1FFFFFFF; 662 private static final int DIR_SHIFT = 30; 663 private static final int TAB_MASK = 0x20000000; 664 665 private static final int ELLIPSIS_UNDEFINED = 0x80000000; 666} 667