• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Bitmap;
20 import android.graphics.Paint;
21 import android.text.style.LeadingMarginSpan;
22 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
23 import android.text.style.LineHeightSpan;
24 import android.text.style.MetricAffectingSpan;
25 import android.text.style.TabStopSpan;
26 import android.util.Log;
27 
28 import com.android.internal.util.ArrayUtils;
29 
30 /**
31  * StaticLayout is a Layout for text that will not be edited after it
32  * is laid out.  Use {@link DynamicLayout} for text that may change.
33  * <p>This is used by widgets to control text layout. You should not need
34  * to use this class directly unless you are implementing your own widget
35  * or custom display object, or would be tempted to call
36  * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
37  * float, float, android.graphics.Paint)
38  * Canvas.drawText()} directly.</p>
39  */
40 public class StaticLayout extends Layout {
41 
42     static final String TAG = "StaticLayout";
43 
StaticLayout(CharSequence source, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)44     public StaticLayout(CharSequence source, TextPaint paint,
45                         int width,
46                         Alignment align, float spacingmult, float spacingadd,
47                         boolean includepad) {
48         this(source, 0, source.length(), paint, width, align,
49              spacingmult, spacingadd, includepad);
50     }
51 
52     /**
53      * @hide
54      */
StaticLayout(CharSequence source, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad)55     public StaticLayout(CharSequence source, TextPaint paint,
56             int width, Alignment align, TextDirectionHeuristic textDir,
57             float spacingmult, float spacingadd,
58             boolean includepad) {
59         this(source, 0, source.length(), paint, width, align, textDir,
60                 spacingmult, spacingadd, includepad);
61     }
62 
StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad)63     public StaticLayout(CharSequence source, int bufstart, int bufend,
64                         TextPaint paint, int outerwidth,
65                         Alignment align,
66                         float spacingmult, float spacingadd,
67                         boolean includepad) {
68         this(source, bufstart, bufend, paint, outerwidth, align,
69              spacingmult, spacingadd, includepad, null, 0);
70     }
71 
72     /**
73      * @hide
74      */
StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad)75     public StaticLayout(CharSequence source, int bufstart, int bufend,
76             TextPaint paint, int outerwidth,
77             Alignment align, TextDirectionHeuristic textDir,
78             float spacingmult, float spacingadd,
79             boolean includepad) {
80         this(source, bufstart, bufend, paint, outerwidth, align, textDir,
81                 spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE);
82 }
83 
StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)84     public StaticLayout(CharSequence source, int bufstart, int bufend,
85             TextPaint paint, int outerwidth,
86             Alignment align,
87             float spacingmult, float spacingadd,
88             boolean includepad,
89             TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
90         this(source, bufstart, bufend, paint, outerwidth, align,
91                 TextDirectionHeuristics.FIRSTSTRONG_LTR,
92                 spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
93     }
94 
95     /**
96      * @hide
97      */
StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines)98     public StaticLayout(CharSequence source, int bufstart, int bufend,
99                         TextPaint paint, int outerwidth,
100                         Alignment align, TextDirectionHeuristic textDir,
101                         float spacingmult, float spacingadd,
102                         boolean includepad,
103                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
104         super((ellipsize == null)
105                 ? source
106                 : (source instanceof Spanned)
107                     ? new SpannedEllipsizer(source)
108                     : new Ellipsizer(source),
109               paint, outerwidth, align, textDir, spacingmult, spacingadd);
110 
111         /*
112          * This is annoying, but we can't refer to the layout until
113          * superclass construction is finished, and the superclass
114          * constructor wants the reference to the display text.
115          *
116          * This will break if the superclass constructor ever actually
117          * cares about the content instead of just holding the reference.
118          */
119         if (ellipsize != null) {
120             Ellipsizer e = (Ellipsizer) getText();
121 
122             e.mLayout = this;
123             e.mWidth = ellipsizedWidth;
124             e.mMethod = ellipsize;
125             mEllipsizedWidth = ellipsizedWidth;
126 
127             mColumns = COLUMNS_ELLIPSIZE;
128         } else {
129             mColumns = COLUMNS_NORMAL;
130             mEllipsizedWidth = outerwidth;
131         }
132 
133         mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
134         mLineDirections = new Directions[
135                              ArrayUtils.idealIntArraySize(2 * mColumns)];
136         mMaximumVisibleLineCount = maxLines;
137 
138         mMeasured = MeasuredText.obtain();
139 
140         generate(source, bufstart, bufend, paint, outerwidth, textDir, spacingmult,
141                  spacingadd, includepad, includepad, ellipsizedWidth,
142                  ellipsize);
143 
144         mMeasured = MeasuredText.recycle(mMeasured);
145         mFontMetricsInt = null;
146     }
147 
StaticLayout(CharSequence text)148     /* package */ StaticLayout(CharSequence text) {
149         super(text, null, 0, null, 0, 0);
150 
151         mColumns = COLUMNS_ELLIPSIZE;
152         mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
153         mLineDirections = new Directions[
154                              ArrayUtils.idealIntArraySize(2 * mColumns)];
155         mMeasured = MeasuredText.obtain();
156     }
157 
generate(CharSequence source, int bufStart, int bufEnd, TextPaint paint, int outerWidth, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad, boolean trackpad, float ellipsizedWidth, TextUtils.TruncateAt ellipsize)158     /* package */ void generate(CharSequence source, int bufStart, int bufEnd,
159                         TextPaint paint, int outerWidth,
160                         TextDirectionHeuristic textDir, float spacingmult,
161                         float spacingadd, boolean includepad,
162                         boolean trackpad, float ellipsizedWidth,
163                         TextUtils.TruncateAt ellipsize) {
164         mLineCount = 0;
165 
166         int v = 0;
167         boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
168 
169         Paint.FontMetricsInt fm = mFontMetricsInt;
170         int[] chooseHtv = null;
171 
172         MeasuredText measured = mMeasured;
173 
174         Spanned spanned = null;
175         if (source instanceof Spanned)
176             spanned = (Spanned) source;
177 
178         int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX
179 
180         int paraEnd;
181         for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
182             paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
183             if (paraEnd < 0)
184                 paraEnd = bufEnd;
185             else
186                 paraEnd++;
187 
188             int firstWidthLineLimit = mLineCount + 1;
189             int firstWidth = outerWidth;
190             int restWidth = outerWidth;
191 
192             LineHeightSpan[] chooseHt = null;
193 
194             if (spanned != null) {
195                 LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
196                         LeadingMarginSpan.class);
197                 for (int i = 0; i < sp.length; i++) {
198                     LeadingMarginSpan lms = sp[i];
199                     firstWidth -= sp[i].getLeadingMargin(true);
200                     restWidth -= sp[i].getLeadingMargin(false);
201 
202                     // LeadingMarginSpan2 is odd.  The count affects all
203                     // leading margin spans, not just this particular one,
204                     // and start from the top of the span, not the top of the
205                     // paragraph.
206                     if (lms instanceof LeadingMarginSpan2) {
207                         LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
208                         int lmsFirstLine = getLineForOffset(spanned.getSpanStart(lms2));
209                         firstWidthLineLimit = lmsFirstLine + lms2.getLeadingMarginLineCount();
210                     }
211                 }
212 
213                 chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
214 
215                 if (chooseHt.length != 0) {
216                     if (chooseHtv == null ||
217                         chooseHtv.length < chooseHt.length) {
218                         chooseHtv = new int[ArrayUtils.idealIntArraySize(
219                                             chooseHt.length)];
220                     }
221 
222                     for (int i = 0; i < chooseHt.length; i++) {
223                         int o = spanned.getSpanStart(chooseHt[i]);
224 
225                         if (o < paraStart) {
226                             // starts in this layout, before the
227                             // current paragraph
228 
229                             chooseHtv[i] = getLineTop(getLineForOffset(o));
230                         } else {
231                             // starts in this paragraph
232 
233                             chooseHtv[i] = v;
234                         }
235                     }
236                 }
237             }
238 
239             measured.setPara(source, paraStart, paraEnd, textDir);
240             char[] chs = measured.mChars;
241             float[] widths = measured.mWidths;
242             byte[] chdirs = measured.mLevels;
243             int dir = measured.mDir;
244             boolean easy = measured.mEasy;
245 
246             int width = firstWidth;
247 
248             float w = 0;
249             // here is the offset of the starting character of the line we are currently measuring
250             int here = paraStart;
251 
252             // ok is a character offset located after a word separator (space, tab, number...) where
253             // we would prefer to cut the current line. Equals to here when no such break was found.
254             int ok = paraStart;
255             float okWidth = w;
256             int okAscent = 0, okDescent = 0, okTop = 0, okBottom = 0;
257 
258             // fit is a character offset such that the [here, fit[ range fits in the allowed width.
259             // We will cut the line there if no ok position is found.
260             int fit = paraStart;
261             float fitWidth = w;
262             int fitAscent = 0, fitDescent = 0, fitTop = 0, fitBottom = 0;
263 
264             boolean hasTabOrEmoji = false;
265             boolean hasTab = false;
266             TabStops tabStops = null;
267 
268             for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
269 
270                 if (spanned == null) {
271                     spanEnd = paraEnd;
272                     int spanLen = spanEnd - spanStart;
273                     measured.addStyleRun(paint, spanLen, fm);
274                 } else {
275                     spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
276                             MetricAffectingSpan.class);
277                     int spanLen = spanEnd - spanStart;
278                     MetricAffectingSpan[] spans =
279                             spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
280                     spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
281                     measured.addStyleRun(paint, spans, spanLen, fm);
282                 }
283 
284                 int fmTop = fm.top;
285                 int fmBottom = fm.bottom;
286                 int fmAscent = fm.ascent;
287                 int fmDescent = fm.descent;
288 
289                 for (int j = spanStart; j < spanEnd; j++) {
290                     char c = chs[j - paraStart];
291 
292                     if (c == CHAR_NEW_LINE) {
293                         // intentionally left empty
294                     } else if (c == CHAR_TAB) {
295                         if (hasTab == false) {
296                             hasTab = true;
297                             hasTabOrEmoji = true;
298                             if (spanned != null) {
299                                 // First tab this para, check for tabstops
300                                 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
301                                         paraEnd, TabStopSpan.class);
302                                 if (spans.length > 0) {
303                                     tabStops = new TabStops(TAB_INCREMENT, spans);
304                                 }
305                             }
306                         }
307                         if (tabStops != null) {
308                             w = tabStops.nextTab(w);
309                         } else {
310                             w = TabStops.nextDefaultStop(w, TAB_INCREMENT);
311                         }
312                     } else if (c >= CHAR_FIRST_HIGH_SURROGATE && c <= CHAR_LAST_LOW_SURROGATE
313                             && j + 1 < spanEnd) {
314                         int emoji = Character.codePointAt(chs, j - paraStart);
315 
316                         if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
317                             Bitmap bm = EMOJI_FACTORY.getBitmapFromAndroidPua(emoji);
318 
319                             if (bm != null) {
320                                 Paint whichPaint;
321 
322                                 if (spanned == null) {
323                                     whichPaint = paint;
324                                 } else {
325                                     whichPaint = mWorkPaint;
326                                 }
327 
328                                 float wid = bm.getWidth() * -whichPaint.ascent() / bm.getHeight();
329 
330                                 w += wid;
331                                 hasTabOrEmoji = true;
332                                 j++;
333                             } else {
334                                 w += widths[j - paraStart];
335                             }
336                         } else {
337                             w += widths[j - paraStart];
338                         }
339                     } else {
340                         w += widths[j - paraStart];
341                     }
342 
343                     if (w <= width) {
344                         fitWidth = w;
345                         fit = j + 1;
346 
347                         if (fmTop < fitTop)
348                             fitTop = fmTop;
349                         if (fmAscent < fitAscent)
350                             fitAscent = fmAscent;
351                         if (fmDescent > fitDescent)
352                             fitDescent = fmDescent;
353                         if (fmBottom > fitBottom)
354                             fitBottom = fmBottom;
355 
356                         /*
357                          * From the Unicode Line Breaking Algorithm:
358                          * (at least approximately)
359                          *
360                          * .,:; are class IS: breakpoints
361                          *      except when adjacent to digits
362                          * /    is class SY: a breakpoint
363                          *      except when followed by a digit.
364                          * -    is class HY: a breakpoint
365                          *      except when followed by a digit.
366                          *
367                          * Ideographs are class ID: breakpoints when adjacent,
368                          * except for NS (non-starters), which can be broken
369                          * after but not before.
370                          */
371                         if (c == CHAR_SPACE || c == CHAR_TAB ||
372                             ((c == CHAR_DOT || c == CHAR_COMMA ||
373                                     c == CHAR_COLON || c == CHAR_SEMICOLON) &&
374                              (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) &&
375                              (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
376                             ((c == CHAR_SLASH || c == CHAR_HYPHEN) &&
377                              (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
378                             (c >= CHAR_FIRST_CJK && isIdeographic(c, true) &&
379                              j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) {
380                             okWidth = w;
381                             ok = j + 1;
382 
383                             if (fitTop < okTop)
384                                 okTop = fitTop;
385                             if (fitAscent < okAscent)
386                                 okAscent = fitAscent;
387                             if (fitDescent > okDescent)
388                                 okDescent = fitDescent;
389                             if (fitBottom > okBottom)
390                                 okBottom = fitBottom;
391                         }
392                     } else {
393                         final boolean moreChars = (j + 1 < spanEnd);
394                         int endPos;
395                         int above, below, top, bottom;
396                         float currentTextWidth;
397 
398                         if (ok != here) {
399                             // If it is a space that makes the length exceed width, cut here
400                             if (c == CHAR_SPACE) ok = j + 1;
401 
402                             while (ok < spanEnd && chs[ok - paraStart] == CHAR_SPACE) {
403                                 ok++;
404                             }
405 
406                             endPos = ok;
407                             above = okAscent;
408                             below = okDescent;
409                             top = okTop;
410                             bottom = okBottom;
411                             currentTextWidth = okWidth;
412                         } else if (fit != here) {
413                             endPos = fit;
414                             above = fitAscent;
415                             below = fitDescent;
416                             top = fitTop;
417                             bottom = fitBottom;
418                             currentTextWidth = fitWidth;
419                         } else {
420                             endPos = here + 1;
421                             above = fm.ascent;
422                             below = fm.descent;
423                             top = fm.top;
424                             bottom = fm.bottom;
425                             currentTextWidth = widths[here - paraStart];
426                         }
427 
428                         v = out(source, here, endPos,
429                                 above, below, top, bottom,
430                                 v, spacingmult, spacingadd, chooseHt,chooseHtv, fm, hasTabOrEmoji,
431                                 needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
432                                 chs, widths, paraStart, ellipsize, ellipsizedWidth,
433                                 currentTextWidth, paint, moreChars);
434 
435                         here = endPos;
436                         j = here - 1; // restart j-span loop from here, compensating for the j++
437                         ok = fit = here;
438                         w = 0;
439                         fitAscent = fitDescent = fitTop = fitBottom = 0;
440                         okAscent = okDescent = okTop = okBottom = 0;
441 
442                         if (--firstWidthLineLimit <= 0) {
443                             width = restWidth;
444                         }
445 
446                         if (here < spanStart) {
447                             // The text was cut before the beginning of the current span range.
448                             // Exit the span loop, and get spanStart to start over from here.
449                             measured.setPos(here);
450                             spanEnd = here;
451                             break;
452                         }
453                     }
454                     // FIXME This should be moved in the above else block which changes mLineCount
455                     if (mLineCount >= mMaximumVisibleLineCount) {
456                         break;
457                     }
458                 }
459             }
460 
461             if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) {
462                 if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) {
463                     paint.getFontMetricsInt(fm);
464 
465                     fitTop = fm.top;
466                     fitBottom = fm.bottom;
467                     fitAscent = fm.ascent;
468                     fitDescent = fm.descent;
469                 }
470 
471                 // Log.e("text", "output rest " + here + " to " + end);
472 
473                 v = out(source,
474                         here, paraEnd, fitAscent, fitDescent,
475                         fitTop, fitBottom,
476                         v,
477                         spacingmult, spacingadd, chooseHt,
478                         chooseHtv, fm, hasTabOrEmoji,
479                         needMultiply, chdirs, dir, easy, bufEnd,
480                         includepad, trackpad, chs,
481                         widths, paraStart, ellipsize,
482                         ellipsizedWidth, w, paint, paraEnd != bufEnd);
483             }
484 
485             paraStart = paraEnd;
486 
487             if (paraEnd == bufEnd)
488                 break;
489         }
490 
491         if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
492                 mLineCount < mMaximumVisibleLineCount) {
493             // Log.e("text", "output last " + bufEnd);
494 
495             paint.getFontMetricsInt(fm);
496 
497             v = out(source,
498                     bufEnd, bufEnd, fm.ascent, fm.descent,
499                     fm.top, fm.bottom,
500                     v,
501                     spacingmult, spacingadd, null,
502                     null, fm, false,
503                     needMultiply, null, DEFAULT_DIR, true, bufEnd,
504                     includepad, trackpad, null,
505                     null, bufStart, ellipsize,
506                     ellipsizedWidth, 0, paint, false);
507         }
508     }
509 
510     /**
511      * Returns true if the specified character is one of those specified
512      * as being Ideographic (class ID) by the Unicode Line Breaking Algorithm
513      * (http://www.unicode.org/unicode/reports/tr14/), and is therefore OK
514      * to break between a pair of.
515      *
516      * @param includeNonStarters also return true for category NS
517      *                           (non-starters), which can be broken
518      *                           after but not before.
519      */
isIdeographic(char c, boolean includeNonStarters)520     private static final boolean isIdeographic(char c, boolean includeNonStarters) {
521         if (c >= '\u2E80' && c <= '\u2FFF') {
522             return true; // CJK, KANGXI RADICALS, DESCRIPTION SYMBOLS
523         }
524         if (c == '\u3000') {
525             return true; // IDEOGRAPHIC SPACE
526         }
527         if (c >= '\u3040' && c <= '\u309F') {
528             if (!includeNonStarters) {
529                 switch (c) {
530                 case '\u3041': //  # HIRAGANA LETTER SMALL A
531                 case '\u3043': //  # HIRAGANA LETTER SMALL I
532                 case '\u3045': //  # HIRAGANA LETTER SMALL U
533                 case '\u3047': //  # HIRAGANA LETTER SMALL E
534                 case '\u3049': //  # HIRAGANA LETTER SMALL O
535                 case '\u3063': //  # HIRAGANA LETTER SMALL TU
536                 case '\u3083': //  # HIRAGANA LETTER SMALL YA
537                 case '\u3085': //  # HIRAGANA LETTER SMALL YU
538                 case '\u3087': //  # HIRAGANA LETTER SMALL YO
539                 case '\u308E': //  # HIRAGANA LETTER SMALL WA
540                 case '\u3095': //  # HIRAGANA LETTER SMALL KA
541                 case '\u3096': //  # HIRAGANA LETTER SMALL KE
542                 case '\u309B': //  # KATAKANA-HIRAGANA VOICED SOUND MARK
543                 case '\u309C': //  # KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
544                 case '\u309D': //  # HIRAGANA ITERATION MARK
545                 case '\u309E': //  # HIRAGANA VOICED ITERATION MARK
546                     return false;
547                 }
548             }
549             return true; // Hiragana (except small characters)
550         }
551         if (c >= '\u30A0' && c <= '\u30FF') {
552             if (!includeNonStarters) {
553                 switch (c) {
554                 case '\u30A0': //  # KATAKANA-HIRAGANA DOUBLE HYPHEN
555                 case '\u30A1': //  # KATAKANA LETTER SMALL A
556                 case '\u30A3': //  # KATAKANA LETTER SMALL I
557                 case '\u30A5': //  # KATAKANA LETTER SMALL U
558                 case '\u30A7': //  # KATAKANA LETTER SMALL E
559                 case '\u30A9': //  # KATAKANA LETTER SMALL O
560                 case '\u30C3': //  # KATAKANA LETTER SMALL TU
561                 case '\u30E3': //  # KATAKANA LETTER SMALL YA
562                 case '\u30E5': //  # KATAKANA LETTER SMALL YU
563                 case '\u30E7': //  # KATAKANA LETTER SMALL YO
564                 case '\u30EE': //  # KATAKANA LETTER SMALL WA
565                 case '\u30F5': //  # KATAKANA LETTER SMALL KA
566                 case '\u30F6': //  # KATAKANA LETTER SMALL KE
567                 case '\u30FB': //  # KATAKANA MIDDLE DOT
568                 case '\u30FC': //  # KATAKANA-HIRAGANA PROLONGED SOUND MARK
569                 case '\u30FD': //  # KATAKANA ITERATION MARK
570                 case '\u30FE': //  # KATAKANA VOICED ITERATION MARK
571                     return false;
572                 }
573             }
574             return true; // Katakana (except small characters)
575         }
576         if (c >= '\u3400' && c <= '\u4DB5') {
577             return true; // CJK UNIFIED IDEOGRAPHS EXTENSION A
578         }
579         if (c >= '\u4E00' && c <= '\u9FBB') {
580             return true; // CJK UNIFIED IDEOGRAPHS
581         }
582         if (c >= '\uF900' && c <= '\uFAD9') {
583             return true; // CJK COMPATIBILITY IDEOGRAPHS
584         }
585         if (c >= '\uA000' && c <= '\uA48F') {
586             return true; // YI SYLLABLES
587         }
588         if (c >= '\uA490' && c <= '\uA4CF') {
589             return true; // YI RADICALS
590         }
591         if (c >= '\uFE62' && c <= '\uFE66') {
592             return true; // SMALL PLUS SIGN to SMALL EQUALS SIGN
593         }
594         if (c >= '\uFF10' && c <= '\uFF19') {
595             return true; // WIDE DIGITS
596         }
597 
598         return false;
599     }
600 
out(CharSequence text, int start, int end, int above, int below, int top, int bottom, int v, float spacingmult, float spacingadd, LineHeightSpan[] chooseHt, int[] chooseHtv, Paint.FontMetricsInt fm, boolean hasTabOrEmoji, boolean needMultiply, byte[] chdirs, int dir, boolean easy, int bufEnd, boolean includePad, boolean trackPad, char[] chs, float[] widths, int widthStart, TextUtils.TruncateAt ellipsize, float ellipsisWidth, float textWidth, TextPaint paint, boolean moreChars)601     private int out(CharSequence text, int start, int end,
602                       int above, int below, int top, int bottom, int v,
603                       float spacingmult, float spacingadd,
604                       LineHeightSpan[] chooseHt, int[] chooseHtv,
605                       Paint.FontMetricsInt fm, boolean hasTabOrEmoji,
606                       boolean needMultiply, byte[] chdirs, int dir,
607                       boolean easy, int bufEnd, boolean includePad,
608                       boolean trackPad, char[] chs,
609                       float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
610                       float ellipsisWidth, float textWidth,
611                       TextPaint paint, boolean moreChars) {
612         int j = mLineCount;
613         int off = j * mColumns;
614         int want = off + mColumns + TOP;
615         int[] lines = mLines;
616 
617         if (want >= lines.length) {
618             int nlen = ArrayUtils.idealIntArraySize(want + 1);
619             int[] grow = new int[nlen];
620             System.arraycopy(lines, 0, grow, 0, lines.length);
621             mLines = grow;
622             lines = grow;
623 
624             Directions[] grow2 = new Directions[nlen];
625             System.arraycopy(mLineDirections, 0, grow2, 0,
626                              mLineDirections.length);
627             mLineDirections = grow2;
628         }
629 
630         if (chooseHt != null) {
631             fm.ascent = above;
632             fm.descent = below;
633             fm.top = top;
634             fm.bottom = bottom;
635 
636             for (int i = 0; i < chooseHt.length; i++) {
637                 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
638                     ((LineHeightSpan.WithDensity) chooseHt[i]).
639                         chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
640 
641                 } else {
642                     chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
643                 }
644             }
645 
646             above = fm.ascent;
647             below = fm.descent;
648             top = fm.top;
649             bottom = fm.bottom;
650         }
651 
652         if (j == 0) {
653             if (trackPad) {
654                 mTopPadding = top - above;
655             }
656 
657             if (includePad) {
658                 above = top;
659             }
660         }
661         if (end == bufEnd) {
662             if (trackPad) {
663                 mBottomPadding = bottom - below;
664             }
665 
666             if (includePad) {
667                 below = bottom;
668             }
669         }
670 
671         int extra;
672 
673         if (needMultiply) {
674             double ex = (below - above) * (spacingmult - 1) + spacingadd;
675             if (ex >= 0) {
676                 extra = (int)(ex + EXTRA_ROUNDING);
677             } else {
678                 extra = -(int)(-ex + EXTRA_ROUNDING);
679             }
680         } else {
681             extra = 0;
682         }
683 
684         lines[off + START] = start;
685         lines[off + TOP] = v;
686         lines[off + DESCENT] = below + extra;
687 
688         v += (below - above) + extra;
689         lines[off + mColumns + START] = end;
690         lines[off + mColumns + TOP] = v;
691 
692         if (hasTabOrEmoji)
693             lines[off + TAB] |= TAB_MASK;
694 
695         lines[off + DIR] |= dir << DIR_SHIFT;
696         Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
697         // easy means all chars < the first RTL, so no emoji, no nothing
698         // XXX a run with no text or all spaces is easy but might be an empty
699         // RTL paragraph.  Make sure easy is false if this is the case.
700         if (easy) {
701             mLineDirections[j] = linedirs;
702         } else {
703             mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
704                     start - widthStart, end - start);
705         }
706 
707         if (ellipsize != null) {
708             // If there is only one line, then do any type of ellipsis except when it is MARQUEE
709             // if there are multiple lines, just allow END ellipsis on the last line
710             boolean firstLine = (j == 0);
711             boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
712             boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
713 
714             boolean doEllipsis =
715                         (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
716                                 ellipsize != TextUtils.TruncateAt.MARQUEE) ||
717                         (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
718                                 ellipsize == TextUtils.TruncateAt.END);
719             if (doEllipsis) {
720                 calculateEllipsis(start, end, widths, widthStart,
721                         ellipsisWidth, ellipsize, j,
722                         textWidth, paint, forceEllipsis);
723             }
724         }
725 
726         mLineCount++;
727         return v;
728     }
729 
calculateEllipsis(int lineStart, int lineEnd, float[] widths, int widthStart, float avail, TextUtils.TruncateAt where, int line, float textWidth, TextPaint paint, boolean forceEllipsis)730     private void calculateEllipsis(int lineStart, int lineEnd,
731                                    float[] widths, int widthStart,
732                                    float avail, TextUtils.TruncateAt where,
733                                    int line, float textWidth, TextPaint paint,
734                                    boolean forceEllipsis) {
735         if (textWidth <= avail && !forceEllipsis) {
736             // Everything fits!
737             mLines[mColumns * line + ELLIPSIS_START] = 0;
738             mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
739             return;
740         }
741 
742         float ellipsisWidth = paint.measureText(
743                 (where == TextUtils.TruncateAt.END_SMALL) ?
744                         ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL, 0, 1);
745         int ellipsisStart = 0;
746         int ellipsisCount = 0;
747         int len = lineEnd - lineStart;
748 
749         // We only support start ellipsis on a single line
750         if (where == TextUtils.TruncateAt.START) {
751             if (mMaximumVisibleLineCount == 1) {
752                 float sum = 0;
753                 int i;
754 
755                 for (i = len; i >= 0; i--) {
756                     float w = widths[i - 1 + lineStart - widthStart];
757 
758                     if (w + sum + ellipsisWidth > avail) {
759                         break;
760                     }
761 
762                     sum += w;
763                 }
764 
765                 ellipsisStart = 0;
766                 ellipsisCount = i;
767             } else {
768                 if (Log.isLoggable(TAG, Log.WARN)) {
769                     Log.w(TAG, "Start Ellipsis only supported with one line");
770                 }
771             }
772         } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
773                 where == TextUtils.TruncateAt.END_SMALL) {
774             float sum = 0;
775             int i;
776 
777             for (i = 0; i < len; i++) {
778                 float w = widths[i + lineStart - widthStart];
779 
780                 if (w + sum + ellipsisWidth > avail) {
781                     break;
782                 }
783 
784                 sum += w;
785             }
786 
787             ellipsisStart = i;
788             ellipsisCount = len - i;
789             if (forceEllipsis && ellipsisCount == 0 && len > 0) {
790                 ellipsisStart = len - 1;
791                 ellipsisCount = 1;
792             }
793         } else {
794             // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
795             if (mMaximumVisibleLineCount == 1) {
796                 float lsum = 0, rsum = 0;
797                 int left = 0, right = len;
798 
799                 float ravail = (avail - ellipsisWidth) / 2;
800                 for (right = len; right >= 0; right--) {
801                     float w = widths[right - 1 + lineStart - widthStart];
802 
803                     if (w + rsum > ravail) {
804                         break;
805                     }
806 
807                     rsum += w;
808                 }
809 
810                 float lavail = avail - ellipsisWidth - rsum;
811                 for (left = 0; left < right; left++) {
812                     float w = widths[left + lineStart - widthStart];
813 
814                     if (w + lsum > lavail) {
815                         break;
816                     }
817 
818                     lsum += w;
819                 }
820 
821                 ellipsisStart = left;
822                 ellipsisCount = right - left;
823             } else {
824                 if (Log.isLoggable(TAG, Log.WARN)) {
825                     Log.w(TAG, "Middle Ellipsis only supported with one line");
826                 }
827             }
828         }
829 
830         mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
831         mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
832     }
833 
834     // Override the base class so we can directly access our members,
835     // rather than relying on member functions.
836     // The logic mirrors that of Layout.getLineForVertical
837     // FIXME: It may be faster to do a linear search for layouts without many lines.
838     @Override
getLineForVertical(int vertical)839     public int getLineForVertical(int vertical) {
840         int high = mLineCount;
841         int low = -1;
842         int guess;
843         int[] lines = mLines;
844         while (high - low > 1) {
845             guess = (high + low) >> 1;
846             if (lines[mColumns * guess + TOP] > vertical){
847                 high = guess;
848             } else {
849                 low = guess;
850             }
851         }
852         if (low < 0) {
853             return 0;
854         } else {
855             return low;
856         }
857     }
858 
859     @Override
getLineCount()860     public int getLineCount() {
861         return mLineCount;
862     }
863 
864     @Override
getLineTop(int line)865     public int getLineTop(int line) {
866         int top = mLines[mColumns * line + TOP];
867         if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount &&
868                 line != mLineCount) {
869             top += getBottomPadding();
870         }
871         return top;
872     }
873 
874     @Override
getLineDescent(int line)875     public int getLineDescent(int line) {
876         int descent = mLines[mColumns * line + DESCENT];
877         if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended
878                 line != mLineCount) {
879             descent += getBottomPadding();
880         }
881         return descent;
882     }
883 
884     @Override
getLineStart(int line)885     public int getLineStart(int line) {
886         return mLines[mColumns * line + START] & START_MASK;
887     }
888 
889     @Override
getParagraphDirection(int line)890     public int getParagraphDirection(int line) {
891         return mLines[mColumns * line + DIR] >> DIR_SHIFT;
892     }
893 
894     @Override
getLineContainsTab(int line)895     public boolean getLineContainsTab(int line) {
896         return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
897     }
898 
899     @Override
getLineDirections(int line)900     public final Directions getLineDirections(int line) {
901         return mLineDirections[line];
902     }
903 
904     @Override
getTopPadding()905     public int getTopPadding() {
906         return mTopPadding;
907     }
908 
909     @Override
getBottomPadding()910     public int getBottomPadding() {
911         return mBottomPadding;
912     }
913 
914     @Override
getEllipsisCount(int line)915     public int getEllipsisCount(int line) {
916         if (mColumns < COLUMNS_ELLIPSIZE) {
917             return 0;
918         }
919 
920         return mLines[mColumns * line + ELLIPSIS_COUNT];
921     }
922 
923     @Override
getEllipsisStart(int line)924     public int getEllipsisStart(int line) {
925         if (mColumns < COLUMNS_ELLIPSIZE) {
926             return 0;
927         }
928 
929         return mLines[mColumns * line + ELLIPSIS_START];
930     }
931 
932     @Override
getEllipsizedWidth()933     public int getEllipsizedWidth() {
934         return mEllipsizedWidth;
935     }
936 
prepare()937     void prepare() {
938         mMeasured = MeasuredText.obtain();
939     }
940 
finish()941     void finish() {
942         mMeasured = MeasuredText.recycle(mMeasured);
943     }
944 
945     private int mLineCount;
946     private int mTopPadding, mBottomPadding;
947     private int mColumns;
948     private int mEllipsizedWidth;
949 
950     private static final int COLUMNS_NORMAL = 3;
951     private static final int COLUMNS_ELLIPSIZE = 5;
952     private static final int START = 0;
953     private static final int DIR = START;
954     private static final int TAB = START;
955     private static final int TOP = 1;
956     private static final int DESCENT = 2;
957     private static final int ELLIPSIS_START = 3;
958     private static final int ELLIPSIS_COUNT = 4;
959 
960     private int[] mLines;
961     private Directions[] mLineDirections;
962     private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
963 
964     private static final int START_MASK = 0x1FFFFFFF;
965     private static final int DIR_SHIFT  = 30;
966     private static final int TAB_MASK   = 0x20000000;
967 
968     private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
969 
970     private static final char CHAR_FIRST_CJK = '\u2E80';
971 
972     private static final char CHAR_NEW_LINE = '\n';
973     private static final char CHAR_TAB = '\t';
974     private static final char CHAR_SPACE = ' ';
975     private static final char CHAR_DOT = '.';
976     private static final char CHAR_COMMA = ',';
977     private static final char CHAR_COLON = ':';
978     private static final char CHAR_SEMICOLON = ';';
979     private static final char CHAR_SLASH = '/';
980     private static final char CHAR_HYPHEN = '-';
981 
982     private static final double EXTRA_ROUNDING = 0.5;
983 
984     private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800;
985     private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF;
986 
987     /*
988      * This is reused across calls to generate()
989      */
990     private MeasuredText mMeasured;
991     private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
992 }
993