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.graphics.Paint;
     20 import android.text.style.UpdateLayout;
     21 import android.text.style.WrapTogetherSpan;
     22 
     23 import java.lang.ref.WeakReference;
     24 
     25 /**
     26  * DynamicLayout is a text layout that updates itself as the text is edited.
     27  * <p>This is used by widgets to control text layout. You should not need
     28  * to use this class directly unless you are implementing your own widget
     29  * or custom display object, or need to call
     30  * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
     31  *  Canvas.drawText()} directly.</p>
     32  */
     33 public class DynamicLayout
     34 extends Layout
     35 {
     36     private static final int PRIORITY = 128;
     37 
     38     /**
     39      * Make a layout for the specified text that will be updated as
     40      * the text is changed.
     41      */
     42     public DynamicLayout(CharSequence base,
     43                          TextPaint paint,
     44                          int width, Alignment align,
     45                          float spacingmult, float spacingadd,
     46                          boolean includepad) {
     47         this(base, base, paint, width, align, spacingmult, spacingadd,
     48              includepad);
     49     }
     50 
     51     /**
     52      * Make a layout for the transformed text (password transformation
     53      * being the primary example of a transformation)
     54      * that will be updated as the base text is changed.
     55      */
     56     public DynamicLayout(CharSequence base, CharSequence display,
     57                          TextPaint paint,
     58                          int width, Alignment align,
     59                          float spacingmult, float spacingadd,
     60                          boolean includepad) {
     61         this(base, display, paint, width, align, spacingmult, spacingadd,
     62              includepad, null, 0);
     63     }
     64 
     65     /**
     66      * Make a layout for the transformed text (password transformation
     67      * being the primary example of a transformation)
     68      * that will be updated as the base text is changed.
     69      * If ellipsize is non-null, the Layout will ellipsize the text
     70      * down to ellipsizedWidth.
     71      */
     72     public DynamicLayout(CharSequence base, CharSequence display,
     73                          TextPaint paint,
     74                          int width, Alignment align,
     75                          float spacingmult, float spacingadd,
     76                          boolean includepad,
     77                          TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
     78         super((ellipsize == null)
     79                 ? display
     80                 : (display instanceof Spanned)
     81                     ? new SpannedEllipsizer(display)
     82                     : new Ellipsizer(display),
     83               paint, width, align, spacingmult, spacingadd);
     84 
     85         mBase = base;
     86         mDisplay = display;
     87 
     88         if (ellipsize != null) {
     89             mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
     90             mEllipsizedWidth = ellipsizedWidth;
     91             mEllipsizeAt = ellipsize;
     92         } else {
     93             mInts = new PackedIntVector(COLUMNS_NORMAL);
     94             mEllipsizedWidth = width;
     95             mEllipsizeAt = ellipsize;
     96         }
     97 
     98         mObjects = new PackedObjectVector<Directions>(1);
     99 
    100         mIncludePad = includepad;
    101 
    102         /*
    103          * This is annoying, but we can't refer to the layout until
    104          * superclass construction is finished, and the superclass
    105          * constructor wants the reference to the display text.
    106          *
    107          * This will break if the superclass constructor ever actually
    108          * cares about the content instead of just holding the reference.
    109          */
    110         if (ellipsize != null) {
    111             Ellipsizer e = (Ellipsizer) getText();
    112 
    113             e.mLayout = this;
    114             e.mWidth = ellipsizedWidth;
    115             e.mMethod = ellipsize;
    116             mEllipsize = true;
    117         }
    118 
    119         // Initial state is a single line with 0 characters (0 to 0),
    120         // with top at 0 and bottom at whatever is natural, and
    121         // undefined ellipsis.
    122 
    123         int[] start;
    124 
    125         if (ellipsize != null) {
    126             start = new int[COLUMNS_ELLIPSIZE];
    127             start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
    128         } else {
    129             start = new int[COLUMNS_NORMAL];
    130         }
    131 
    132         Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
    133 
    134         Paint.FontMetricsInt fm = paint.getFontMetricsInt();
    135         int asc = fm.ascent;
    136         int desc = fm.descent;
    137 
    138         start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
    139         start[TOP] = 0;
    140         start[DESCENT] = desc;
    141         mInts.insertAt(0, start);
    142 
    143         start[TOP] = desc - asc;
    144         mInts.insertAt(1, start);
    145 
    146         mObjects.insertAt(0, dirs);
    147 
    148         // Update from 0 characters to whatever the real text is
    149 
    150         reflow(base, 0, 0, base.length());
    151 
    152         if (base instanceof Spannable) {
    153             if (mWatcher == null)
    154                 mWatcher = new ChangeWatcher(this);
    155 
    156             // Strip out any watchers for other DynamicLayouts.
    157             Spannable sp = (Spannable) base;
    158             ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
    159             for (int i = 0; i < spans.length; i++)
    160                 sp.removeSpan(spans[i]);
    161 
    162             sp.setSpan(mWatcher, 0, base.length(),
    163                        Spannable.SPAN_INCLUSIVE_INCLUSIVE |
    164                        (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
    165         }
    166     }
    167 
    168     private void reflow(CharSequence s, int where, int before, int after) {
    169         if (s != mBase)
    170             return;
    171 
    172         CharSequence text = mDisplay;
    173         int len = text.length();
    174 
    175         // seek back to the start of the paragraph
    176 
    177         int find = TextUtils.lastIndexOf(text, '\n', where - 1);
    178         if (find < 0)
    179             find = 0;
    180         else
    181             find = find + 1;
    182 
    183         {
    184             int diff = where - find;
    185             before += diff;
    186             after += diff;
    187             where -= diff;
    188         }
    189 
    190         // seek forward to the end of the paragraph
    191 
    192         int look = TextUtils.indexOf(text, '\n', where + after);
    193         if (look < 0)
    194             look = len;
    195         else
    196             look++; // we want the index after the \n
    197 
    198         int change = look - (where + after);
    199         before += change;
    200         after += change;
    201 
    202         // seek further out to cover anything that is forced to wrap together
    203 
    204         if (text instanceof Spanned) {
    205             Spanned sp = (Spanned) text;
    206             boolean again;
    207 
    208             do {
    209                 again = false;
    210 
    211                 Object[] force = sp.getSpans(where, where + after,
    212                                              WrapTogetherSpan.class);
    213 
    214                 for (int i = 0; i < force.length; i++) {
    215                     int st = sp.getSpanStart(force[i]);
    216                     int en = sp.getSpanEnd(force[i]);
    217 
    218                     if (st < where) {
    219                         again = true;
    220 
    221                         int diff = where - st;
    222                         before += diff;
    223                         after += diff;
    224                         where -= diff;
    225                     }
    226 
    227                     if (en > where + after) {
    228                         again = true;
    229 
    230                         int diff = en - (where + after);
    231                         before += diff;
    232                         after += diff;
    233                     }
    234                 }
    235             } while (again);
    236         }
    237 
    238         // find affected region of old layout
    239 
    240         int startline = getLineForOffset(where);
    241         int startv = getLineTop(startline);
    242 
    243         int endline = getLineForOffset(where + before);
    244         if (where + after == len)
    245             endline = getLineCount();
    246         int endv = getLineTop(endline);
    247         boolean islast = (endline == getLineCount());
    248 
    249         // generate new layout for affected text
    250 
    251         StaticLayout reflowed;
    252 
    253         synchronized (sLock) {
    254             reflowed = sStaticLayout;
    255             sStaticLayout = null;
    256         }
    257 
    258         if (reflowed == null)
    259             reflowed = new StaticLayout(true);
    260 
    261         reflowed.generate(text, where, where + after,
    262                                       getPaint(), getWidth(), getAlignment(),
    263                                       getSpacingMultiplier(), getSpacingAdd(),
    264                                       false, true, mEllipsize,
    265                                       mEllipsizedWidth, mEllipsizeAt);
    266         int n = reflowed.getLineCount();
    267 
    268         // If the new layout has a blank line at the end, but it is not
    269         // the very end of the buffer, then we already have a line that
    270         // starts there, so disregard the blank line.
    271 
    272         if (where + after != len &&
    273             reflowed.getLineStart(n - 1) == where + after)
    274             n--;
    275 
    276         // remove affected lines from old layout
    277 
    278         mInts.deleteAt(startline, endline - startline);
    279         mObjects.deleteAt(startline, endline - startline);
    280 
    281         // adjust offsets in layout for new height and offsets
    282 
    283         int ht = reflowed.getLineTop(n);
    284         int toppad = 0, botpad = 0;
    285 
    286         if (mIncludePad && startline == 0) {
    287             toppad = reflowed.getTopPadding();
    288             mTopPadding = toppad;
    289             ht -= toppad;
    290         }
    291         if (mIncludePad && islast) {
    292             botpad = reflowed.getBottomPadding();
    293             mBottomPadding = botpad;
    294             ht += botpad;
    295         }
    296 
    297         mInts.adjustValuesBelow(startline, START, after - before);
    298         mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
    299 
    300         // insert new layout
    301 
    302         int[] ints;
    303 
    304         if (mEllipsize) {
    305             ints = new int[COLUMNS_ELLIPSIZE];
    306             ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
    307         } else {
    308             ints = new int[COLUMNS_NORMAL];
    309         }
    310 
    311         Directions[] objects = new Directions[1];
    312 
    313 
    314         for (int i = 0; i < n; i++) {
    315             ints[START] = reflowed.getLineStart(i) |
    316                           (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
    317                           (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
    318 
    319             int top = reflowed.getLineTop(i) + startv;
    320             if (i > 0)
    321                 top -= toppad;
    322             ints[TOP] = top;
    323 
    324             int desc = reflowed.getLineDescent(i);
    325             if (i == n - 1)
    326                 desc += botpad;
    327 
    328             ints[DESCENT] = desc;
    329             objects[0] = reflowed.getLineDirections(i);
    330 
    331             if (mEllipsize) {
    332                 ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
    333                 ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
    334             }
    335 
    336             mInts.insertAt(startline + i, ints);
    337             mObjects.insertAt(startline + i, objects);
    338         }
    339 
    340         synchronized (sLock) {
    341             sStaticLayout = reflowed;
    342         }
    343     }
    344 
    345     private void dump(boolean show) {
    346         int n = getLineCount();
    347 
    348         for (int i = 0; i < n; i++) {
    349             System.out.print("line " + i + ": " + getLineStart(i) + " to " + getLineEnd(i) + " ");
    350 
    351             if (show) {
    352                 System.out.print(getText().subSequence(getLineStart(i),
    353                                                        getLineEnd(i)));
    354             }
    355 
    356             System.out.println("");
    357         }
    358 
    359         System.out.println("");
    360     }
    361 
    362     public int getLineCount() {
    363         return mInts.size() - 1;
    364     }
    365 
    366     public int getLineTop(int line) {
    367         return mInts.getValue(line, TOP);
    368     }
    369 
    370     public int getLineDescent(int line) {
    371         return mInts.getValue(line, DESCENT);
    372     }
    373 
    374     public int getLineStart(int line) {
    375         return mInts.getValue(line, START) & START_MASK;
    376     }
    377 
    378     public boolean getLineContainsTab(int line) {
    379         return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
    380     }
    381 
    382     public int getParagraphDirection(int line) {
    383         return mInts.getValue(line, DIR) >> DIR_SHIFT;
    384     }
    385 
    386     public final Directions getLineDirections(int line) {
    387         return mObjects.getValue(line, 0);
    388     }
    389 
    390     public int getTopPadding() {
    391         return mTopPadding;
    392     }
    393 
    394     public int getBottomPadding() {
    395         return mBottomPadding;
    396     }
    397 
    398     @Override
    399     public int getEllipsizedWidth() {
    400         return mEllipsizedWidth;
    401     }
    402 
    403     private static class ChangeWatcher
    404     implements TextWatcher, SpanWatcher
    405     {
    406         public ChangeWatcher(DynamicLayout layout) {
    407             mLayout = new WeakReference(layout);
    408         }
    409 
    410         private void reflow(CharSequence s, int where, int before, int after) {
    411             DynamicLayout ml = (DynamicLayout) mLayout.get();
    412 
    413             if (ml != null)
    414                 ml.reflow(s, where, before, after);
    415             else if (s instanceof Spannable)
    416                 ((Spannable) s).removeSpan(this);
    417         }
    418 
    419         public void beforeTextChanged(CharSequence s,
    420                                       int where, int before, int after) {
    421             ;
    422         }
    423 
    424         public void onTextChanged(CharSequence s,
    425                                   int where, int before, int after) {
    426             reflow(s, where, before, after);
    427         }
    428 
    429         public void afterTextChanged(Editable s) {
    430             ;
    431         }
    432 
    433         public void onSpanAdded(Spannable s, Object o, int start, int end) {
    434             if (o instanceof UpdateLayout)
    435                 reflow(s, start, end - start, end - start);
    436         }
    437 
    438         public void onSpanRemoved(Spannable s, Object o, int start, int end) {
    439             if (o instanceof UpdateLayout)
    440                 reflow(s, start, end - start, end - start);
    441         }
    442 
    443         public void onSpanChanged(Spannable s, Object o, int start, int end,
    444                                   int nstart, int nend) {
    445             if (o instanceof UpdateLayout) {
    446                 reflow(s, start, end - start, end - start);
    447                 reflow(s, nstart, nend - nstart, nend - nstart);
    448             }
    449         }
    450 
    451         private WeakReference mLayout;
    452     }
    453 
    454     public int getEllipsisStart(int line) {
    455         if (mEllipsizeAt == null) {
    456             return 0;
    457         }
    458 
    459         return mInts.getValue(line, ELLIPSIS_START);
    460     }
    461 
    462     public int getEllipsisCount(int line) {
    463         if (mEllipsizeAt == null) {
    464             return 0;
    465         }
    466 
    467         return mInts.getValue(line, ELLIPSIS_COUNT);
    468     }
    469 
    470     private CharSequence mBase;
    471     private CharSequence mDisplay;
    472     private ChangeWatcher mWatcher;
    473     private boolean mIncludePad;
    474     private boolean mEllipsize;
    475     private int mEllipsizedWidth;
    476     private TextUtils.TruncateAt mEllipsizeAt;
    477 
    478     private PackedIntVector mInts;
    479     private PackedObjectVector<Directions> mObjects;
    480 
    481     private int mTopPadding, mBottomPadding;
    482 
    483     private static StaticLayout sStaticLayout = new StaticLayout(true);
    484     private static Object sLock = new Object();
    485 
    486     private static final int START = 0;
    487     private static final int DIR = START;
    488     private static final int TAB = START;
    489     private static final int TOP = 1;
    490     private static final int DESCENT = 2;
    491     private static final int COLUMNS_NORMAL = 3;
    492 
    493     private static final int ELLIPSIS_START = 3;
    494     private static final int ELLIPSIS_COUNT = 4;
    495     private static final int COLUMNS_ELLIPSIZE = 5;
    496 
    497     private static final int START_MASK = 0x1FFFFFFF;
    498     private static final int DIR_MASK   = 0xC0000000;
    499     private static final int DIR_SHIFT  = 30;
    500     private static final int TAB_MASK   = 0x20000000;
    501 
    502     private static final int ELLIPSIS_UNDEFINED = 0x80000000;
    503 }
    504