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    private static final int PRIORITY = 128;
47    private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
48
49    /**
50     * 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.
56     */
57    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;
82            b.mFallbackLineSpacing = false;
83            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        /**
175         * 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        /**
194         * 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;
292        private boolean mFallbackLineSpacing;
293        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
301        private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
302    }
303
304    /**
305     * @deprecated Use {@link Builder} instead.
306     */
307    @Deprecated
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     * @deprecated Use {@link Builder} instead.
319     */
320    @Deprecated
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     * @deprecated Use {@link Builder} instead.
332     */
333    @Deprecated
334    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,
338                         boolean includepad,
339                         @Nullable TextUtils.TruncateAt ellipsize,
340                         @IntRange(from = 0) int ellipsizedWidth) {
341        this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
342                spacingmult, spacingadd, includepad,
343                Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE,
344                Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth);
345    }
346
347    /**
348     * 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
353     * @deprecated Use {@link Builder} instead.
354     */
355    @Deprecated
356    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),
367              paint, width, align, textDir, spacingmult, spacingadd);
368
369        final Builder b = Builder.obtain(base, paint, width)
370                .setAlignment(align)
371                .setTextDirection(textDir)
372                .setLineSpacing(spacingadd, spacingmult)
373                .setEllipsizedWidth(ellipsizedWidth)
374                .setEllipsize(ellipsize);
375        mDisplay = display;
376        mIncludePad = includepad;
377        mBreakStrategy = breakStrategy;
378        mJustificationMode = justificationMode;
379        mHyphenationFrequency = hyphenationFrequency;
380
381        generate(b);
382
383        Builder.recycle(b);
384    }
385
386    private DynamicLayout(@NonNull Builder b) {
387        super(createEllipsizer(b.mEllipsize, b.mDisplay),
388                b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd);
389
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;
413        mFallbackLineSpacing = b.mFallbackLineSpacing;
414        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();
434            e.mLayout = this;
435            e.mWidth = b.mEllipsizedWidth;
436            e.mMethod = b.mEllipsize;
437            mEllipsize = true;
438        } else {
439            mInts = new PackedIntVector(COLUMNS_NORMAL);
440            mEllipsizedWidth = b.mWidth;
441            mEllipsizeAt = null;
442        }
443
444        mObjects = new PackedObjectVector<>(1);
445
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.
448
449        int[] start;
450
451        if (b.mEllipsize != null) {
452            start = new int[COLUMNS_ELLIPSIZE];
453            start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
454        } else {
455            start = new int[COLUMNS_NORMAL];
456        }
457
458        final Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
459
460        final Paint.FontMetricsInt fm = b.mFontMetricsInt;
461        b.mPaint.getFontMetricsInt(fm);
462        final int asc = fm.ascent;
463        final int desc = fm.descent;
464
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
475        final int baseLength = mBase.length();
476        // Update from 0 characters to whatever the real text is
477        reflow(mBase, 0, 0, baseLength);
478
479        if (mBase instanceof Spannable) {
480            if (mWatcher == null)
481                mWatcher = new ChangeWatcher(this);
482
483            // Strip out any watchers for other DynamicLayouts.
484            final Spannable sp = (Spannable) mBase;
485            final ChangeWatcher[] spans = sp.getSpans(0, baseLength, ChangeWatcher.class);
486            for (int i = 0; i < spans.length; i++) {
487                sp.removeSpan(spans[i]);
488            }
489
490            sp.setSpan(mWatcher, 0, baseLength,
491                       Spannable.SPAN_INCLUSIVE_INCLUSIVE |
492                       (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
493        }
494    }
495
496    /** @hide */
497    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
498    public void reflow(CharSequence s, int where, int before, int after) {
499        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;
582        StaticLayout.Builder b;
583
584        synchronized (sLock) {
585            reflowed = sStaticLayout;
586            b = sBuilder;
587            sStaticLayout = null;
588            sBuilder = null;
589        }
590
591        if (reflowed == null) {
592            reflowed = new StaticLayout(null);
593            b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth());
594        }
595
596        b.setText(text, where, where + after)
597                .setPaint(getPaint())
598                .setWidth(getWidth())
599                .setTextDirection(getTextDirectionHeuristic())
600                .setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
601                .setUseLineSpacingFromFallbacks(mFallbackLineSpacing)
602                .setEllipsizedWidth(mEllipsizedWidth)
603                .setEllipsize(mEllipsizeAt)
604                .setBreakStrategy(mBreakStrategy)
605                .setHyphenationFrequency(mHyphenationFrequency)
606                .setJustificationMode(mJustificationMode)
607                .setAddLastLineLineSpacing(!islast);
608
609        reflowed.generate(b, false /*includepad*/, true /*trackpad*/);
610        int n = reflowed.getLineCount();
611        // 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
615        if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
616            n--;
617
618        // remove affected lines from old layout
619        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
654        for (int i = 0; i < n; i++) {
655            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;
659
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;
670            ints[EXTRA] = reflowed.getLineExtra(i);
671            objects[0] = reflowed.getLineDirections(i);
672
673            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;
678
679            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
688        updateBlocks(startline, endline - 1, n);
689
690        b.finish();
691        synchronized (sLock) {
692            sStaticLayout = reflowed;
693            sBuilder = b;
694        }
695    }
696
697    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        if (text instanceof PrecomputedText) {
708            PrecomputedText precomputed = (PrecomputedText) text;
709            precomputed.getBounds(start, end, mTempRect);
710        } else {
711            paint.getTextBounds(text, start, end, mTempRect);
712        }
713        final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
714        return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom;
715    }
716
717    /**
718     * Create the initial block structure, cutting the text into blocks of at least
719     * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
720     */
721    private void createBlocks() {
722        int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
723        mNumberOfBlocks = 0;
724        final CharSequence text = mDisplay;
725
726        while (true) {
727            offset = TextUtils.indexOf(text, '\n', offset);
728            if (offset < 0) {
729                addBlockAtOffset(text.length());
730                break;
731            } else {
732                addBlockAtOffset(offset);
733                offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
734            }
735        }
736
737        // mBlockIndices and mBlockEndLines should have the same length
738        mBlockIndices = new int[mBlockEndLines.length];
739        for (int i = 0; i < mBlockEndLines.length; i++) {
740            mBlockIndices[i] = INVALID_BLOCK_INDEX;
741        }
742    }
743
744    /**
745     * @hide
746     */
747    public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
748        return mBlocksAlwaysNeedToBeRedrawn;
749    }
750
751    private void updateAlwaysNeedsToBeRedrawn(int blockIndex) {
752        int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1);
753        int endLine = mBlockEndLines[blockIndex];
754        for (int i = startLine; i <= endLine; i++) {
755            if (getContentMayProtrudeFromTopOrBottom(i)) {
756                if (mBlocksAlwaysNeedToBeRedrawn == null) {
757                    mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>();
758                }
759                mBlocksAlwaysNeedToBeRedrawn.add(blockIndex);
760                return;
761            }
762        }
763        if (mBlocksAlwaysNeedToBeRedrawn != null) {
764            mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex);
765        }
766    }
767
768    /**
769     * Create a new block, ending at the specified character offset.
770     * A block will actually be created only if has at least one line, i.e. this offset is
771     * not on the end line of the previous block.
772     */
773    private void addBlockAtOffset(int offset) {
774        final int line = getLineForOffset(offset);
775        if (mBlockEndLines == null) {
776            // Initial creation of the array, no test on previous block ending line
777            mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
778            mBlockEndLines[mNumberOfBlocks] = line;
779            updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
780            mNumberOfBlocks++;
781            return;
782        }
783
784        final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
785        if (line > previousBlockEndLine) {
786            mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
787            updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
788            mNumberOfBlocks++;
789        }
790    }
791
792    /**
793     * This method is called every time the layout is reflowed after an edition.
794     * It updates the internal block data structure. The text is split in blocks
795     * of contiguous lines, with at least one block for the entire text.
796     * When a range of lines is edited, new blocks (from 0 to 3 depending on the
797     * overlap structure) will replace the set of overlapping blocks.
798     * Blocks are listed in order and are represented by their ending line number.
799     * An index is associated to each block (which will be used by display lists),
800     * this class simply invalidates the index of blocks overlapping a modification.
801     *
802     * @param startLine the first line of the range of modified lines
803     * @param endLine the last line of the range, possibly equal to startLine, lower
804     * than getLineCount()
805     * @param newLineCount the number of lines that will replace the range, possibly 0
806     *
807     * @hide
808     */
809    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
810    public void updateBlocks(int startLine, int endLine, int newLineCount) {
811        if (mBlockEndLines == null) {
812            createBlocks();
813            return;
814        }
815
816        /*final*/ int firstBlock = -1;
817        /*final*/ int lastBlock = -1;
818        for (int i = 0; i < mNumberOfBlocks; i++) {
819            if (mBlockEndLines[i] >= startLine) {
820                firstBlock = i;
821                break;
822            }
823        }
824        for (int i = firstBlock; i < mNumberOfBlocks; i++) {
825            if (mBlockEndLines[i] >= endLine) {
826                lastBlock = i;
827                break;
828            }
829        }
830        final int lastBlockEndLine = mBlockEndLines[lastBlock];
831
832        final boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
833                mBlockEndLines[firstBlock - 1] + 1);
834        final boolean createBlock = newLineCount > 0;
835        final boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
836
837        int numAddedBlocks = 0;
838        if (createBlockBefore) numAddedBlocks++;
839        if (createBlock) numAddedBlocks++;
840        if (createBlockAfter) numAddedBlocks++;
841
842        final int numRemovedBlocks = lastBlock - firstBlock + 1;
843        final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
844
845        if (newNumberOfBlocks == 0) {
846            // Even when text is empty, there is actually one line and hence one block
847            mBlockEndLines[0] = 0;
848            mBlockIndices[0] = INVALID_BLOCK_INDEX;
849            mNumberOfBlocks = 1;
850            return;
851        }
852
853        if (newNumberOfBlocks > mBlockEndLines.length) {
854            int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
855                    Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
856            int[] blockIndices = new int[blockEndLines.length];
857            System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
858            System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
859            System.arraycopy(mBlockEndLines, lastBlock + 1,
860                    blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
861            System.arraycopy(mBlockIndices, lastBlock + 1,
862                    blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
863            mBlockEndLines = blockEndLines;
864            mBlockIndices = blockIndices;
865        } else if (numAddedBlocks + numRemovedBlocks != 0) {
866            System.arraycopy(mBlockEndLines, lastBlock + 1,
867                    mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
868            System.arraycopy(mBlockIndices, lastBlock + 1,
869                    mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
870        }
871
872        if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
873            final ArraySet<Integer> set = new ArraySet<>();
874            final int changedBlockCount = numAddedBlocks - numRemovedBlocks;
875            for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
876                Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
877                if (block < firstBlock) {
878                    // block index is before firstBlock add it since it did not change
879                    set.add(block);
880                }
881                if (block > lastBlock) {
882                    // block index is after lastBlock, the index reduced to += changedBlockCount
883                    block += changedBlockCount;
884                    set.add(block);
885                }
886            }
887            mBlocksAlwaysNeedToBeRedrawn = set;
888        }
889
890        mNumberOfBlocks = newNumberOfBlocks;
891        int newFirstChangedBlock;
892        final int deltaLines = newLineCount - (endLine - startLine + 1);
893        if (deltaLines != 0) {
894            // Display list whose index is >= mIndexFirstChangedBlock is valid
895            // but it needs to update its drawing location.
896            newFirstChangedBlock = firstBlock + numAddedBlocks;
897            for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
898                mBlockEndLines[i] += deltaLines;
899            }
900        } else {
901            newFirstChangedBlock = mNumberOfBlocks;
902        }
903        mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
904
905        int blockIndex = firstBlock;
906        if (createBlockBefore) {
907            mBlockEndLines[blockIndex] = startLine - 1;
908            updateAlwaysNeedsToBeRedrawn(blockIndex);
909            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
910            blockIndex++;
911        }
912
913        if (createBlock) {
914            mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
915            updateAlwaysNeedsToBeRedrawn(blockIndex);
916            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
917            blockIndex++;
918        }
919
920        if (createBlockAfter) {
921            mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
922            updateAlwaysNeedsToBeRedrawn(blockIndex);
923            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
924        }
925    }
926
927    /**
928     * This method is used for test purposes only.
929     * @hide
930     */
931    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
932    public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks,
933            int totalLines) {
934        mBlockEndLines = new int[blockEndLines.length];
935        mBlockIndices = new int[blockIndices.length];
936        System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
937        System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
938        mNumberOfBlocks = numberOfBlocks;
939        while (mInts.size() < totalLines) {
940            mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]);
941        }
942    }
943
944    /**
945     * @hide
946     */
947    public int[] getBlockEndLines() {
948        return mBlockEndLines;
949    }
950
951    /**
952     * @hide
953     */
954    public int[] getBlockIndices() {
955        return mBlockIndices;
956    }
957
958    /**
959     * @hide
960     */
961    public int getBlockIndex(int index) {
962        return mBlockIndices[index];
963    }
964
965    /**
966     * @hide
967     * @param index
968     */
969    public void setBlockIndex(int index, int blockIndex) {
970        mBlockIndices[index] = blockIndex;
971    }
972
973    /**
974     * @hide
975     */
976    public int getNumberOfBlocks() {
977        return mNumberOfBlocks;
978    }
979
980    /**
981     * @hide
982     */
983    public int getIndexFirstChangedBlock() {
984        return mIndexFirstChangedBlock;
985    }
986
987    /**
988     * @hide
989     */
990    public void setIndexFirstChangedBlock(int i) {
991        mIndexFirstChangedBlock = i;
992    }
993
994    @Override
995    public int getLineCount() {
996        return mInts.size() - 1;
997    }
998
999    @Override
1000    public int getLineTop(int line) {
1001        return mInts.getValue(line, TOP);
1002    }
1003
1004    @Override
1005    public int getLineDescent(int line) {
1006        return mInts.getValue(line, DESCENT);
1007    }
1008
1009    /**
1010     * @hide
1011     */
1012    @Override
1013    public int getLineExtra(int line) {
1014        return mInts.getValue(line, EXTRA);
1015    }
1016
1017    @Override
1018    public int getLineStart(int line) {
1019        return mInts.getValue(line, START) & START_MASK;
1020    }
1021
1022    @Override
1023    public boolean getLineContainsTab(int line) {
1024        return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
1025    }
1026
1027    @Override
1028    public int getParagraphDirection(int line) {
1029        return mInts.getValue(line, DIR) >> DIR_SHIFT;
1030    }
1031
1032    @Override
1033    public final Directions getLineDirections(int line) {
1034        return mObjects.getValue(line, 0);
1035    }
1036
1037    @Override
1038    public int getTopPadding() {
1039        return mTopPadding;
1040    }
1041
1042    @Override
1043    public int getBottomPadding() {
1044        return mBottomPadding;
1045    }
1046
1047    /**
1048     * @hide
1049     */
1050    @Override
1051    public int getHyphen(int line) {
1052        return mInts.getValue(line, HYPHEN) & HYPHEN_MASK;
1053    }
1054
1055    private boolean getContentMayProtrudeFromTopOrBottom(int line) {
1056        return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
1057                & MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
1058    }
1059
1060    @Override
1061    public int getEllipsizedWidth() {
1062        return mEllipsizedWidth;
1063    }
1064
1065    private static class ChangeWatcher implements TextWatcher, SpanWatcher {
1066        public ChangeWatcher(DynamicLayout layout) {
1067            mLayout = new WeakReference<>(layout);
1068        }
1069
1070        private void reflow(CharSequence s, int where, int before, int after) {
1071            DynamicLayout ml = mLayout.get();
1072
1073            if (ml != null) {
1074                ml.reflow(s, where, before, after);
1075            } else if (s instanceof Spannable) {
1076                ((Spannable) s).removeSpan(this);
1077            }
1078        }
1079
1080        public void beforeTextChanged(CharSequence s, int where, int before, int after) {
1081            // Intentionally empty
1082        }
1083
1084        public void onTextChanged(CharSequence s, int where, int before, int after) {
1085            reflow(s, where, before, after);
1086        }
1087
1088        public void afterTextChanged(Editable s) {
1089            // Intentionally empty
1090        }
1091
1092        public void onSpanAdded(Spannable s, Object o, int start, int end) {
1093            if (o instanceof UpdateLayout)
1094                reflow(s, start, end - start, end - start);
1095        }
1096
1097        public void onSpanRemoved(Spannable s, Object o, int start, int end) {
1098            if (o instanceof UpdateLayout)
1099                reflow(s, start, end - start, end - start);
1100        }
1101
1102        public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
1103            if (o instanceof UpdateLayout) {
1104                if (start > end) {
1105                    // Bug: 67926915 start cannot be determined, fallback to reflow from start
1106                    // instead of causing an exception
1107                    start = 0;
1108                }
1109                reflow(s, start, end - start, end - start);
1110                reflow(s, nstart, nend - nstart, nend - nstart);
1111            }
1112        }
1113
1114        private WeakReference<DynamicLayout> mLayout;
1115    }
1116
1117    @Override
1118    public int getEllipsisStart(int line) {
1119        if (mEllipsizeAt == null) {
1120            return 0;
1121        }
1122
1123        return mInts.getValue(line, ELLIPSIS_START);
1124    }
1125
1126    @Override
1127    public int getEllipsisCount(int line) {
1128        if (mEllipsizeAt == null) {
1129            return 0;
1130        }
1131
1132        return mInts.getValue(line, ELLIPSIS_COUNT);
1133    }
1134
1135    private CharSequence mBase;
1136    private CharSequence mDisplay;
1137    private ChangeWatcher mWatcher;
1138    private boolean mIncludePad;
1139    private boolean mFallbackLineSpacing;
1140    private boolean mEllipsize;
1141    private int mEllipsizedWidth;
1142    private TextUtils.TruncateAt mEllipsizeAt;
1143    private int mBreakStrategy;
1144    private int mHyphenationFrequency;
1145    private int mJustificationMode;
1146
1147    private PackedIntVector mInts;
1148    private PackedObjectVector<Directions> mObjects;
1149
1150    /**
1151     * Value used in mBlockIndices when a block has been created or recycled and indicating that its
1152     * display list needs to be re-created.
1153     * @hide
1154     */
1155    public static final int INVALID_BLOCK_INDEX = -1;
1156    // Stores the line numbers of the last line of each block (inclusive)
1157    private int[] mBlockEndLines;
1158    // The indices of this block's display list in TextView's internal display list array or
1159    // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
1160    private int[] mBlockIndices;
1161    // Set of blocks that always need to be redrawn.
1162    private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
1163    // Number of items actually currently being used in the above 2 arrays
1164    private int mNumberOfBlocks;
1165    // The first index of the blocks whose locations are changed
1166    private int mIndexFirstChangedBlock;
1167
1168    private int mTopPadding, mBottomPadding;
1169
1170    private Rect mTempRect = new Rect();
1171
1172    private static StaticLayout sStaticLayout = null;
1173    private static StaticLayout.Builder sBuilder = null;
1174
1175    private static final Object[] sLock = new Object[0];
1176
1177    // START, DIR, and TAB share the same entry.
1178    private static final int START = 0;
1179    private static final int DIR = START;
1180    private static final int TAB = START;
1181    private static final int TOP = 1;
1182    private static final int DESCENT = 2;
1183    private static final int EXTRA = 3;
1184    // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
1185    private static final int HYPHEN = 4;
1186    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
1187    private static final int COLUMNS_NORMAL = 5;
1188
1189    private static final int ELLIPSIS_START = 5;
1190    private static final int ELLIPSIS_COUNT = 6;
1191    private static final int COLUMNS_ELLIPSIZE = 7;
1192
1193    private static final int START_MASK = 0x1FFFFFFF;
1194    private static final int DIR_SHIFT  = 30;
1195    private static final int TAB_MASK   = 0x20000000;
1196    private static final int HYPHEN_MASK = 0xFF;
1197    private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
1198
1199    private static final int ELLIPSIS_UNDEFINED = 0x80000000;
1200}
1201