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