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