blob: 18431cacbfaf3df9e5cf2b49c8d7d5e06276c891 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
Roozbeh Pournader22a167c2017-08-21 12:53:44 -070019import android.annotation.FloatRange;
20import android.annotation.IntRange;
21import android.annotation.NonNull;
22import android.annotation.Nullable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import android.graphics.Paint;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090024import android.graphics.Rect;
25import android.text.style.ReplacementSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026import android.text.style.UpdateLayout;
27import android.text.style.WrapTogetherSpan;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090028import android.util.ArraySet;
Roozbeh Pournader22a167c2017-08-21 12:53:44 -070029import android.util.Pools.SynchronizedPool;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030
Siyamed Sinired09ae12016-02-16 14:36:26 -080031import com.android.internal.annotations.VisibleForTesting;
Gilles Debunne33b7de852012-03-12 11:57:48 -070032import com.android.internal.util.ArrayUtils;
Adam Lesinski776abc22014-03-07 11:30:59 -050033import com.android.internal.util.GrowingArrayUtils;
Gilles Debunne33b7de852012-03-12 11:57:48 -070034
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080035import java.lang.ref.WeakReference;
36
37/**
38 * DynamicLayout is a text layout that updates itself as the text is edited.
39 * <p>This is used by widgets to control text layout. You should not need
40 * to use this class directly unless you are implementing your own widget
41 * or custom display object, or need to call
42 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
43 * Canvas.drawText()} directly.</p>
44 */
Clara Bayarri81a4f2e2017-12-19 16:46:56 +000045public class DynamicLayout extends Layout {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046 private static final int PRIORITY = 128;
Gilles Debunne71afc392012-05-10 10:24:20 -070047 private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080048
49 /**
Roozbeh Pournader22a167c2017-08-21 12:53:44 -070050 * Builder for dynamic layouts. The builder is the preferred pattern for constructing
51 * DynamicLayout objects and should be preferred over the constructors, particularly to access
52 * newer features. To build a dynamic layout, first call {@link #obtain} with the required
53 * arguments (base, paint, and width), then call setters for optional parameters, and finally
54 * {@link #build} to build the DynamicLayout object. Parameters not explicitly set will get
55 * default values.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080056 */
Roozbeh Pournader22a167c2017-08-21 12:53:44 -070057 public static final class Builder {
58 private Builder() {
59 }
60
61 /**
62 * Obtain a builder for constructing DynamicLayout objects.
63 */
64 @NonNull
65 public static Builder obtain(@NonNull CharSequence base, @NonNull TextPaint paint,
66 @IntRange(from = 0) int width) {
67 Builder b = sPool.acquire();
68 if (b == null) {
69 b = new Builder();
70 }
71
72 // set default initial values
73 b.mBase = base;
74 b.mDisplay = base;
75 b.mPaint = paint;
76 b.mWidth = width;
77 b.mAlignment = Alignment.ALIGN_NORMAL;
78 b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
79 b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
80 b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
81 b.mIncludePad = true;
Roozbeh Pournader15b213d2017-08-21 17:43:17 -070082 b.mFallbackLineSpacing = false;
Roozbeh Pournader22a167c2017-08-21 12:53:44 -070083 b.mEllipsizedWidth = width;
84 b.mEllipsize = null;
85 b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
86 b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
87 b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
88 return b;
89 }
90
91 /**
92 * This method should be called after the layout is finished getting constructed and the
93 * builder needs to be cleaned up and returned to the pool.
94 */
95 private static void recycle(@NonNull Builder b) {
96 b.mBase = null;
97 b.mDisplay = null;
98 b.mPaint = null;
99 sPool.release(b);
100 }
101
102 /**
103 * Set the transformed text (password transformation being the primary example of a
104 * transformation) that will be updated as the base text is changed. The default is the
105 * 'base' text passed to the builder's constructor.
106 *
107 * @param display the transformed text
108 * @return this builder, useful for chaining
109 */
110 @NonNull
111 public Builder setDisplayText(@NonNull CharSequence display) {
112 mDisplay = display;
113 return this;
114 }
115
116 /**
117 * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}.
118 *
119 * @param alignment Alignment for the resulting {@link DynamicLayout}
120 * @return this builder, useful for chaining
121 */
122 @NonNull
123 public Builder setAlignment(@NonNull Alignment alignment) {
124 mAlignment = alignment;
125 return this;
126 }
127
128 /**
129 * Set the text direction heuristic. The text direction heuristic is used to resolve text
130 * direction per-paragraph based on the input text. The default is
131 * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
132 *
133 * @param textDir text direction heuristic for resolving bidi behavior.
134 * @return this builder, useful for chaining
135 */
136 @NonNull
137 public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
138 mTextDir = textDir;
139 return this;
140 }
141
142 /**
143 * Set line spacing parameters. Each line will have its line spacing multiplied by
144 * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for
145 * {@code spacingAdd} and 1.0 for {@code spacingMult}.
146 *
147 * @param spacingAdd the amount of line spacing addition
148 * @param spacingMult the line spacing multiplier
149 * @return this builder, useful for chaining
150 * @see android.widget.TextView#setLineSpacing
151 */
152 @NonNull
153 public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) {
154 mSpacingAdd = spacingAdd;
155 mSpacingMult = spacingMult;
156 return this;
157 }
158
159 /**
160 * Set whether to include extra space beyond font ascent and descent (which is needed to
161 * avoid clipping in some languages, such as Arabic and Kannada). The default is
162 * {@code true}.
163 *
164 * @param includePad whether to include padding
165 * @return this builder, useful for chaining
166 * @see android.widget.TextView#setIncludeFontPadding
167 */
168 @NonNull
169 public Builder setIncludePad(boolean includePad) {
170 mIncludePad = includePad;
171 return this;
172 }
173
174 /**
Roozbeh Pournader15b213d2017-08-21 17:43:17 -0700175 * Set whether to respect the ascent and descent of the fallback fonts that are used in
176 * displaying the text (which is needed to avoid text from consecutive lines running into
177 * each other). If set, fallback fonts that end up getting used can increase the ascent
178 * and descent of the lines that they are used on.
179 *
180 * <p>For backward compatibility reasons, the default is {@code false}, but setting this to
181 * true is strongly recommended. It is required to be true if text could be in languages
182 * like Burmese or Tibetan where text is typically much taller or deeper than Latin text.
183 *
184 * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts
185 * @return this builder, useful for chaining
186 */
187 @NonNull
188 public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) {
189 mFallbackLineSpacing = useLineSpacingFromFallbacks;
190 return this;
191 }
192
193 /**
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700194 * Set the width as used for ellipsizing purposes, if it differs from the normal layout
195 * width. The default is the {@code width} passed to {@link #obtain}.
196 *
197 * @param ellipsizedWidth width used for ellipsizing, in pixels
198 * @return this builder, useful for chaining
199 * @see android.widget.TextView#setEllipsize
200 */
201 @NonNull
202 public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) {
203 mEllipsizedWidth = ellipsizedWidth;
204 return this;
205 }
206
207 /**
208 * Set ellipsizing on the layout. Causes words that are longer than the view is wide, or
209 * exceeding the number of lines (see #setMaxLines) in the case of
210 * {@link android.text.TextUtils.TruncateAt#END} or
211 * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead of broken.
212 * The default is {@code null}, indicating no ellipsis is to be applied.
213 *
214 * @param ellipsize type of ellipsis behavior
215 * @return this builder, useful for chaining
216 * @see android.widget.TextView#setEllipsize
217 */
218 public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
219 mEllipsize = ellipsize;
220 return this;
221 }
222
223 /**
224 * Set break strategy, useful for selecting high quality or balanced paragraph layout
225 * options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}.
226 *
227 * @param breakStrategy break strategy for paragraph layout
228 * @return this builder, useful for chaining
229 * @see android.widget.TextView#setBreakStrategy
230 */
231 @NonNull
232 public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
233 mBreakStrategy = breakStrategy;
234 return this;
235 }
236
237 /**
238 * Set hyphenation frequency, to control the amount of automatic hyphenation used. The
239 * possible values are defined in {@link Layout}, by constants named with the pattern
240 * {@code HYPHENATION_FREQUENCY_*}. The default is
241 * {@link Layout#HYPHENATION_FREQUENCY_NONE}.
242 *
243 * @param hyphenationFrequency hyphenation frequency for the paragraph
244 * @return this builder, useful for chaining
245 * @see android.widget.TextView#setHyphenationFrequency
246 */
247 @NonNull
248 public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
249 mHyphenationFrequency = hyphenationFrequency;
250 return this;
251 }
252
253 /**
254 * Set paragraph justification mode. The default value is
255 * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification,
256 * the last line will be displayed with the alignment set by {@link #setAlignment}.
257 *
258 * @param justificationMode justification mode for the paragraph.
259 * @return this builder, useful for chaining.
260 */
261 @NonNull
262 public Builder setJustificationMode(@JustificationMode int justificationMode) {
263 mJustificationMode = justificationMode;
264 return this;
265 }
266
267 /**
268 * Build the {@link DynamicLayout} after options have been set.
269 *
270 * <p>Note: the builder object must not be reused in any way after calling this method.
271 * Setting parameters after calling this method, or calling it a second time on the same
272 * builder object, will likely lead to unexpected results.
273 *
274 * @return the newly constructed {@link DynamicLayout} object
275 */
276 @NonNull
277 public DynamicLayout build() {
278 final DynamicLayout result = new DynamicLayout(this);
279 Builder.recycle(this);
280 return result;
281 }
282
283 private CharSequence mBase;
284 private CharSequence mDisplay;
285 private TextPaint mPaint;
286 private int mWidth;
287 private Alignment mAlignment;
288 private TextDirectionHeuristic mTextDir;
289 private float mSpacingMult;
290 private float mSpacingAdd;
291 private boolean mIncludePad;
Roozbeh Pournader15b213d2017-08-21 17:43:17 -0700292 private boolean mFallbackLineSpacing;
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700293 private int mBreakStrategy;
294 private int mHyphenationFrequency;
295 private int mJustificationMode;
296 private TextUtils.TruncateAt mEllipsize;
297 private int mEllipsizedWidth;
298
299 private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
300
Siyamed Sinira273a702017-10-05 11:22:12 -0700301 private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700302 }
303
304 /**
Clara Bayarri81a4f2e2017-12-19 16:46:56 +0000305 * @deprecated Use {@link Builder} instead.
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700306 */
Clara Bayarri81a4f2e2017-12-19 16:46:56 +0000307 @Deprecated
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700308 public DynamicLayout(@NonNull CharSequence base,
309 @NonNull TextPaint paint,
310 @IntRange(from = 0) int width, @NonNull Alignment align,
311 @FloatRange(from = 0.0) float spacingmult, float spacingadd,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800312 boolean includepad) {
313 this(base, base, paint, width, align, spacingmult, spacingadd,
314 includepad);
315 }
316
317 /**
Clara Bayarri81a4f2e2017-12-19 16:46:56 +0000318 * @deprecated Use {@link Builder} instead.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800319 */
Clara Bayarri81a4f2e2017-12-19 16:46:56 +0000320 @Deprecated
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700321 public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
322 @NonNull TextPaint paint,
323 @IntRange(from = 0) int width, @NonNull Alignment align,
324 @FloatRange(from = 0.0) float spacingmult, float spacingadd,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800325 boolean includepad) {
326 this(base, display, paint, width, align, spacingmult, spacingadd,
327 includepad, null, 0);
328 }
329
330 /**
Clara Bayarri81a4f2e2017-12-19 16:46:56 +0000331 * @deprecated Use {@link Builder} instead.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800332 */
Clara Bayarri81a4f2e2017-12-19 16:46:56 +0000333 @Deprecated
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700334 public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
335 @NonNull TextPaint paint,
336 @IntRange(from = 0) int width, @NonNull Alignment align,
337 @FloatRange(from = 0.0) float spacingmult, float spacingadd,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800338 boolean includepad,
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700339 @Nullable TextUtils.TruncateAt ellipsize,
340 @IntRange(from = 0) int ellipsizedWidth) {
Doug Feltcb3791202011-07-07 11:57:48 -0700341 this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700342 spacingmult, spacingadd, includepad,
Roozbeh Pournader15b213d2017-08-21 17:43:17 -0700343 Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE,
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700344 Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth);
Doug Feltcb3791202011-07-07 11:57:48 -0700345 }
346
347 /**
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700348 * Make a layout for the transformed text (password transformation being the primary example of
349 * a transformation) that will be updated as the base text is changed. If ellipsize is non-null,
350 * the Layout will ellipsize the text down to ellipsizedWidth.
351 *
352 * @hide
Clara Bayarri81a4f2e2017-12-19 16:46:56 +0000353 * @deprecated Use {@link Builder} instead.
Doug Feltcb3791202011-07-07 11:57:48 -0700354 */
Clara Bayarri81a4f2e2017-12-19 16:46:56 +0000355 @Deprecated
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700356 public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
357 @NonNull TextPaint paint,
358 @IntRange(from = 0) int width,
359 @NonNull Alignment align, @NonNull TextDirectionHeuristic textDir,
360 @FloatRange(from = 0.0) float spacingmult, float spacingadd,
361 boolean includepad, @BreakStrategy int breakStrategy,
362 @HyphenationFrequency int hyphenationFrequency,
363 @JustificationMode int justificationMode,
364 @Nullable TextUtils.TruncateAt ellipsize,
365 @IntRange(from = 0) int ellipsizedWidth) {
366 super(createEllipsizer(ellipsize, display),
Doug Feltcb3791202011-07-07 11:57:48 -0700367 paint, width, align, textDir, spacingmult, spacingadd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800368
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700369 final Builder b = Builder.obtain(base, paint, width)
370 .setAlignment(align)
371 .setTextDirection(textDir)
372 .setLineSpacing(spacingadd, spacingmult)
373 .setEllipsizedWidth(ellipsizedWidth)
374 .setEllipsize(ellipsize);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800375 mDisplay = display;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800376 mIncludePad = includepad;
Raph Levien39b4db72015-03-25 13:18:20 -0700377 mBreakStrategy = breakStrategy;
Seigo Nonaka4b4730d2017-03-31 09:42:16 -0700378 mJustificationMode = justificationMode;
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700379 mHyphenationFrequency = hyphenationFrequency;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800380
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700381 generate(b);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800382
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700383 Builder.recycle(b);
384 }
385
386 private DynamicLayout(@NonNull Builder b) {
387 super(createEllipsizer(b.mEllipsize, b.mDisplay),
Roozbeh Pournader9fc67252017-09-20 16:07:13 -0700388 b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd);
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700389
390 mDisplay = b.mDisplay;
391 mIncludePad = b.mIncludePad;
392 mBreakStrategy = b.mBreakStrategy;
393 mJustificationMode = b.mJustificationMode;
394 mHyphenationFrequency = b.mHyphenationFrequency;
395
396 generate(b);
397 }
398
399 @NonNull
400 private static CharSequence createEllipsizer(@Nullable TextUtils.TruncateAt ellipsize,
401 @NonNull CharSequence display) {
402 if (ellipsize == null) {
403 return display;
404 } else if (display instanceof Spanned) {
405 return new SpannedEllipsizer(display);
406 } else {
407 return new Ellipsizer(display);
408 }
409 }
410
411 private void generate(@NonNull Builder b) {
412 mBase = b.mBase;
Roozbeh Pournader15b213d2017-08-21 17:43:17 -0700413 mFallbackLineSpacing = b.mFallbackLineSpacing;
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700414 if (b.mEllipsize != null) {
415 mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
416 mEllipsizedWidth = b.mEllipsizedWidth;
417 mEllipsizeAt = b.mEllipsize;
418
419 /*
420 * This is annoying, but we can't refer to the layout until superclass construction is
421 * finished, and the superclass constructor wants the reference to the display text.
422 *
423 * In other words, the two Ellipsizer classes in Layout.java need a
424 * (Dynamic|Static)Layout as a parameter to do their calculations, but the Ellipsizers
425 * also need to be the input to the superclass's constructor (Layout). In order to go
426 * around the circular dependency, we construct the Ellipsizer with only one of the
427 * parameters, the text (in createEllipsizer). And we fill in the rest of the needed
428 * information (layout, width, and method) later, here.
429 *
430 * This will break if the superclass constructor ever actually cares about the content
431 * instead of just holding the reference.
432 */
433 final Ellipsizer e = (Ellipsizer) getText();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800434 e.mLayout = this;
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700435 e.mWidth = b.mEllipsizedWidth;
436 e.mMethod = b.mEllipsize;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800437 mEllipsize = true;
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700438 } else {
439 mInts = new PackedIntVector(COLUMNS_NORMAL);
440 mEllipsizedWidth = b.mWidth;
441 mEllipsizeAt = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800442 }
443
Siyamed Sinira273a702017-10-05 11:22:12 -0700444 mObjects = new PackedObjectVector<>(1);
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700445
446 // Initial state is a single line with 0 characters (0 to 0), with top at 0 and bottom at
447 // whatever is natural, and undefined ellipsis.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800448
449 int[] start;
450
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700451 if (b.mEllipsize != null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800452 start = new int[COLUMNS_ELLIPSIZE];
453 start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
454 } else {
455 start = new int[COLUMNS_NORMAL];
456 }
457
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700458 final Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800459
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700460 final Paint.FontMetricsInt fm = b.mFontMetricsInt;
461 b.mPaint.getFontMetricsInt(fm);
462 final int asc = fm.ascent;
463 final int desc = fm.descent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800464
465 start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
466 start[TOP] = 0;
467 start[DESCENT] = desc;
468 mInts.insertAt(0, start);
469
470 start[TOP] = desc - asc;
471 mInts.insertAt(1, start);
472
473 mObjects.insertAt(0, dirs);
474
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700475 final int baseLength = mBase.length();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800476 // Update from 0 characters to whatever the real text is
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700477 reflow(mBase, 0, 0, baseLength);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800478
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700479 if (mBase instanceof Spannable) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800480 if (mWatcher == null)
481 mWatcher = new ChangeWatcher(this);
482
483 // Strip out any watchers for other DynamicLayouts.
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700484 final Spannable sp = (Spannable) mBase;
485 final ChangeWatcher[] spans = sp.getSpans(0, baseLength, ChangeWatcher.class);
486 for (int i = 0; i < spans.length; i++) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800487 sp.removeSpan(spans[i]);
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700488 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800489
Roozbeh Pournader22a167c2017-08-21 12:53:44 -0700490 sp.setSpan(mWatcher, 0, baseLength,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800491 Spannable.SPAN_INCLUSIVE_INCLUSIVE |
492 (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
493 }
494 }
495
Siyamed Sinir50b585e2017-12-18 18:47:37 -0800496 /** @hide */
497 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
498 public void reflow(CharSequence s, int where, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800499 if (s != mBase)
500 return;
501
502 CharSequence text = mDisplay;
503 int len = text.length();
504
505 // seek back to the start of the paragraph
506
507 int find = TextUtils.lastIndexOf(text, '\n', where - 1);
508 if (find < 0)
509 find = 0;
510 else
511 find = find + 1;
512
513 {
514 int diff = where - find;
515 before += diff;
516 after += diff;
517 where -= diff;
518 }
519
520 // seek forward to the end of the paragraph
521
522 int look = TextUtils.indexOf(text, '\n', where + after);
523 if (look < 0)
524 look = len;
525 else
526 look++; // we want the index after the \n
527
528 int change = look - (where + after);
529 before += change;
530 after += change;
531
532 // seek further out to cover anything that is forced to wrap together
533
534 if (text instanceof Spanned) {
535 Spanned sp = (Spanned) text;
536 boolean again;
537
538 do {
539 again = false;
540
541 Object[] force = sp.getSpans(where, where + after,
542 WrapTogetherSpan.class);
543
544 for (int i = 0; i < force.length; i++) {
545 int st = sp.getSpanStart(force[i]);
546 int en = sp.getSpanEnd(force[i]);
547
548 if (st < where) {
549 again = true;
550
551 int diff = where - st;
552 before += diff;
553 after += diff;
554 where -= diff;
555 }
556
557 if (en > where + after) {
558 again = true;
559
560 int diff = en - (where + after);
561 before += diff;
562 after += diff;
563 }
564 }
565 } while (again);
566 }
567
568 // find affected region of old layout
569
570 int startline = getLineForOffset(where);
571 int startv = getLineTop(startline);
572
573 int endline = getLineForOffset(where + before);
574 if (where + after == len)
575 endline = getLineCount();
576 int endv = getLineTop(endline);
577 boolean islast = (endline == getLineCount());
578
579 // generate new layout for affected text
580
581 StaticLayout reflowed;
Raph Leviend3ab6922015-03-02 14:30:53 -0800582 StaticLayout.Builder b;
Amith Yamasanib724c342011-08-13 07:43:07 -0700583
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800584 synchronized (sLock) {
Amith Yamasanib724c342011-08-13 07:43:07 -0700585 reflowed = sStaticLayout;
Raph Leviend3ab6922015-03-02 14:30:53 -0800586 b = sBuilder;
Amith Yamasanib724c342011-08-13 07:43:07 -0700587 sStaticLayout = null;
Raph Leviend3ab6922015-03-02 14:30:53 -0800588 sBuilder = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800589 }
590
Romain Guye5ea4402011-08-01 14:01:37 -0700591 if (reflowed == null) {
Fabrice Di Meglio09175732011-09-25 16:48:04 -0700592 reflowed = new StaticLayout(null);
Raph Levienebd66ca2015-04-30 15:27:57 -0700593 b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth());
Romain Guye5ea4402011-08-01 14:01:37 -0700594 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800595
Raph Leviend3ab6922015-03-02 14:30:53 -0800596 b.setText(text, where, where + after)
597 .setPaint(getPaint())
598 .setWidth(getWidth())
Raph Leviena6a08282015-06-03 13:20:45 -0700599 .setTextDirection(getTextDirectionHeuristic())
Raph Levien531c30c2015-04-30 16:29:59 -0700600 .setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
Roozbeh Pournader15b213d2017-08-21 17:43:17 -0700601 .setUseLineSpacingFromFallbacks(mFallbackLineSpacing)
Raph Leviend3ab6922015-03-02 14:30:53 -0800602 .setEllipsizedWidth(mEllipsizedWidth)
Raph Levien39b4db72015-03-25 13:18:20 -0700603 .setEllipsize(mEllipsizeAt)
Roozbeh Pournader95c7a132015-05-12 12:01:06 -0700604 .setBreakStrategy(mBreakStrategy)
Seigo Nonaka09da71a2016-11-28 16:24:14 +0900605 .setHyphenationFrequency(mHyphenationFrequency)
Siyamed Sinir442c1512017-07-24 12:18:27 -0700606 .setJustificationMode(mJustificationMode)
Siyamed Sinirfb0b2dc2017-07-26 22:08:24 -0700607 .setAddLastLineLineSpacing(!islast);
608
Siyamed Sinir442c1512017-07-24 12:18:27 -0700609 reflowed.generate(b, false /*includepad*/, true /*trackpad*/);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800610 int n = reflowed.getLineCount();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800611 // If the new layout has a blank line at the end, but it is not
612 // the very end of the buffer, then we already have a line that
613 // starts there, so disregard the blank line.
614
Gilles Debunne71afc392012-05-10 10:24:20 -0700615 if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800616 n--;
617
618 // remove affected lines from old layout
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800619 mInts.deleteAt(startline, endline - startline);
620 mObjects.deleteAt(startline, endline - startline);
621
622 // adjust offsets in layout for new height and offsets
623
624 int ht = reflowed.getLineTop(n);
625 int toppad = 0, botpad = 0;
626
627 if (mIncludePad && startline == 0) {
628 toppad = reflowed.getTopPadding();
629 mTopPadding = toppad;
630 ht -= toppad;
631 }
632 if (mIncludePad && islast) {
633 botpad = reflowed.getBottomPadding();
634 mBottomPadding = botpad;
635 ht += botpad;
636 }
637
638 mInts.adjustValuesBelow(startline, START, after - before);
639 mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
640
641 // insert new layout
642
643 int[] ints;
644
645 if (mEllipsize) {
646 ints = new int[COLUMNS_ELLIPSIZE];
647 ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
648 } else {
649 ints = new int[COLUMNS_NORMAL];
650 }
651
652 Directions[] objects = new Directions[1];
653
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800654 for (int i = 0; i < n; i++) {
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900655 final int start = reflowed.getLineStart(i);
656 ints[START] = start;
657 ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT;
658 ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800659
660 int top = reflowed.getLineTop(i) + startv;
661 if (i > 0)
662 top -= toppad;
663 ints[TOP] = top;
664
665 int desc = reflowed.getLineDescent(i);
666 if (i == n - 1)
667 desc += botpad;
668
669 ints[DESCENT] = desc;
Siyamed Sinir0fa89d62017-07-24 20:46:41 -0700670 ints[EXTRA] = reflowed.getLineExtra(i);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800671 objects[0] = reflowed.getLineDirections(i);
672
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900673 final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1);
674 ints[HYPHEN] = reflowed.getHyphen(i) & HYPHEN_MASK;
675 ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |=
676 contentMayProtrudeFromLineTopOrBottom(text, start, end) ?
677 MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0;
Raph Levien26d443a2015-03-30 14:18:32 -0700678
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800679 if (mEllipsize) {
680 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
681 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
682 }
683
684 mInts.insertAt(startline + i, ints);
685 mObjects.insertAt(startline + i, objects);
686 }
687
Gilles Debunne71afc392012-05-10 10:24:20 -0700688 updateBlocks(startline, endline - 1, n);
689
Raph Leviend3ab6922015-03-02 14:30:53 -0800690 b.finish();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800691 synchronized (sLock) {
Amith Yamasanib724c342011-08-13 07:43:07 -0700692 sStaticLayout = reflowed;
Raph Leviend3ab6922015-03-02 14:30:53 -0800693 sBuilder = b;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800694 }
695 }
696
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900697 private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) {
698 if (text instanceof Spanned) {
699 final Spanned spanned = (Spanned) text;
700 if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) {
701 return true;
702 }
703 }
704 // Spans other than ReplacementSpan can be ignored because line top and bottom are
705 // disjunction of all tops and bottoms, although it's not optimal.
706 final Paint paint = getPaint();
707 paint.getTextBounds(text, start, end, mTempRect);
708 final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
709 return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom;
710 }
711
Gilles Debunne33b7de852012-03-12 11:57:48 -0700712 /**
Gilles Debunne71afc392012-05-10 10:24:20 -0700713 * Create the initial block structure, cutting the text into blocks of at least
714 * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
715 */
716 private void createBlocks() {
717 int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
718 mNumberOfBlocks = 0;
719 final CharSequence text = mDisplay;
720
721 while (true) {
722 offset = TextUtils.indexOf(text, '\n', offset);
723 if (offset < 0) {
724 addBlockAtOffset(text.length());
725 break;
726 } else {
727 addBlockAtOffset(offset);
728 offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
729 }
730 }
731
732 // mBlockIndices and mBlockEndLines should have the same length
733 mBlockIndices = new int[mBlockEndLines.length];
734 for (int i = 0; i < mBlockEndLines.length; i++) {
735 mBlockIndices[i] = INVALID_BLOCK_INDEX;
736 }
737 }
738
739 /**
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900740 * @hide
741 */
742 public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
743 return mBlocksAlwaysNeedToBeRedrawn;
744 }
745
746 private void updateAlwaysNeedsToBeRedrawn(int blockIndex) {
747 int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1);
748 int endLine = mBlockEndLines[blockIndex];
749 for (int i = startLine; i <= endLine; i++) {
750 if (getContentMayProtrudeFromTopOrBottom(i)) {
751 if (mBlocksAlwaysNeedToBeRedrawn == null) {
752 mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>();
753 }
754 mBlocksAlwaysNeedToBeRedrawn.add(blockIndex);
755 return;
756 }
757 }
758 if (mBlocksAlwaysNeedToBeRedrawn != null) {
759 mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex);
760 }
761 }
762
763 /**
Gilles Debunne71afc392012-05-10 10:24:20 -0700764 * Create a new block, ending at the specified character offset.
765 * A block will actually be created only if has at least one line, i.e. this offset is
766 * not on the end line of the previous block.
767 */
768 private void addBlockAtOffset(int offset) {
769 final int line = getLineForOffset(offset);
Gilles Debunne71afc392012-05-10 10:24:20 -0700770 if (mBlockEndLines == null) {
771 // Initial creation of the array, no test on previous block ending line
Adam Lesinski776abc22014-03-07 11:30:59 -0500772 mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
Gilles Debunne71afc392012-05-10 10:24:20 -0700773 mBlockEndLines[mNumberOfBlocks] = line;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900774 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
Gilles Debunne71afc392012-05-10 10:24:20 -0700775 mNumberOfBlocks++;
776 return;
777 }
778
779 final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
780 if (line > previousBlockEndLine) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500781 mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900782 updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
Gilles Debunne71afc392012-05-10 10:24:20 -0700783 mNumberOfBlocks++;
784 }
785 }
786
787 /**
Gilles Debunne33b7de852012-03-12 11:57:48 -0700788 * This method is called every time the layout is reflowed after an edition.
789 * It updates the internal block data structure. The text is split in blocks
790 * of contiguous lines, with at least one block for the entire text.
791 * When a range of lines is edited, new blocks (from 0 to 3 depending on the
792 * overlap structure) will replace the set of overlapping blocks.
793 * Blocks are listed in order and are represented by their ending line number.
794 * An index is associated to each block (which will be used by display lists),
795 * this class simply invalidates the index of blocks overlapping a modification.
796 *
797 * @param startLine the first line of the range of modified lines
798 * @param endLine the last line of the range, possibly equal to startLine, lower
799 * than getLineCount()
800 * @param newLineCount the number of lines that will replace the range, possibly 0
Gilles Debunne1e130b22012-03-14 17:40:42 -0700801 *
802 * @hide
Gilles Debunne33b7de852012-03-12 11:57:48 -0700803 */
Siyamed Sinired09ae12016-02-16 14:36:26 -0800804 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
805 public void updateBlocks(int startLine, int endLine, int newLineCount) {
Gilles Debunne71afc392012-05-10 10:24:20 -0700806 if (mBlockEndLines == null) {
807 createBlocks();
808 return;
809 }
810
Siyamed Sinir50b585e2017-12-18 18:47:37 -0800811 /*final*/ int firstBlock = -1;
812 /*final*/ int lastBlock = -1;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700813 for (int i = 0; i < mNumberOfBlocks; i++) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700814 if (mBlockEndLines[i] >= startLine) {
Gilles Debunne33b7de852012-03-12 11:57:48 -0700815 firstBlock = i;
816 break;
817 }
818 }
819 for (int i = firstBlock; i < mNumberOfBlocks; i++) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700820 if (mBlockEndLines[i] >= endLine) {
Gilles Debunne33b7de852012-03-12 11:57:48 -0700821 lastBlock = i;
822 break;
823 }
824 }
Gilles Debunne157aafc2012-04-19 17:21:57 -0700825 final int lastBlockEndLine = mBlockEndLines[lastBlock];
Gilles Debunne33b7de852012-03-12 11:57:48 -0700826
Siyamed Sinir50b585e2017-12-18 18:47:37 -0800827 final boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
Gilles Debunne157aafc2012-04-19 17:21:57 -0700828 mBlockEndLines[firstBlock - 1] + 1);
Siyamed Sinir50b585e2017-12-18 18:47:37 -0800829 final boolean createBlock = newLineCount > 0;
830 final boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
Gilles Debunne33b7de852012-03-12 11:57:48 -0700831
832 int numAddedBlocks = 0;
833 if (createBlockBefore) numAddedBlocks++;
834 if (createBlock) numAddedBlocks++;
835 if (createBlockAfter) numAddedBlocks++;
836
837 final int numRemovedBlocks = lastBlock - firstBlock + 1;
838 final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
839
840 if (newNumberOfBlocks == 0) {
841 // Even when text is empty, there is actually one line and hence one block
Gilles Debunne157aafc2012-04-19 17:21:57 -0700842 mBlockEndLines[0] = 0;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700843 mBlockIndices[0] = INVALID_BLOCK_INDEX;
844 mNumberOfBlocks = 1;
845 return;
846 }
847
Gilles Debunne157aafc2012-04-19 17:21:57 -0700848 if (newNumberOfBlocks > mBlockEndLines.length) {
Adam Lesinski776abc22014-03-07 11:30:59 -0500849 int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
850 Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
851 int[] blockIndices = new int[blockEndLines.length];
Gilles Debunne157aafc2012-04-19 17:21:57 -0700852 System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700853 System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
Gilles Debunne157aafc2012-04-19 17:21:57 -0700854 System.arraycopy(mBlockEndLines, lastBlock + 1,
855 blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700856 System.arraycopy(mBlockIndices, lastBlock + 1,
857 blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
Gilles Debunne157aafc2012-04-19 17:21:57 -0700858 mBlockEndLines = blockEndLines;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700859 mBlockIndices = blockIndices;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900860 } else if (numAddedBlocks + numRemovedBlocks != 0) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700861 System.arraycopy(mBlockEndLines, lastBlock + 1,
862 mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700863 System.arraycopy(mBlockIndices, lastBlock + 1,
864 mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
865 }
866
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900867 if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
868 final ArraySet<Integer> set = new ArraySet<>();
Siyamed Sinir50b585e2017-12-18 18:47:37 -0800869 final int changedBlockCount = numAddedBlocks - numRemovedBlocks;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900870 for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
871 Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
Siyamed Sinir50b585e2017-12-18 18:47:37 -0800872 if (block < firstBlock) {
873 // block index is before firstBlock add it since it did not change
874 set.add(block);
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900875 }
Siyamed Sinir50b585e2017-12-18 18:47:37 -0800876 if (block > lastBlock) {
877 // block index is after lastBlock, the index reduced to += changedBlockCount
878 block += changedBlockCount;
879 set.add(block);
880 }
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900881 }
882 mBlocksAlwaysNeedToBeRedrawn = set;
883 }
884
Gilles Debunne33b7de852012-03-12 11:57:48 -0700885 mNumberOfBlocks = newNumberOfBlocks;
Raph Levien63b3eb62013-10-03 13:48:04 -0700886 int newFirstChangedBlock;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700887 final int deltaLines = newLineCount - (endLine - startLine + 1);
Sangkyu Lee955beb22012-12-10 15:47:00 +0900888 if (deltaLines != 0) {
889 // Display list whose index is >= mIndexFirstChangedBlock is valid
890 // but it needs to update its drawing location.
Raph Levien63b3eb62013-10-03 13:48:04 -0700891 newFirstChangedBlock = firstBlock + numAddedBlocks;
892 for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
Sangkyu Lee955beb22012-12-10 15:47:00 +0900893 mBlockEndLines[i] += deltaLines;
894 }
895 } else {
Raph Levien63b3eb62013-10-03 13:48:04 -0700896 newFirstChangedBlock = mNumberOfBlocks;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700897 }
Raph Levien63b3eb62013-10-03 13:48:04 -0700898 mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700899
900 int blockIndex = firstBlock;
901 if (createBlockBefore) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700902 mBlockEndLines[blockIndex] = startLine - 1;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900903 updateAlwaysNeedsToBeRedrawn(blockIndex);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700904 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
905 blockIndex++;
906 }
907
908 if (createBlock) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700909 mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900910 updateAlwaysNeedsToBeRedrawn(blockIndex);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700911 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
912 blockIndex++;
913 }
914
915 if (createBlockAfter) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700916 mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900917 updateAlwaysNeedsToBeRedrawn(blockIndex);
Gilles Debunne33b7de852012-03-12 11:57:48 -0700918 mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
919 }
920 }
921
922 /**
Roozbeh Pournaderfc778692017-05-17 14:24:02 -0700923 * This method is used for test purposes only.
Gilles Debunne1e130b22012-03-14 17:40:42 -0700924 * @hide
925 */
Siyamed Sinired09ae12016-02-16 14:36:26 -0800926 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Roozbeh Pournaderfc778692017-05-17 14:24:02 -0700927 public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks,
928 int totalLines) {
Gilles Debunne157aafc2012-04-19 17:21:57 -0700929 mBlockEndLines = new int[blockEndLines.length];
Gilles Debunne1e130b22012-03-14 17:40:42 -0700930 mBlockIndices = new int[blockIndices.length];
Gilles Debunne157aafc2012-04-19 17:21:57 -0700931 System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
Gilles Debunne1e130b22012-03-14 17:40:42 -0700932 System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
933 mNumberOfBlocks = numberOfBlocks;
Roozbeh Pournaderfc778692017-05-17 14:24:02 -0700934 while (mInts.size() < totalLines) {
935 mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]);
936 }
Gilles Debunne1e130b22012-03-14 17:40:42 -0700937 }
938
939 /**
Gilles Debunne33b7de852012-03-12 11:57:48 -0700940 * @hide
941 */
Gilles Debunne157aafc2012-04-19 17:21:57 -0700942 public int[] getBlockEndLines() {
943 return mBlockEndLines;
Gilles Debunne33b7de852012-03-12 11:57:48 -0700944 }
945
946 /**
947 * @hide
948 */
949 public int[] getBlockIndices() {
950 return mBlockIndices;
951 }
952
953 /**
954 * @hide
955 */
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +0900956 public int getBlockIndex(int index) {
957 return mBlockIndices[index];
958 }
959
960 /**
961 * @hide
962 * @param index
963 */
964 public void setBlockIndex(int index, int blockIndex) {
965 mBlockIndices[index] = blockIndex;
966 }
967
968 /**
969 * @hide
970 */
Gilles Debunne33b7de852012-03-12 11:57:48 -0700971 public int getNumberOfBlocks() {
972 return mNumberOfBlocks;
973 }
974
Sangkyu Lee955beb22012-12-10 15:47:00 +0900975 /**
976 * @hide
977 */
978 public int getIndexFirstChangedBlock() {
979 return mIndexFirstChangedBlock;
980 }
981
982 /**
983 * @hide
984 */
985 public void setIndexFirstChangedBlock(int i) {
986 mIndexFirstChangedBlock = i;
987 }
988
Gilles Debunned6e568c2011-01-25 10:14:43 -0800989 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800990 public int getLineCount() {
991 return mInts.size() - 1;
992 }
993
Gilles Debunned6e568c2011-01-25 10:14:43 -0800994 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800995 public int getLineTop(int line) {
996 return mInts.getValue(line, TOP);
997 }
998
Gilles Debunned6e568c2011-01-25 10:14:43 -0800999 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001000 public int getLineDescent(int line) {
1001 return mInts.getValue(line, DESCENT);
1002 }
1003
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001004 /**
1005 * @hide
1006 */
1007 @Override
1008 public int getLineExtra(int line) {
1009 return mInts.getValue(line, EXTRA);
1010 }
1011
Gilles Debunned6e568c2011-01-25 10:14:43 -08001012 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001013 public int getLineStart(int line) {
1014 return mInts.getValue(line, START) & START_MASK;
1015 }
1016
Gilles Debunned6e568c2011-01-25 10:14:43 -08001017 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001018 public boolean getLineContainsTab(int line) {
1019 return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
1020 }
1021
Gilles Debunned6e568c2011-01-25 10:14:43 -08001022 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001023 public int getParagraphDirection(int line) {
1024 return mInts.getValue(line, DIR) >> DIR_SHIFT;
1025 }
1026
Gilles Debunned6e568c2011-01-25 10:14:43 -08001027 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001028 public final Directions getLineDirections(int line) {
1029 return mObjects.getValue(line, 0);
1030 }
1031
Gilles Debunned6e568c2011-01-25 10:14:43 -08001032 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001033 public int getTopPadding() {
1034 return mTopPadding;
1035 }
1036
Gilles Debunned6e568c2011-01-25 10:14:43 -08001037 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001038 public int getBottomPadding() {
1039 return mBottomPadding;
1040 }
1041
Raph Levien26d443a2015-03-30 14:18:32 -07001042 /**
1043 * @hide
1044 */
1045 @Override
1046 public int getHyphen(int line) {
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001047 return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
1048 }
1049
1050 private boolean getContentMayProtrudeFromTopOrBottom(int line) {
1051 return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
1052 & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
Raph Levien26d443a2015-03-30 14:18:32 -07001053 }
1054
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001055 @Override
1056 public int getEllipsizedWidth() {
1057 return mEllipsizedWidth;
1058 }
1059
Gilles Debunne0a4db3c2011-01-14 12:12:04 -08001060 private static class ChangeWatcher implements TextWatcher, SpanWatcher {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001061 public ChangeWatcher(DynamicLayout layout) {
Siyamed Sinira273a702017-10-05 11:22:12 -07001062 mLayout = new WeakReference<>(layout);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001063 }
1064
1065 private void reflow(CharSequence s, int where, int before, int after) {
Gilles Debunned6e568c2011-01-25 10:14:43 -08001066 DynamicLayout ml = mLayout.get();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001067
Roozbeh Pournader15b213d2017-08-21 17:43:17 -07001068 if (ml != null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001069 ml.reflow(s, where, before, after);
Roozbeh Pournader15b213d2017-08-21 17:43:17 -07001070 } else if (s instanceof Spannable) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001071 ((Spannable) s).removeSpan(this);
Roozbeh Pournader15b213d2017-08-21 17:43:17 -07001072 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001073 }
1074
Gilles Debunne0a4db3c2011-01-14 12:12:04 -08001075 public void beforeTextChanged(CharSequence s, int where, int before, int after) {
Gilles Debunne33b7de852012-03-12 11:57:48 -07001076 // Intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001077 }
1078
Gilles Debunne0a4db3c2011-01-14 12:12:04 -08001079 public void onTextChanged(CharSequence s, int where, int before, int after) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001080 reflow(s, where, before, after);
1081 }
1082
1083 public void afterTextChanged(Editable s) {
Gilles Debunne33b7de852012-03-12 11:57:48 -07001084 // Intentionally empty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001085 }
1086
1087 public void onSpanAdded(Spannable s, Object o, int start, int end) {
1088 if (o instanceof UpdateLayout)
1089 reflow(s, start, end - start, end - start);
1090 }
1091
1092 public void onSpanRemoved(Spannable s, Object o, int start, int end) {
1093 if (o instanceof UpdateLayout)
1094 reflow(s, start, end - start, end - start);
1095 }
1096
Gilles Debunne0a4db3c2011-01-14 12:12:04 -08001097 public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001098 if (o instanceof UpdateLayout) {
Siyamed Sinir1fb80bb2018-01-08 13:57:59 -08001099 if (start > end) {
1100 // Bug: 67926915 start cannot be determined, fallback to reflow from start
1101 // instead of causing an exception
1102 start = 0;
1103 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001104 reflow(s, start, end - start, end - start);
1105 reflow(s, nstart, nend - nstart, nend - nstart);
1106 }
1107 }
1108
Gilles Debunned6e568c2011-01-25 10:14:43 -08001109 private WeakReference<DynamicLayout> mLayout;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001110 }
1111
Gilles Debunned6e568c2011-01-25 10:14:43 -08001112 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001113 public int getEllipsisStart(int line) {
1114 if (mEllipsizeAt == null) {
1115 return 0;
1116 }
1117
1118 return mInts.getValue(line, ELLIPSIS_START);
1119 }
1120
Gilles Debunned6e568c2011-01-25 10:14:43 -08001121 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001122 public int getEllipsisCount(int line) {
1123 if (mEllipsizeAt == null) {
1124 return 0;
1125 }
1126
1127 return mInts.getValue(line, ELLIPSIS_COUNT);
1128 }
1129
1130 private CharSequence mBase;
1131 private CharSequence mDisplay;
1132 private ChangeWatcher mWatcher;
1133 private boolean mIncludePad;
Roozbeh Pournader15b213d2017-08-21 17:43:17 -07001134 private boolean mFallbackLineSpacing;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001135 private boolean mEllipsize;
1136 private int mEllipsizedWidth;
1137 private TextUtils.TruncateAt mEllipsizeAt;
Raph Levien39b4db72015-03-25 13:18:20 -07001138 private int mBreakStrategy;
Roozbeh Pournader95c7a132015-05-12 12:01:06 -07001139 private int mHyphenationFrequency;
Seigo Nonaka4b4730d2017-03-31 09:42:16 -07001140 private int mJustificationMode;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001141
1142 private PackedIntVector mInts;
1143 private PackedObjectVector<Directions> mObjects;
1144
Romain Guycde6adf2012-03-15 17:04:47 -07001145 /**
Gilles Debunne33b7de852012-03-12 11:57:48 -07001146 * Value used in mBlockIndices when a block has been created or recycled and indicating that its
1147 * display list needs to be re-created.
1148 * @hide
1149 */
1150 public static final int INVALID_BLOCK_INDEX = -1;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001151 // Stores the line numbers of the last line of each block (inclusive)
1152 private int[] mBlockEndLines;
Gilles Debunne33b7de852012-03-12 11:57:48 -07001153 // The indices of this block's display list in TextView's internal display list array or
1154 // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
1155 private int[] mBlockIndices;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001156 // Set of blocks that always need to be redrawn.
1157 private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
Gilles Debunne33b7de852012-03-12 11:57:48 -07001158 // Number of items actually currently being used in the above 2 arrays
1159 private int mNumberOfBlocks;
Sangkyu Lee955beb22012-12-10 15:47:00 +09001160 // The first index of the blocks whose locations are changed
1161 private int mIndexFirstChangedBlock;
Gilles Debunne33b7de852012-03-12 11:57:48 -07001162
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001163 private int mTopPadding, mBottomPadding;
1164
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001165 private Rect mTempRect = new Rect();
1166
Raph Leviend3ab6922015-03-02 14:30:53 -08001167 private static StaticLayout sStaticLayout = null;
1168 private static StaticLayout.Builder sBuilder = null;
Fabrice Di Meglio8059e0902011-08-10 16:31:58 -07001169
Romain Guye5ea4402011-08-01 14:01:37 -07001170 private static final Object[] sLock = new Object[0];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001171
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001172 // START, DIR, and TAB share the same entry.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001173 private static final int START = 0;
1174 private static final int DIR = START;
1175 private static final int TAB = START;
1176 private static final int TOP = 1;
1177 private static final int DESCENT = 2;
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001178 private static final int EXTRA = 3;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001179 // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001180 private static final int HYPHEN = 4;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001181 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001182 private static final int COLUMNS_NORMAL = 5;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001183
Siyamed Sinir0fa89d62017-07-24 20:46:41 -07001184 private static final int ELLIPSIS_START = 5;
1185 private static final int ELLIPSIS_COUNT = 6;
1186 private static final int COLUMNS_ELLIPSIZE = 7;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001187
1188 private static final int START_MASK = 0x1FFFFFFF;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001189 private static final int DIR_SHIFT = 30;
1190 private static final int TAB_MASK = 0x20000000;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001191 private static final int HYPHEN_MASK = 0xFF;
1192 private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001193
1194 private static final int ELLIPSIS_UNDEFINED = 0x80000000;
1195}