DynamicLayout.java revision 0a4db3c5270440eeb7e4e44a7029926e239ec3bd
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 = null; 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, mEllipsizedWidth, mEllipsizeAt); 265 int n = reflowed.getLineCount(); 266 267 // If the new layout has a blank line at the end, but it is not 268 // the very end of the buffer, then we already have a line that 269 // starts there, so disregard the blank line. 270 271 if (where + after != len && 272 reflowed.getLineStart(n - 1) == where + after) 273 n--; 274 275 // remove affected lines from old layout 276 277 mInts.deleteAt(startline, endline - startline); 278 mObjects.deleteAt(startline, endline - startline); 279 280 // adjust offsets in layout for new height and offsets 281 282 int ht = reflowed.getLineTop(n); 283 int toppad = 0, botpad = 0; 284 285 if (mIncludePad && startline == 0) { 286 toppad = reflowed.getTopPadding(); 287 mTopPadding = toppad; 288 ht -= toppad; 289 } 290 if (mIncludePad && islast) { 291 botpad = reflowed.getBottomPadding(); 292 mBottomPadding = botpad; 293 ht += botpad; 294 } 295 296 mInts.adjustValuesBelow(startline, START, after - before); 297 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht); 298 299 // insert new layout 300 301 int[] ints; 302 303 if (mEllipsize) { 304 ints = new int[COLUMNS_ELLIPSIZE]; 305 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; 306 } else { 307 ints = new int[COLUMNS_NORMAL]; 308 } 309 310 Directions[] objects = new Directions[1]; 311 312 for (int i = 0; i < n; i++) { 313 ints[START] = reflowed.getLineStart(i) | 314 (reflowed.getParagraphDirection(i) << DIR_SHIFT) | 315 (reflowed.getLineContainsTab(i) ? TAB_MASK : 0); 316 317 int top = reflowed.getLineTop(i) + startv; 318 if (i > 0) 319 top -= toppad; 320 ints[TOP] = top; 321 322 int desc = reflowed.getLineDescent(i); 323 if (i == n - 1) 324 desc += botpad; 325 326 ints[DESCENT] = desc; 327 objects[0] = reflowed.getLineDirections(i); 328 329 if (mEllipsize) { 330 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); 331 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); 332 } 333 334 mInts.insertAt(startline + i, ints); 335 mObjects.insertAt(startline + i, objects); 336 } 337 338 synchronized (sLock) { 339 sStaticLayout = reflowed; 340 } 341 } 342 343 @Override 344 public int getLineCount() { 345 return mInts.size() - 1; 346 } 347 348 @Override 349 public int getLineTop(int line) { 350 return mInts.getValue(line, TOP); 351 } 352 353 @Override 354 public int getLineDescent(int line) { 355 return mInts.getValue(line, DESCENT); 356 } 357 358 @Override 359 public int getLineStart(int line) { 360 return mInts.getValue(line, START) & START_MASK; 361 } 362 363 @Override 364 public boolean getLineContainsTab(int line) { 365 return (mInts.getValue(line, TAB) & TAB_MASK) != 0; 366 } 367 368 @Override 369 public int getParagraphDirection(int line) { 370 return mInts.getValue(line, DIR) >> DIR_SHIFT; 371 } 372 373 @Override 374 public final Directions getLineDirections(int line) { 375 return mObjects.getValue(line, 0); 376 } 377 378 @Override 379 public int getTopPadding() { 380 return mTopPadding; 381 } 382 383 @Override 384 public int getBottomPadding() { 385 return mBottomPadding; 386 } 387 388 @Override 389 public int getEllipsizedWidth() { 390 return mEllipsizedWidth; 391 } 392 393 private static class ChangeWatcher implements TextWatcher, SpanWatcher { 394 public ChangeWatcher(DynamicLayout layout) { 395 mLayout = new WeakReference<DynamicLayout>(layout); 396 } 397 398 private void reflow(CharSequence s, int where, int before, int after) { 399 DynamicLayout ml = mLayout.get(); 400 401 if (ml != null) 402 ml.reflow(s, where, before, after); 403 else if (s instanceof Spannable) 404 ((Spannable) s).removeSpan(this); 405 } 406 407 public void beforeTextChanged(CharSequence s, int where, int before, int after) { 408 } 409 410 public void onTextChanged(CharSequence s, int where, int before, int after) { 411 reflow(s, where, before, after); 412 } 413 414 public void afterTextChanged(Editable s) { 415 } 416 417 public void onSpanAdded(Spannable s, Object o, int start, int end) { 418 if (o instanceof UpdateLayout) 419 reflow(s, start, end - start, end - start); 420 } 421 422 public void onSpanRemoved(Spannable s, Object o, int start, int end) { 423 if (o instanceof UpdateLayout) 424 reflow(s, start, end - start, end - start); 425 } 426 427 public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { 428 if (o instanceof UpdateLayout) { 429 reflow(s, start, end - start, end - start); 430 reflow(s, nstart, nend - nstart, nend - nstart); 431 } 432 } 433 434 private WeakReference<DynamicLayout> mLayout; 435 } 436 437 @Override 438 public int getEllipsisStart(int line) { 439 if (mEllipsizeAt == null) { 440 return 0; 441 } 442 443 return mInts.getValue(line, ELLIPSIS_START); 444 } 445 446 @Override 447 public int getEllipsisCount(int line) { 448 if (mEllipsizeAt == null) { 449 return 0; 450 } 451 452 return mInts.getValue(line, ELLIPSIS_COUNT); 453 } 454 455 private CharSequence mBase; 456 private CharSequence mDisplay; 457 private ChangeWatcher mWatcher; 458 private boolean mIncludePad; 459 private boolean mEllipsize; 460 private int mEllipsizedWidth; 461 private TextUtils.TruncateAt mEllipsizeAt; 462 463 private PackedIntVector mInts; 464 private PackedObjectVector<Directions> mObjects; 465 466 private int mTopPadding, mBottomPadding; 467 468 private static StaticLayout sStaticLayout = new StaticLayout(true); 469 private static Object sLock = new Object(); 470 471 private static final int START = 0; 472 private static final int DIR = START; 473 private static final int TAB = START; 474 private static final int TOP = 1; 475 private static final int DESCENT = 2; 476 private static final int COLUMNS_NORMAL = 3; 477 478 private static final int ELLIPSIS_START = 3; 479 private static final int ELLIPSIS_COUNT = 4; 480 private static final int COLUMNS_ELLIPSIZE = 5; 481 482 private static final int START_MASK = 0x1FFFFFFF; 483 private static final int DIR_SHIFT = 30; 484 private static final int TAB_MASK = 0x20000000; 485 486 private static final int ELLIPSIS_UNDEFINED = 0x80000000; 487} 488