DynamicLayout.java revision 9f7a4442b89cc06cb8cae6992484e7ae795323ab
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 java.lang.ref.WeakReference; 24 25/** 26 * DynamicLayout is a text layout that updates itself as the text is edited. 27 * <p>This is used by widgets to control text layout. You should not need 28 * to use this class directly unless you are implementing your own widget 29 * or custom display object, or need to call 30 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) 31 * Canvas.drawText()} directly.</p> 32 */ 33public class DynamicLayout 34extends Layout 35{ 36 private static final int PRIORITY = 128; 37 38 /** 39 * Make a layout for the specified text that will be updated as 40 * the text is changed. 41 */ 42 public DynamicLayout(CharSequence base, 43 TextPaint paint, 44 int width, Alignment align, 45 float spacingmult, float spacingadd, 46 boolean includepad) { 47 this(base, base, paint, width, align, spacingmult, spacingadd, 48 includepad); 49 } 50 51 /** 52 * Make a layout for the transformed text (password transformation 53 * being the primary example of a transformation) 54 * that will be updated as the base text is changed. 55 */ 56 public DynamicLayout(CharSequence base, CharSequence display, 57 TextPaint paint, 58 int width, Alignment align, 59 float spacingmult, float spacingadd, 60 boolean includepad) { 61 this(base, display, paint, width, align, spacingmult, spacingadd, 62 includepad, null, 0); 63 } 64 65 /** 66 * Make a layout for the transformed text (password transformation 67 * being the primary example of a transformation) 68 * that will be updated as the base text is changed. 69 * If ellipsize is non-null, the Layout will ellipsize the text 70 * down to ellipsizedWidth. 71 */ 72 public DynamicLayout(CharSequence base, CharSequence display, 73 TextPaint paint, 74 int width, Alignment align, 75 float spacingmult, float spacingadd, 76 boolean includepad, 77 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 78 super((ellipsize == null) 79 ? display 80 : (display instanceof Spanned) 81 ? new SpannedEllipsizer(display) 82 : new Ellipsizer(display), 83 paint, width, align, spacingmult, spacingadd); 84 85 mBase = base; 86 mDisplay = display; 87 88 if (ellipsize != null) { 89 mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); 90 mEllipsizedWidth = ellipsizedWidth; 91 mEllipsizeAt = ellipsize; 92 } else { 93 mInts = new PackedIntVector(COLUMNS_NORMAL); 94 mEllipsizedWidth = width; 95 mEllipsizeAt = ellipsize; 96 } 97 98 mObjects = new PackedObjectVector<Directions>(1); 99 100 mIncludePad = includepad; 101 102 /* 103 * This is annoying, but we can't refer to the layout until 104 * superclass construction is finished, and the superclass 105 * constructor wants the reference to the display text. 106 * 107 * This will break if the superclass constructor ever actually 108 * cares about the content instead of just holding the reference. 109 */ 110 if (ellipsize != null) { 111 Ellipsizer e = (Ellipsizer) getText(); 112 113 e.mLayout = this; 114 e.mWidth = ellipsizedWidth; 115 e.mMethod = ellipsize; 116 mEllipsize = true; 117 } 118 119 // Initial state is a single line with 0 characters (0 to 0), 120 // with top at 0 and bottom at whatever is natural, and 121 // undefined ellipsis. 122 123 int[] start; 124 125 if (ellipsize != null) { 126 start = new int[COLUMNS_ELLIPSIZE]; 127 start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 128 } else { 129 start = new int[COLUMNS_NORMAL]; 130 } 131 132 Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT }; 133 134 Paint.FontMetricsInt fm = paint.getFontMetricsInt(); 135 int asc = fm.ascent; 136 int desc = fm.descent; 137 138 start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT; 139 start[TOP] = 0; 140 start[DESCENT] = desc; 141 mInts.insertAt(0, start); 142 143 start[TOP] = desc - asc; 144 mInts.insertAt(1, start); 145 146 mObjects.insertAt(0, dirs); 147 148 // Update from 0 characters to whatever the real text is 149 150 reflow(base, 0, 0, base.length()); 151 152 if (base instanceof Spannable) { 153 if (mWatcher == null) 154 mWatcher = new ChangeWatcher(this); 155 156 // Strip out any watchers for other DynamicLayouts. 157 Spannable sp = (Spannable) base; 158 ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class); 159 for (int i = 0; i < spans.length; i++) 160 sp.removeSpan(spans[i]); 161 162 sp.setSpan(mWatcher, 0, base.length(), 163 Spannable.SPAN_INCLUSIVE_INCLUSIVE | 164 (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT)); 165 } 166 } 167 168 private void reflow(CharSequence s, int where, int before, int after) { 169 if (s != mBase) 170 return; 171 172 CharSequence text = mDisplay; 173 int len = text.length(); 174 175 // seek back to the start of the paragraph 176 177 int find = TextUtils.lastIndexOf(text, '\n', where - 1); 178 if (find < 0) 179 find = 0; 180 else 181 find = find + 1; 182 183 { 184 int diff = where - find; 185 before += diff; 186 after += diff; 187 where -= diff; 188 } 189 190 // seek forward to the end of the paragraph 191 192 int look = TextUtils.indexOf(text, '\n', where + after); 193 if (look < 0) 194 look = len; 195 else 196 look++; // we want the index after the \n 197 198 int change = look - (where + after); 199 before += change; 200 after += change; 201 202 // seek further out to cover anything that is forced to wrap together 203 204 if (text instanceof Spanned) { 205 Spanned sp = (Spanned) text; 206 boolean again; 207 208 do { 209 again = false; 210 211 Object[] force = sp.getSpans(where, where + after, 212 WrapTogetherSpan.class); 213 214 for (int i = 0; i < force.length; i++) { 215 int st = sp.getSpanStart(force[i]); 216 int en = sp.getSpanEnd(force[i]); 217 218 if (st < where) { 219 again = true; 220 221 int diff = where - st; 222 before += diff; 223 after += diff; 224 where -= diff; 225 } 226 227 if (en > where + after) { 228 again = true; 229 230 int diff = en - (where + after); 231 before += diff; 232 after += diff; 233 } 234 } 235 } while (again); 236 } 237 238 // find affected region of old layout 239 240 int startline = getLineForOffset(where); 241 int startv = getLineTop(startline); 242 243 int endline = getLineForOffset(where + before); 244 if (where + after == len) 245 endline = getLineCount(); 246 int endv = getLineTop(endline); 247 boolean islast = (endline == getLineCount()); 248 249 // generate new layout for affected text 250 251 StaticLayout reflowed; 252 253 synchronized (sLock) { 254 reflowed = sStaticLayout; 255 sStaticLayout = null; 256 } 257 258 if (reflowed == null) 259 reflowed = new StaticLayout(true); 260 261 reflowed.generate(text, where, where + after, 262 getPaint(), getWidth(), getAlignment(), 263 getSpacingMultiplier(), getSpacingAdd(), 264 false, true, mEllipsize, 265 mEllipsizedWidth, mEllipsizeAt); 266 int n = reflowed.getLineCount(); 267 268 // If the new layout has a blank line at the end, but it is not 269 // the very end of the buffer, then we already have a line that 270 // starts there, so disregard the blank line. 271 272 if (where + after != len && 273 reflowed.getLineStart(n - 1) == where + after) 274 n--; 275 276 // remove affected lines from old layout 277 278 mInts.deleteAt(startline, endline - startline); 279 mObjects.deleteAt(startline, endline - startline); 280 281 // adjust offsets in layout for new height and offsets 282 283 int ht = reflowed.getLineTop(n); 284 int toppad = 0, botpad = 0; 285 286 if (mIncludePad && startline == 0) { 287 toppad = reflowed.getTopPadding(); 288 mTopPadding = toppad; 289 ht -= toppad; 290 } 291 if (mIncludePad && islast) { 292 botpad = reflowed.getBottomPadding(); 293 mBottomPadding = botpad; 294 ht += botpad; 295 } 296 297 mInts.adjustValuesBelow(startline, START, after - before); 298 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht); 299 300 // insert new layout 301 302 int[] ints; 303 304 if (mEllipsize) { 305 ints = new int[COLUMNS_ELLIPSIZE]; 306 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 307 } else { 308 ints = new int[COLUMNS_NORMAL]; 309 } 310 311 Directions[] objects = new Directions[1]; 312 313 for (int i = 0; i < n; i++) { 314 ints[START] = reflowed.getLineStart(i) | 315 (reflowed.getParagraphDirection(i) << DIR_SHIFT) | 316 (reflowed.getLineContainsTab(i) ? TAB_MASK : 0); 317 318 int top = reflowed.getLineTop(i) + startv; 319 if (i > 0) 320 top -= toppad; 321 ints[TOP] = top; 322 323 int desc = reflowed.getLineDescent(i); 324 if (i == n - 1) 325 desc += botpad; 326 327 ints[DESCENT] = desc; 328 objects[0] = reflowed.getLineDirections(i); 329 330 if (mEllipsize) { 331 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); 332 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); 333 } 334 335 mInts.insertAt(startline + i, ints); 336 mObjects.insertAt(startline + i, objects); 337 } 338 339 synchronized (sLock) { 340 sStaticLayout = reflowed; 341 } 342 } 343 344 private void dump(boolean show) { 345 int n = getLineCount(); 346 347 for (int i = 0; i < n; i++) { 348 System.out.print("line " + i + ": " + getLineStart(i) + " to " + getLineEnd(i) + " "); 349 350 if (show) { 351 System.out.print(getText().subSequence(getLineStart(i), 352 getLineEnd(i))); 353 } 354 355 System.out.println(""); 356 } 357 358 System.out.println(""); 359 } 360 361 public int getLineCount() { 362 return mInts.size() - 1; 363 } 364 365 public int getLineTop(int line) { 366 return mInts.getValue(line, TOP); 367 } 368 369 public int getLineDescent(int line) { 370 return mInts.getValue(line, DESCENT); 371 } 372 373 public int getLineStart(int line) { 374 return mInts.getValue(line, START) & START_MASK; 375 } 376 377 public boolean getLineContainsTab(int line) { 378 return (mInts.getValue(line, TAB) & TAB_MASK) != 0; 379 } 380 381 public int getParagraphDirection(int line) { 382 return mInts.getValue(line, DIR) >> DIR_SHIFT; 383 } 384 385 public final Directions getLineDirections(int line) { 386 return mObjects.getValue(line, 0); 387 } 388 389 public int getTopPadding() { 390 return mTopPadding; 391 } 392 393 public int getBottomPadding() { 394 return mBottomPadding; 395 } 396 397 @Override 398 public int getEllipsizedWidth() { 399 return mEllipsizedWidth; 400 } 401 402 private static class ChangeWatcher 403 implements TextWatcher, SpanWatcher 404 { 405 public ChangeWatcher(DynamicLayout layout) { 406 mLayout = new WeakReference(layout); 407 } 408 409 private void reflow(CharSequence s, int where, int before, int after) { 410 DynamicLayout ml = (DynamicLayout) mLayout.get(); 411 412 if (ml != null) 413 ml.reflow(s, where, before, after); 414 else if (s instanceof Spannable) 415 ((Spannable) s).removeSpan(this); 416 } 417 418 public void beforeTextChanged(CharSequence s, 419 int where, int before, int after) { 420 ; 421 } 422 423 public void onTextChanged(CharSequence s, 424 int where, int before, int after) { 425 reflow(s, where, before, after); 426 } 427 428 public void afterTextChanged(Editable s) { 429 ; 430 } 431 432 public void onSpanAdded(Spannable s, Object o, int start, int end) { 433 if (o instanceof UpdateLayout) 434 reflow(s, start, end - start, end - start); 435 } 436 437 public void onSpanRemoved(Spannable s, Object o, int start, int end) { 438 if (o instanceof UpdateLayout) 439 reflow(s, start, end - start, end - start); 440 } 441 442 public void onSpanChanged(Spannable s, Object o, int start, int end, 443 int nstart, int nend) { 444 if (o instanceof UpdateLayout) { 445 reflow(s, start, end - start, end - start); 446 reflow(s, nstart, nend - nstart, nend - nstart); 447 } 448 } 449 450 private WeakReference mLayout; 451 } 452 453 public int getEllipsisStart(int line) { 454 if (mEllipsizeAt == null) { 455 return 0; 456 } 457 458 return mInts.getValue(line, ELLIPSIS_START); 459 } 460 461 public int getEllipsisCount(int line) { 462 if (mEllipsizeAt == null) { 463 return 0; 464 } 465 466 return mInts.getValue(line, ELLIPSIS_COUNT); 467 } 468 469 private CharSequence mBase; 470 private CharSequence mDisplay; 471 private ChangeWatcher mWatcher; 472 private boolean mIncludePad; 473 private boolean mEllipsize; 474 private int mEllipsizedWidth; 475 private TextUtils.TruncateAt mEllipsizeAt; 476 477 private PackedIntVector mInts; 478 private PackedObjectVector<Directions> mObjects; 479 480 private int mTopPadding, mBottomPadding; 481 482 private static StaticLayout sStaticLayout = new StaticLayout(true); 483 private static Object sLock = new Object(); 484 485 private static final int START = 0; 486 private static final int DIR = START; 487 private static final int TAB = START; 488 private static final int TOP = 1; 489 private static final int DESCENT = 2; 490 private static final int COLUMNS_NORMAL = 3; 491 492 private static final int ELLIPSIS_START = 3; 493 private static final int ELLIPSIS_COUNT = 4; 494 private static final int COLUMNS_ELLIPSIZE = 5; 495 496 private static final int START_MASK = 0x1FFFFFFF; 497 private static final int DIR_MASK = 0xC0000000; 498 private static final int DIR_SHIFT = 30; 499 private static final int TAB_MASK = 0x20000000; 500 501 private static final int ELLIPSIS_UNDEFINED = 0x80000000; 502} 503