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.graphics.Paint;
     24 import android.graphics.Rect;
     25 import android.text.style.ReplacementSpan;
     26 import android.text.style.UpdateLayout;
     27 import android.text.style.WrapTogetherSpan;
     28 import android.util.ArraySet;
     29 import android.util.Pools.SynchronizedPool;
     30 
     31 import com.android.internal.annotations.VisibleForTesting;
     32 import com.android.internal.util.ArrayUtils;
     33 import com.android.internal.util.GrowingArrayUtils;
     34 
     35 import 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  */
     45 public 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