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 17 package android.text; 18 19 import android.graphics.Paint; 20 import android.text.style.UpdateLayout; 21 import android.text.style.WrapTogetherSpan; 22 23 import 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 */ 33 public class DynamicLayout 34 extends 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 */ DynamicLayout(CharSequence base, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)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 */ DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)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 */ DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)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 reflow(CharSequence s, int where, int before, int after)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 314 for (int i = 0; i < n; i++) { 315 ints[START] = reflowed.getLineStart(i) | 316 (reflowed.getParagraphDirection(i) << DIR_SHIFT) | 317 (reflowed.getLineContainsTab(i) ? TAB_MASK : 0); 318 319 int top = reflowed.getLineTop(i) + startv; 320 if (i > 0) 321 top -= toppad; 322 ints[TOP] = top; 323 324 int desc = reflowed.getLineDescent(i); 325 if (i == n - 1) 326 desc += botpad; 327 328 ints[DESCENT] = desc; 329 objects[0] = reflowed.getLineDirections(i); 330 331 if (mEllipsize) { 332 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); 333 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); 334 } 335 336 mInts.insertAt(startline + i, ints); 337 mObjects.insertAt(startline + i, objects); 338 } 339 340 synchronized (sLock) { 341 sStaticLayout = reflowed; 342 } 343 } 344 dump(boolean show)345 private void dump(boolean show) { 346 int n = getLineCount(); 347 348 for (int i = 0; i < n; i++) { 349 System.out.print("line " + i + ": " + getLineStart(i) + " to " + getLineEnd(i) + " "); 350 351 if (show) { 352 System.out.print(getText().subSequence(getLineStart(i), 353 getLineEnd(i))); 354 } 355 356 System.out.println(""); 357 } 358 359 System.out.println(""); 360 } 361 getLineCount()362 public int getLineCount() { 363 return mInts.size() - 1; 364 } 365 getLineTop(int line)366 public int getLineTop(int line) { 367 return mInts.getValue(line, TOP); 368 } 369 getLineDescent(int line)370 public int getLineDescent(int line) { 371 return mInts.getValue(line, DESCENT); 372 } 373 getLineStart(int line)374 public int getLineStart(int line) { 375 return mInts.getValue(line, START) & START_MASK; 376 } 377 getLineContainsTab(int line)378 public boolean getLineContainsTab(int line) { 379 return (mInts.getValue(line, TAB) & TAB_MASK) != 0; 380 } 381 getParagraphDirection(int line)382 public int getParagraphDirection(int line) { 383 return mInts.getValue(line, DIR) >> DIR_SHIFT; 384 } 385 getLineDirections(int line)386 public final Directions getLineDirections(int line) { 387 return mObjects.getValue(line, 0); 388 } 389 getTopPadding()390 public int getTopPadding() { 391 return mTopPadding; 392 } 393 getBottomPadding()394 public int getBottomPadding() { 395 return mBottomPadding; 396 } 397 398 @Override getEllipsizedWidth()399 public int getEllipsizedWidth() { 400 return mEllipsizedWidth; 401 } 402 403 private static class ChangeWatcher 404 implements TextWatcher, SpanWatcher 405 { ChangeWatcher(DynamicLayout layout)406 public ChangeWatcher(DynamicLayout layout) { 407 mLayout = new WeakReference(layout); 408 } 409 reflow(CharSequence s, int where, int before, int after)410 private void reflow(CharSequence s, int where, int before, int after) { 411 DynamicLayout ml = (DynamicLayout) mLayout.get(); 412 413 if (ml != null) 414 ml.reflow(s, where, before, after); 415 else if (s instanceof Spannable) 416 ((Spannable) s).removeSpan(this); 417 } 418 beforeTextChanged(CharSequence s, int where, int before, int after)419 public void beforeTextChanged(CharSequence s, 420 int where, int before, int after) { 421 ; 422 } 423 onTextChanged(CharSequence s, int where, int before, int after)424 public void onTextChanged(CharSequence s, 425 int where, int before, int after) { 426 reflow(s, where, before, after); 427 } 428 afterTextChanged(Editable s)429 public void afterTextChanged(Editable s) { 430 ; 431 } 432 onSpanAdded(Spannable s, Object o, int start, int end)433 public void onSpanAdded(Spannable s, Object o, int start, int end) { 434 if (o instanceof UpdateLayout) 435 reflow(s, start, end - start, end - start); 436 } 437 onSpanRemoved(Spannable s, Object o, int start, int end)438 public void onSpanRemoved(Spannable s, Object o, int start, int end) { 439 if (o instanceof UpdateLayout) 440 reflow(s, start, end - start, end - start); 441 } 442 onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend)443 public void onSpanChanged(Spannable s, Object o, int start, int end, 444 int nstart, int nend) { 445 if (o instanceof UpdateLayout) { 446 reflow(s, start, end - start, end - start); 447 reflow(s, nstart, nend - nstart, nend - nstart); 448 } 449 } 450 451 private WeakReference mLayout; 452 } 453 getEllipsisStart(int line)454 public int getEllipsisStart(int line) { 455 if (mEllipsizeAt == null) { 456 return 0; 457 } 458 459 return mInts.getValue(line, ELLIPSIS_START); 460 } 461 getEllipsisCount(int line)462 public int getEllipsisCount(int line) { 463 if (mEllipsizeAt == null) { 464 return 0; 465 } 466 467 return mInts.getValue(line, ELLIPSIS_COUNT); 468 } 469 470 private CharSequence mBase; 471 private CharSequence mDisplay; 472 private ChangeWatcher mWatcher; 473 private boolean mIncludePad; 474 private boolean mEllipsize; 475 private int mEllipsizedWidth; 476 private TextUtils.TruncateAt mEllipsizeAt; 477 478 private PackedIntVector mInts; 479 private PackedObjectVector<Directions> mObjects; 480 481 private int mTopPadding, mBottomPadding; 482 483 private static StaticLayout sStaticLayout = new StaticLayout(true); 484 private static Object sLock = new Object(); 485 486 private static final int START = 0; 487 private static final int DIR = START; 488 private static final int TAB = START; 489 private static final int TOP = 1; 490 private static final int DESCENT = 2; 491 private static final int COLUMNS_NORMAL = 3; 492 493 private static final int ELLIPSIS_START = 3; 494 private static final int ELLIPSIS_COUNT = 4; 495 private static final int COLUMNS_ELLIPSIZE = 5; 496 497 private static final int START_MASK = 0x1FFFFFFF; 498 private static final int DIR_MASK = 0xC0000000; 499 private static final int DIR_SHIFT = 30; 500 private static final int TAB_MASK = 0x20000000; 501 502 private static final int ELLIPSIS_UNDEFINED = 0x80000000; 503 } 504