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 */ 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 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 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 362 public int getLineCount() { 363 return mInts.size() - 1; 364 } 365 366 public int getLineTop(int line) { 367 return mInts.getValue(line, TOP); 368 } 369 370 public int getLineDescent(int line) { 371 return mInts.getValue(line, DESCENT); 372 } 373 374 public int getLineStart(int line) { 375 return mInts.getValue(line, START) & START_MASK; 376 } 377 378 public boolean getLineContainsTab(int line) { 379 return (mInts.getValue(line, TAB) & TAB_MASK) != 0; 380 } 381 382 public int getParagraphDirection(int line) { 383 return mInts.getValue(line, DIR) >> DIR_SHIFT; 384 } 385 386 public final Directions getLineDirections(int line) { 387 return mObjects.getValue(line, 0); 388 } 389 390 public int getTopPadding() { 391 return mTopPadding; 392 } 393 394 public int getBottomPadding() { 395 return mBottomPadding; 396 } 397 398 @Override 399 public int getEllipsizedWidth() { 400 return mEllipsizedWidth; 401 } 402 403 private static class ChangeWatcher 404 implements TextWatcher, SpanWatcher 405 { 406 public ChangeWatcher(DynamicLayout layout) { 407 mLayout = new WeakReference(layout); 408 } 409 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 419 public void beforeTextChanged(CharSequence s, 420 int where, int before, int after) { 421 ; 422 } 423 424 public void onTextChanged(CharSequence s, 425 int where, int before, int after) { 426 reflow(s, where, before, after); 427 } 428 429 public void afterTextChanged(Editable s) { 430 ; 431 } 432 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 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 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 454 public int getEllipsisStart(int line) { 455 if (mEllipsizeAt == null) { 456 return 0; 457 } 458 459 return mInts.getValue(line, ELLIPSIS_START); 460 } 461 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