DynamicLayout.java revision a273a70ecbb0d1aaf9aeceec7aa91591290a4871
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.annotation.FloatRange;
20import android.annotation.IntRange;
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.graphics.Paint;
24import android.graphics.Rect;
25import android.text.style.ReplacementSpan;
26import android.text.style.UpdateLayout;
27import android.text.style.WrapTogetherSpan;
28import android.util.ArraySet;
29import android.util.Pools.SynchronizedPool;
30
31import com.android.internal.annotations.VisibleForTesting;
32import com.android.internal.util.ArrayUtils;
33import com.android.internal.util.GrowingArrayUtils;
34
35import 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 */
45public class DynamicLayout extends Layout
46{
47    private static final int PRIORITY = 128;
48    private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
49
50    /**
51     * Builder for dynamic layouts. The builder is the preferred pattern for constructing
52     * DynamicLayout objects and should be preferred over the constructors, particularly to access
53     * newer features. To build a dynamic layout, first call {@link #obtain} with the required
54     * arguments (base, paint, and width), then call setters for optional parameters, and finally
55     * {@link #build} to build the DynamicLayout object. Parameters not explicitly set will get
56     * default values.
57     */
58    public static final class Builder {
59        private Builder() {
60        }
61
62        /**
63         * Obtain a builder for constructing DynamicLayout objects.
64         */
65        @NonNull
66        public static Builder obtain(@NonNull CharSequence base, @NonNull TextPaint paint,
67                @IntRange(from = 0) int width) {
68            Builder b = sPool.acquire();
69            if (b == null) {
70                b = new Builder();
71            }
72
73            // set default initial values
74            b.mBase = base;
75            b.mDisplay = base;
76            b.mPaint = paint;
77            b.mWidth = width;
78            b.mAlignment = Alignment.ALIGN_NORMAL;
79            b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
80            b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
81            b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
82            b.mIncludePad = true;
83            b.mFallbackLineSpacing = false;
84            b.mEllipsizedWidth = width;
85            b.mEllipsize = null;
86            b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
87            b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
88            b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
89            return b;
90        }
91
92        /**
93         * This method should be called after the layout is finished getting constructed and the
94         * builder needs to be cleaned up and returned to the pool.
95         */
96        private static void recycle(@NonNull Builder b) {
97            b.mBase = null;
98            b.mDisplay = null;
99            b.mPaint = null;
100            sPool.release(b);
101        }
102
103        /**
104         * Set the transformed text (password transformation being the primary example of a
105         * transformation) that will be updated as the base text is changed. The default is the
106         * 'base' text passed to the builder's constructor.
107         *
108         * @param display the transformed text
109         * @return this builder, useful for chaining
110         */
111        @NonNull
112        public Builder setDisplayText(@NonNull CharSequence display) {
113            mDisplay = display;
114            return this;
115        }
116
117        /**
118         * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}.
119         *
120         * @param alignment Alignment for the resulting {@link DynamicLayout}
121         * @return this builder, useful for chaining
122         */
123        @NonNull
124        public Builder setAlignment(@NonNull Alignment alignment) {
125            mAlignment = alignment;
126            return this;
127        }
128
129        /**
130         * Set the text direction heuristic. The text direction heuristic is used to resolve text
131         * direction per-paragraph based on the input text. The default is
132         * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
133         *
134         * @param textDir text direction heuristic for resolving bidi behavior.
135         * @return this builder, useful for chaining
136         */
137        @NonNull
138        public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
139            mTextDir = textDir;
140            return this;
141        }
142
143        /**
144         * Set line spacing parameters. Each line will have its line spacing multiplied by
145         * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for
146         * {@code spacingAdd} and 1.0 for {@code spacingMult}.
147         *
148         * @param spacingAdd the amount of line spacing addition
149         * @param spacingMult the line spacing multiplier
150         * @return this builder, useful for chaining
151         * @see android.widget.TextView#setLineSpacing
152         */
153        @NonNull
154        public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) {
155            mSpacingAdd = spacingAdd;
156            mSpacingMult = spacingMult;
157            return this;
158        }
159
160        /**
161         * Set whether to include extra space beyond font ascent and descent (which is needed to
162         * avoid clipping in some languages, such as Arabic and Kannada). The default is
163         * {@code true}.
164         *
165         * @param includePad whether to include padding
166         * @return this builder, useful for chaining
167         * @see android.widget.TextView#setIncludeFontPadding
168         */
169        @NonNull
170        public Builder setIncludePad(boolean includePad) {
171            mIncludePad = includePad;
172            return this;
173        }
174
175        /**
176         * Set whether to respect the ascent and descent of the fallback fonts that are used in
177         * displaying the text (which is needed to avoid text from consecutive lines running into
178         * each other). If set, fallback fonts that end up getting used can increase the ascent
179         * and descent of the lines that they are used on.
180         *
181         * <p>For backward compatibility reasons, the default is {@code false}, but setting this to
182         * true is strongly recommended. It is required to be true if text could be in languages
183         * like Burmese or Tibetan where text is typically much taller or deeper than Latin text.
184         *
185         * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts
186         * @return this builder, useful for chaining
187         */
188        @NonNull
189        public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) {
190            mFallbackLineSpacing = useLineSpacingFromFallbacks;
191            return this;
192        }
193
194        /**
195         * Set the width as used for ellipsizing purposes, if it differs from the normal layout
196         * width. The default is the {@code width} passed to {@link #obtain}.
197         *
198         * @param ellipsizedWidth width used for ellipsizing, in pixels
199         * @return this builder, useful for chaining
200         * @see android.widget.TextView#setEllipsize
201         */
202        @NonNull
203        public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) {
204            mEllipsizedWidth = ellipsizedWidth;
205            return this;
206        }
207
208        /**
209         * Set ellipsizing on the layout. Causes words that are longer than the view is wide, or
210         * exceeding the number of lines (see #setMaxLines) in the case of
211         * {@link android.text.TextUtils.TruncateAt#END} or
212         * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead of broken.
213         * The default is {@code null}, indicating no ellipsis is to be applied.
214         *
215         * @param ellipsize type of ellipsis behavior
216         * @return this builder, useful for chaining
217         * @see android.widget.TextView#setEllipsize
218         */
219        public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
220            mEllipsize = ellipsize;
221            return this;
222        }
223
224        /**
225         * Set break strategy, useful for selecting high quality or balanced paragraph layout
226         * options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}.
227         *
228         * @param breakStrategy break strategy for paragraph layout
229         * @return this builder, useful for chaining
230         * @see android.widget.TextView#setBreakStrategy
231         */
232        @NonNull
233        public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
234            mBreakStrategy = breakStrategy;
235            return this;
236        }
237
238        /**
239         * Set hyphenation frequency, to control the amount of automatic hyphenation used. The
240         * possible values are defined in {@link Layout}, by constants named with the pattern
241         * {@code HYPHENATION_FREQUENCY_*}. The default is
242         * {@link Layout#HYPHENATION_FREQUENCY_NONE}.
243         *
244         * @param hyphenationFrequency hyphenation frequency for the paragraph
245         * @return this builder, useful for chaining
246         * @see android.widget.TextView#setHyphenationFrequency
247         */
248        @NonNull
249        public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
250            mHyphenationFrequency = hyphenationFrequency;
251            return this;
252        }
253
254        /**
255         * Set paragraph justification mode. The default value is
256         * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification,
257         * the last line will be displayed with the alignment set by {@link #setAlignment}.
258         *
259         * @param justificationMode justification mode for the paragraph.
260         * @return this builder, useful for chaining.
261         */
262        @NonNull
263        public Builder setJustificationMode(@JustificationMode int justificationMode) {
264            mJustificationMode = justificationMode;
265            return this;
266        }
267
268        /**
269         * Build the {@link DynamicLayout} after options have been set.
270         *
271         * <p>Note: the builder object must not be reused in any way after calling this method.
272         * Setting parameters after calling this method, or calling it a second time on the same
273         * builder object, will likely lead to unexpected results.
274         *
275         * @return the newly constructed {@link DynamicLayout} object
276         */
277        @NonNull
278        public DynamicLayout build() {
279            final DynamicLayout result = new DynamicLayout(this);
280            Builder.recycle(this);
281            return result;
282        }
283
284        private CharSequence mBase;
285        private CharSequence mDisplay;
286        private TextPaint mPaint;
287        private int mWidth;
288        private Alignment mAlignment;
289        private TextDirectionHeuristic mTextDir;
290        private float mSpacingMult;
291        private float mSpacingAdd;
292        private boolean mIncludePad;
293        private boolean mFallbackLineSpacing;
294        private int mBreakStrategy;
295        private int mHyphenationFrequency;
296        private int mJustificationMode;
297        private TextUtils.TruncateAt mEllipsize;
298        private int mEllipsizedWidth;
299
300        private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
301
302        private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
303    }
304
305    /**
306     * Make a layout for the specified text that will be updated as the text is changed.
307     */
308    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,
312                         boolean includepad) {
313        this(base, base, paint, width, align, spacingmult, spacingadd,
314             includepad);
315    }
316
317    /**
318     * Make a layout for the transformed text (password transformation being the primary example of
319     * a transformation) that will be updated as the base text is changed.
320     */
321    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,
325                         boolean includepad) {
326        this(base, display, paint, width, align, spacingmult, spacingadd,
327             includepad, null, 0);
328    }
329
330    /**
331     * Make a layout for the transformed text (password transformation being the primary example of
332     * a transformation) that will be updated as the base text is changed. If ellipsize is non-null,
333     * the Layout will ellipsize the text down to ellipsizedWidth.
334     */
335    public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
336                         @NonNull TextPaint paint,
337                         @IntRange(from = 0) int width, @NonNull Alignment align,
338                         @FloatRange(from = 0.0) float spacingmult, float spacingadd,
339                         boolean includepad,
340                         @Nullable TextUtils.TruncateAt ellipsize,
341                         @IntRange(from = 0) int ellipsizedWidth) {
342        this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
343                spacingmult, spacingadd, includepad,
344                Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE,
345                Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth);
346    }
347
348    /**
349     * Make a layout for the transformed text (password transformation being the primary example of
350     * a transformation) that will be updated as the base text is changed. If ellipsize is non-null,
351     * the Layout will ellipsize the text down to ellipsizedWidth.
352     *
353     * @hide
354     */
355    public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display,
356                         @NonNull TextPaint paint,
357                         @IntRange(from = 0) int width,
358                         @NonNull Alignment align, @NonNull TextDirectionHeuristic textDir,
359                         @FloatRange(from = 0.0) float spacingmult, float spacingadd,
360                         boolean includepad, @BreakStrategy int breakStrategy,
361                         @HyphenationFrequency int hyphenationFrequency,
362                         @JustificationMode int justificationMode,
363                         @Nullable TextUtils.TruncateAt ellipsize,
364                         @IntRange(from = 0) int ellipsizedWidth) {
365        super(createEllipsizer(ellipsize, display),
366              paint, width, align, textDir, spacingmult, spacingadd);
367
368        final Builder b = Builder.obtain(base, paint, width)
369                .setAlignment(align)
370                .setTextDirection(textDir)
371                .setLineSpacing(spacingadd, spacingmult)
372                .setEllipsizedWidth(ellipsizedWidth)
373                .setEllipsize(ellipsize);
374        mDisplay = display;
375        mIncludePad = includepad;
376        mBreakStrategy = breakStrategy;
377        mJustificationMode = justificationMode;
378        mHyphenationFrequency = hyphenationFrequency;
379
380        generate(b);
381
382        Builder.recycle(b);
383    }
384
385    private DynamicLayout(@NonNull Builder b) {
386        super(createEllipsizer(b.mEllipsize, b.mDisplay),
387                b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd);
388
389        mDisplay = b.mDisplay;
390        mIncludePad = b.mIncludePad;
391        mBreakStrategy = b.mBreakStrategy;
392        mJustificationMode = b.mJustificationMode;
393        mHyphenationFrequency = b.mHyphenationFrequency;
394
395        generate(b);
396    }
397
398    @NonNull
399    private static CharSequence createEllipsizer(@Nullable TextUtils.TruncateAt ellipsize,
400            @NonNull CharSequence display) {
401        if (ellipsize == null) {
402            return display;
403        } else if (display instanceof Spanned) {
404            return new SpannedEllipsizer(display);
405        } else {
406            return new Ellipsizer(display);
407        }
408    }
409
410    private void generate(@NonNull Builder b) {
411        mBase = b.mBase;
412        mFallbackLineSpacing = b.mFallbackLineSpacing;
413        if (b.mEllipsize != null) {
414            mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
415            mEllipsizedWidth = b.mEllipsizedWidth;
416            mEllipsizeAt = b.mEllipsize;
417
418            /*
419             * This is annoying, but we can't refer to the layout until superclass construction is
420             * finished, and the superclass constructor wants the reference to the display text.
421             *
422             * In other words, the two Ellipsizer classes in Layout.java need a
423             * (Dynamic|Static)Layout as a parameter to do their calculations, but the Ellipsizers
424             * also need to be the input to the superclass's constructor (Layout). In order to go
425             * around the circular dependency, we construct the Ellipsizer with only one of the
426             * parameters, the text (in createEllipsizer). And we fill in the rest of the needed
427             * information (layout, width, and method) later, here.
428             *
429             * This will break if the superclass constructor ever actually cares about the content
430             * instead of just holding the reference.
431             */
432            final Ellipsizer e = (Ellipsizer) getText();
433            e.mLayout = this;
434            e.mWidth = b.mEllipsizedWidth;
435            e.mMethod = b.mEllipsize;
436            mEllipsize = true;
437        } else {
438            mInts = new PackedIntVector(COLUMNS_NORMAL);
439            mEllipsizedWidth = b.mWidth;
440            mEllipsizeAt = null;
441        }
442
443        mObjects = new PackedObjectVector<>(1);
444
445        // Initial state is a single line with 0 characters (0 to 0), with top at 0 and bottom at
446        // whatever is natural, and undefined ellipsis.
447
448        int[] start;
449
450        if (b.mEllipsize != null) {
451            start = new int[COLUMNS_ELLIPSIZE];
452            start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
453        } else {
454            start = new int[COLUMNS_NORMAL];
455        }
456
457        final Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
458
459        final Paint.FontMetricsInt fm = b.mFontMetricsInt;
460        b.mPaint.getFontMetricsInt(fm);
461        final int asc = fm.ascent;
462        final int desc = fm.descent;
463
464        start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
465        start[TOP] = 0;
466        start[DESCENT] = desc;
467        mInts.insertAt(0, start);
468
469        start[TOP] = desc - asc;
470        mInts.insertAt(1, start);
471
472        mObjects.insertAt(0, dirs);
473
474        final int baseLength = mBase.length();
475        // Update from 0 characters to whatever the real text is
476        reflow(mBase, 0, 0, baseLength);
477
478        if (mBase instanceof Spannable) {
479            if (mWatcher == null)
480                mWatcher = new ChangeWatcher(this);
481
482            // Strip out any watchers for other DynamicLayouts.
483            final Spannable sp = (Spannable) mBase;
484            final ChangeWatcher[] spans = sp.getSpans(0, baseLength, ChangeWatcher.class);
485            for (int i = 0; i < spans.length; i++) {
486                sp.removeSpan(spans[i]);
487            }
488
489            sp.setSpan(mWatcher, 0, baseLength,
490                       Spannable.SPAN_INCLUSIVE_INCLUSIVE |
491                       (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
492        }
493    }
494
495    private void reflow(CharSequence s, int where, int before, int after) {
496        if (s != mBase)
497            return;
498
499        CharSequence text = mDisplay;
500        int len = text.length();
501
502        // seek back to the start of the paragraph
503
504        int find = TextUtils.lastIndexOf(text, '\n', where - 1);
505        if (find < 0)
506            find = 0;
507        else
508            find = find + 1;
509
510        {
511            int diff = where - find;
512            before += diff;
513            after += diff;
514            where -= diff;
515        }
516
517        // seek forward to the end of the paragraph
518
519        int look = TextUtils.indexOf(text, '\n', where + after);
520        if (look < 0)
521            look = len;
522        else
523            look++; // we want the index after the \n
524
525        int change = look - (where + after);
526        before += change;
527        after += change;
528
529        // seek further out to cover anything that is forced to wrap together
530
531        if (text instanceof Spanned) {
532            Spanned sp = (Spanned) text;
533            boolean again;
534
535            do {
536                again = false;
537
538                Object[] force = sp.getSpans(where, where + after,
539                                             WrapTogetherSpan.class);
540
541                for (int i = 0; i < force.length; i++) {
542                    int st = sp.getSpanStart(force[i]);
543                    int en = sp.getSpanEnd(force[i]);
544
545                    if (st < where) {
546                        again = true;
547
548                        int diff = where - st;
549                        before += diff;
550                        after += diff;
551                        where -= diff;
552                    }
553
554                    if (en > where + after) {
555                        again = true;
556
557                        int diff = en - (where + after);
558                        before += diff;
559                        after += diff;
560                    }
561                }
562            } while (again);
563        }
564
565        // find affected region of old layout
566
567        int startline = getLineForOffset(where);
568        int startv = getLineTop(startline);
569
570        int endline = getLineForOffset(where + before);
571        if (where + after == len)
572            endline = getLineCount();
573        int endv = getLineTop(endline);
574        boolean islast = (endline == getLineCount());
575
576        // generate new layout for affected text
577
578        StaticLayout reflowed;
579        StaticLayout.Builder b;
580
581        synchronized (sLock) {
582            reflowed = sStaticLayout;
583            b = sBuilder;
584            sStaticLayout = null;
585            sBuilder = null;
586        }
587
588        if (reflowed == null) {
589            reflowed = new StaticLayout(null);
590            b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth());
591        }
592
593        b.setText(text, where, where + after)
594                .setPaint(getPaint())
595                .setWidth(getWidth())
596                .setTextDirection(getTextDirectionHeuristic())
597                .setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
598                .setUseLineSpacingFromFallbacks(mFallbackLineSpacing)
599                .setEllipsizedWidth(mEllipsizedWidth)
600                .setEllipsize(mEllipsizeAt)
601                .setBreakStrategy(mBreakStrategy)
602                .setHyphenationFrequency(mHyphenationFrequency)
603                .setJustificationMode(mJustificationMode)
604                .setAddLastLineLineSpacing(!islast);
605
606        reflowed.generate(b, false /*includepad*/, true /*trackpad*/);
607        int n = reflowed.getLineCount();
608        // If the new layout has a blank line at the end, but it is not
609        // the very end of the buffer, then we already have a line that
610        // starts there, so disregard the blank line.
611
612        if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
613            n--;
614
615        // remove affected lines from old layout
616        mInts.deleteAt(startline, endline - startline);
617        mObjects.deleteAt(startline, endline - startline);
618
619        // adjust offsets in layout for new height and offsets
620
621        int ht = reflowed.getLineTop(n);
622        int toppad = 0, botpad = 0;
623
624        if (mIncludePad && startline == 0) {
625            toppad = reflowed.getTopPadding();
626            mTopPadding = toppad;
627            ht -= toppad;
628        }
629        if (mIncludePad && islast) {
630            botpad = reflowed.getBottomPadding();
631            mBottomPadding = botpad;
632            ht += botpad;
633        }
634
635        mInts.adjustValuesBelow(startline, START, after - before);
636        mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
637
638        // insert new layout
639
640        int[] ints;
641
642        if (mEllipsize) {
643            ints = new int[COLUMNS_ELLIPSIZE];
644            ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
645        } else {
646            ints = new int[COLUMNS_NORMAL];
647        }
648
649        Directions[] objects = new Directions[1];
650
651        for (int i = 0; i < n; i++) {
652            final int start = reflowed.getLineStart(i);
653            ints[START] = start;
654            ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT;
655            ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0;
656
657            int top = reflowed.getLineTop(i) + startv;
658            if (i > 0)
659                top -= toppad;
660            ints[TOP] = top;
661
662            int desc = reflowed.getLineDescent(i);
663            if (i == n - 1)
664                desc += botpad;
665
666            ints[DESCENT] = desc;
667            ints[EXTRA] = reflowed.getLineExtra(i);
668            objects[0] = reflowed.getLineDirections(i);
669
670            final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1);
671            ints[HYPHEN] = reflowed.getHyphen(i) & HYPHEN_MASK;
672            ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |=
673                    contentMayProtrudeFromLineTopOrBottom(text, start, end) ?
674                            MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0;
675
676            if (mEllipsize) {
677                ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
678                ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
679            }
680
681            mInts.insertAt(startline + i, ints);
682            mObjects.insertAt(startline + i, objects);
683        }
684
685        updateBlocks(startline, endline - 1, n);
686
687        b.finish();
688        synchronized (sLock) {
689            sStaticLayout = reflowed;
690            sBuilder = b;
691        }
692    }
693
694    private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) {
695        if (text instanceof Spanned) {
696            final Spanned spanned = (Spanned) text;
697            if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) {
698                return true;
699            }
700        }
701        // Spans other than ReplacementSpan can be ignored because line top and bottom are
702        // disjunction of all tops and bottoms, although it's not optimal.
703        final Paint paint = getPaint();
704        paint.getTextBounds(text, start, end, mTempRect);
705        final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
706        return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom;
707    }
708
709    /**
710     * Create the initial block structure, cutting the text into blocks of at least
711     * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
712     */
713    private void createBlocks() {
714        int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
715        mNumberOfBlocks = 0;
716        final CharSequence text = mDisplay;
717
718        while (true) {
719            offset = TextUtils.indexOf(text, '\n', offset);
720            if (offset < 0) {
721                addBlockAtOffset(text.length());
722                break;
723            } else {
724                addBlockAtOffset(offset);
725                offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
726            }
727        }
728
729        // mBlockIndices and mBlockEndLines should have the same length
730        mBlockIndices = new int[mBlockEndLines.length];
731        for (int i = 0; i < mBlockEndLines.length; i++) {
732            mBlockIndices[i] = INVALID_BLOCK_INDEX;
733        }
734    }
735
736    /**
737     * @hide
738     */
739    public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
740        return mBlocksAlwaysNeedToBeRedrawn;
741    }
742
743    private void updateAlwaysNeedsToBeRedrawn(int blockIndex) {
744        int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1);
745        int endLine = mBlockEndLines[blockIndex];
746        for (int i = startLine; i <= endLine; i++) {
747            if (getContentMayProtrudeFromTopOrBottom(i)) {
748                if (mBlocksAlwaysNeedToBeRedrawn == null) {
749                    mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>();
750                }
751                mBlocksAlwaysNeedToBeRedrawn.add(blockIndex);
752                return;
753            }
754        }
755        if (mBlocksAlwaysNeedToBeRedrawn != null) {
756            mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex);
757        }
758    }
759
760    /**
761     * Create a new block, ending at the specified character offset.
762     * A block will actually be created only if has at least one line, i.e. this offset is
763     * not on the end line of the previous block.
764     */
765    private void addBlockAtOffset(int offset) {
766        final int line = getLineForOffset(offset);
767        if (mBlockEndLines == null) {
768            // Initial creation of the array, no test on previous block ending line
769            mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
770            mBlockEndLines[mNumberOfBlocks] = line;
771            updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
772            mNumberOfBlocks++;
773            return;
774        }
775
776        final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
777        if (line > previousBlockEndLine) {
778            mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
779            updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
780            mNumberOfBlocks++;
781        }
782    }
783
784    /**
785     * This method is called every time the layout is reflowed after an edition.
786     * It updates the internal block data structure. The text is split in blocks
787     * of contiguous lines, with at least one block for the entire text.
788     * When a range of lines is edited, new blocks (from 0 to 3 depending on the
789     * overlap structure) will replace the set of overlapping blocks.
790     * Blocks are listed in order and are represented by their ending line number.
791     * An index is associated to each block (which will be used by display lists),
792     * this class simply invalidates the index of blocks overlapping a modification.
793     *
794     * @param startLine the first line of the range of modified lines
795     * @param endLine the last line of the range, possibly equal to startLine, lower
796     * than getLineCount()
797     * @param newLineCount the number of lines that will replace the range, possibly 0
798     *
799     * @hide
800     */
801    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
802    public void updateBlocks(int startLine, int endLine, int newLineCount) {
803        if (mBlockEndLines == null) {
804            createBlocks();
805            return;
806        }
807
808        int firstBlock = -1;
809        int lastBlock = -1;
810        for (int i = 0; i < mNumberOfBlocks; i++) {
811            if (mBlockEndLines[i] >= startLine) {
812                firstBlock = i;
813                break;
814            }
815        }
816        for (int i = firstBlock; i < mNumberOfBlocks; i++) {
817            if (mBlockEndLines[i] >= endLine) {
818                lastBlock = i;
819                break;
820            }
821        }
822        final int lastBlockEndLine = mBlockEndLines[lastBlock];
823
824        boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
825                mBlockEndLines[firstBlock - 1] + 1);
826        boolean createBlock = newLineCount > 0;
827        boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
828
829        int numAddedBlocks = 0;
830        if (createBlockBefore) numAddedBlocks++;
831        if (createBlock) numAddedBlocks++;
832        if (createBlockAfter) numAddedBlocks++;
833
834        final int numRemovedBlocks = lastBlock - firstBlock + 1;
835        final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
836
837        if (newNumberOfBlocks == 0) {
838            // Even when text is empty, there is actually one line and hence one block
839            mBlockEndLines[0] = 0;
840            mBlockIndices[0] = INVALID_BLOCK_INDEX;
841            mNumberOfBlocks = 1;
842            return;
843        }
844
845        if (newNumberOfBlocks > mBlockEndLines.length) {
846            int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
847                    Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
848            int[] blockIndices = new int[blockEndLines.length];
849            System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
850            System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
851            System.arraycopy(mBlockEndLines, lastBlock + 1,
852                    blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
853            System.arraycopy(mBlockIndices, lastBlock + 1,
854                    blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
855            mBlockEndLines = blockEndLines;
856            mBlockIndices = blockIndices;
857        } else if (numAddedBlocks + numRemovedBlocks != 0) {
858            System.arraycopy(mBlockEndLines, lastBlock + 1,
859                    mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
860            System.arraycopy(mBlockIndices, lastBlock + 1,
861                    mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
862        }
863
864        if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
865            final ArraySet<Integer> set = new ArraySet<>();
866            for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
867                Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
868                if (block > firstBlock) {
869                    block += numAddedBlocks - numRemovedBlocks;
870                }
871                set.add(block);
872            }
873            mBlocksAlwaysNeedToBeRedrawn = set;
874        }
875
876        mNumberOfBlocks = newNumberOfBlocks;
877        int newFirstChangedBlock;
878        final int deltaLines = newLineCount - (endLine - startLine + 1);
879        if (deltaLines != 0) {
880            // Display list whose index is >= mIndexFirstChangedBlock is valid
881            // but it needs to update its drawing location.
882            newFirstChangedBlock = firstBlock + numAddedBlocks;
883            for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
884                mBlockEndLines[i] += deltaLines;
885            }
886        } else {
887            newFirstChangedBlock = mNumberOfBlocks;
888        }
889        mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
890
891        int blockIndex = firstBlock;
892        if (createBlockBefore) {
893            mBlockEndLines[blockIndex] = startLine - 1;
894            updateAlwaysNeedsToBeRedrawn(blockIndex);
895            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
896            blockIndex++;
897        }
898
899        if (createBlock) {
900            mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
901            updateAlwaysNeedsToBeRedrawn(blockIndex);
902            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
903            blockIndex++;
904        }
905
906        if (createBlockAfter) {
907            mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
908            updateAlwaysNeedsToBeRedrawn(blockIndex);
909            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
910        }
911    }
912
913    /**
914     * This method is used for test purposes only.
915     * @hide
916     */
917    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
918    public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks,
919            int totalLines) {
920        mBlockEndLines = new int[blockEndLines.length];
921        mBlockIndices = new int[blockIndices.length];
922        System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
923        System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
924        mNumberOfBlocks = numberOfBlocks;
925        while (mInts.size() < totalLines) {
926            mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]);
927        }
928    }
929
930    /**
931     * @hide
932     */
933    public int[] getBlockEndLines() {
934        return mBlockEndLines;
935    }
936
937    /**
938     * @hide
939     */
940    public int[] getBlockIndices() {
941        return mBlockIndices;
942    }
943
944    /**
945     * @hide
946     */
947    public int getBlockIndex(int index) {
948        return mBlockIndices[index];
949    }
950
951    /**
952     * @hide
953     * @param index
954     */
955    public void setBlockIndex(int index, int blockIndex) {
956        mBlockIndices[index] = blockIndex;
957    }
958
959    /**
960     * @hide
961     */
962    public int getNumberOfBlocks() {
963        return mNumberOfBlocks;
964    }
965
966    /**
967     * @hide
968     */
969    public int getIndexFirstChangedBlock() {
970        return mIndexFirstChangedBlock;
971    }
972
973    /**
974     * @hide
975     */
976    public void setIndexFirstChangedBlock(int i) {
977        mIndexFirstChangedBlock = i;
978    }
979
980    @Override
981    public int getLineCount() {
982        return mInts.size() - 1;
983    }
984
985    @Override
986    public int getLineTop(int line) {
987        return mInts.getValue(line, TOP);
988    }
989
990    @Override
991    public int getLineDescent(int line) {
992        return mInts.getValue(line, DESCENT);
993    }
994
995    /**
996     * @hide
997     */
998    @Override
999    public int getLineExtra(int line) {
1000        return mInts.getValue(line, EXTRA);
1001    }
1002
1003    @Override
1004    public int getLineStart(int line) {
1005        return mInts.getValue(line, START) & START_MASK;
1006    }
1007
1008    @Override
1009    public boolean getLineContainsTab(int line) {
1010        return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
1011    }
1012
1013    @Override
1014    public int getParagraphDirection(int line) {
1015        return mInts.getValue(line, DIR) >> DIR_SHIFT;
1016    }
1017
1018    @Override
1019    public final Directions getLineDirections(int line) {
1020        return mObjects.getValue(line, 0);
1021    }
1022
1023    @Override
1024    public int getTopPadding() {
1025        return mTopPadding;
1026    }
1027
1028    @Override
1029    public int getBottomPadding() {
1030        return mBottomPadding;
1031    }
1032
1033    /**
1034     * @hide
1035     */
1036    @Override
1037    public int getHyphen(int line) {
1038        return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
1039    }
1040
1041    private boolean getContentMayProtrudeFromTopOrBottom(int line) {
1042        return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
1043                & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
1044    }
1045
1046    @Override
1047    public int getEllipsizedWidth() {
1048        return mEllipsizedWidth;
1049    }
1050
1051    private static class ChangeWatcher implements TextWatcher, SpanWatcher {
1052        public ChangeWatcher(DynamicLayout layout) {
1053            mLayout = new WeakReference<>(layout);
1054        }
1055
1056        private void reflow(CharSequence s, int where, int before, int after) {
1057            DynamicLayout ml = mLayout.get();
1058
1059            if (ml != null) {
1060                ml.reflow(s, where, before, after);
1061            } else if (s instanceof Spannable) {
1062                ((Spannable) s).removeSpan(this);
1063            }
1064        }
1065
1066        public void beforeTextChanged(CharSequence s, int where, int before, int after) {
1067            // Intentionally empty
1068        }
1069
1070        public void onTextChanged(CharSequence s, int where, int before, int after) {
1071            reflow(s, where, before, after);
1072        }
1073
1074        public void afterTextChanged(Editable s) {
1075            // Intentionally empty
1076        }
1077
1078        public void onSpanAdded(Spannable s, Object o, int start, int end) {
1079            if (o instanceof UpdateLayout)
1080                reflow(s, start, end - start, end - start);
1081        }
1082
1083        public void onSpanRemoved(Spannable s, Object o, int start, int end) {
1084            if (o instanceof UpdateLayout)
1085                reflow(s, start, end - start, end - start);
1086        }
1087
1088        public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
1089            if (o instanceof UpdateLayout) {
1090                reflow(s, start, end - start, end - start);
1091                reflow(s, nstart, nend - nstart, nend - nstart);
1092            }
1093        }
1094
1095        private WeakReference<DynamicLayout> mLayout;
1096    }
1097
1098    @Override
1099    public int getEllipsisStart(int line) {
1100        if (mEllipsizeAt == null) {
1101            return 0;
1102        }
1103
1104        return mInts.getValue(line, ELLIPSIS_START);
1105    }
1106
1107    @Override
1108    public int getEllipsisCount(int line) {
1109        if (mEllipsizeAt == null) {
1110            return 0;
1111        }
1112
1113        return mInts.getValue(line, ELLIPSIS_COUNT);
1114    }
1115
1116    private CharSequence mBase;
1117    private CharSequence mDisplay;
1118    private ChangeWatcher mWatcher;
1119    private boolean mIncludePad;
1120    private boolean mFallbackLineSpacing;
1121    private boolean mEllipsize;
1122    private int mEllipsizedWidth;
1123    private TextUtils.TruncateAt mEllipsizeAt;
1124    private int mBreakStrategy;
1125    private int mHyphenationFrequency;
1126    private int mJustificationMode;
1127
1128    private PackedIntVector mInts;
1129    private PackedObjectVector<Directions> mObjects;
1130
1131    /**
1132     * Value used in mBlockIndices when a block has been created or recycled and indicating that its
1133     * display list needs to be re-created.
1134     * @hide
1135     */
1136    public static final int INVALID_BLOCK_INDEX = -1;
1137    // Stores the line numbers of the last line of each block (inclusive)
1138    private int[] mBlockEndLines;
1139    // The indices of this block's display list in TextView's internal display list array or
1140    // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
1141    private int[] mBlockIndices;
1142    // Set of blocks that always need to be redrawn.
1143    private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
1144    // Number of items actually currently being used in the above 2 arrays
1145    private int mNumberOfBlocks;
1146    // The first index of the blocks whose locations are changed
1147    private int mIndexFirstChangedBlock;
1148
1149    private int mTopPadding, mBottomPadding;
1150
1151    private Rect mTempRect = new Rect();
1152
1153    private static StaticLayout sStaticLayout = null;
1154    private static StaticLayout.Builder sBuilder = null;
1155
1156    private static final Object[] sLock = new Object[0];
1157
1158    // START, DIR, and TAB share the same entry.
1159    private static final int START = 0;
1160    private static final int DIR = START;
1161    private static final int TAB = START;
1162    private static final int TOP = 1;
1163    private static final int DESCENT = 2;
1164    private static final int EXTRA = 3;
1165    // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
1166    private static final int HYPHEN = 4;
1167    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
1168    private static final int COLUMNS_NORMAL = 5;
1169
1170    private static final int ELLIPSIS_START = 5;
1171    private static final int ELLIPSIS_COUNT = 6;
1172    private static final int COLUMNS_ELLIPSIZE = 7;
1173
1174    private static final int START_MASK = 0x1FFFFFFF;
1175    private static final int DIR_SHIFT  = 30;
1176    private static final int TAB_MASK   = 0x20000000;
1177    private static final int HYPHEN_MASK = 0xFF;
1178    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
1179
1180    private static final int ELLIPSIS_UNDEFINED = 0x80000000;
1181}
1182