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