• 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, align, textDir,
141                  spacingmult, spacingadd, includepad, includepad,
142                  ellipsizedWidth, 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, Alignment align, 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                         Alignment align, TextDirectionHeuristic textDir,
161                         float spacingmult, float spacingadd,
162                         boolean includepad, boolean trackpad,
163                         float ellipsizedWidth, 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             int here = paraStart;
250 
251             int ok = paraStart;
252             float okWidth = w;
253             int okAscent = 0, okDescent = 0, okTop = 0, okBottom = 0;
254 
255             int fit = paraStart;
256             float fitWidth = w;
257             int fitAscent = 0, fitDescent = 0, fitTop = 0, fitBottom = 0;
258 
259             boolean hasTabOrEmoji = false;
260             boolean hasTab = false;
261             TabStops tabStops = null;
262 
263             for (int spanStart = paraStart, spanEnd = spanStart, nextSpanStart;
264                     spanStart < paraEnd; spanStart = nextSpanStart) {
265 
266                 if (spanStart == spanEnd) {
267                     if (spanned == null)
268                         spanEnd = paraEnd;
269                     else
270                         spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
271                                 MetricAffectingSpan.class);
272 
273                     int spanLen = spanEnd - spanStart;
274                     if (spanned == null) {
275                         measured.addStyleRun(paint, spanLen, fm);
276                     } else {
277                         MetricAffectingSpan[] spans =
278                             spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
279                         spans = TextUtils.removeEmptySpans(spans, spanned,
280                                 MetricAffectingSpan.class);
281                         measured.addStyleRun(paint, spans, spanLen, fm);
282                     }
283                 }
284 
285                 nextSpanStart = spanEnd;
286 
287                 int fmTop = fm.top;
288                 int fmBottom = fm.bottom;
289                 int fmAscent = fm.ascent;
290                 int fmDescent = fm.descent;
291 
292                 for (int j = spanStart; j < spanEnd; j++) {
293                     char c = chs[j - paraStart];
294 
295                     if (c == CHAR_NEW_LINE) {
296                         // intentionally left empty
297                     } else if (c == CHAR_TAB) {
298                         if (hasTab == false) {
299                             hasTab = true;
300                             hasTabOrEmoji = true;
301                             if (spanned != null) {
302                                 // First tab this para, check for tabstops
303                                 TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
304                                         paraEnd, TabStopSpan.class);
305                                 if (spans.length > 0) {
306                                     tabStops = new TabStops(TAB_INCREMENT, spans);
307                                 }
308                             }
309                         }
310                         if (tabStops != null) {
311                             w = tabStops.nextTab(w);
312                         } else {
313                             w = TabStops.nextDefaultStop(w, TAB_INCREMENT);
314                         }
315                     } else if (c >= CHAR_FIRST_HIGH_SURROGATE && c <= CHAR_LAST_LOW_SURROGATE
316                             && j + 1 < spanEnd) {
317                         int emoji = Character.codePointAt(chs, j - paraStart);
318 
319                         if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
320                             Bitmap bm = EMOJI_FACTORY.getBitmapFromAndroidPua(emoji);
321 
322                             if (bm != null) {
323                                 Paint whichPaint;
324 
325                                 if (spanned == null) {
326                                     whichPaint = paint;
327                                 } else {
328                                     whichPaint = mWorkPaint;
329                                 }
330 
331                                 float wid = bm.getWidth() *
332                                             -whichPaint.ascent() /
333                                             bm.getHeight();
334 
335                                 w += wid;
336                                 hasTabOrEmoji = true;
337                                 j++;
338                             } else {
339                                 w += widths[j - paraStart];
340                             }
341                         } else {
342                             w += widths[j - paraStart];
343                         }
344                     } else {
345                         w += widths[j - paraStart];
346                     }
347 
348                     // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width);
349 
350                     if (w <= width) {
351                         fitWidth = w;
352                         fit = j + 1;
353 
354                         if (fmTop < fitTop)
355                             fitTop = fmTop;
356                         if (fmAscent < fitAscent)
357                             fitAscent = fmAscent;
358                         if (fmDescent > fitDescent)
359                             fitDescent = fmDescent;
360                         if (fmBottom > fitBottom)
361                             fitBottom = fmBottom;
362 
363                         /*
364                          * From the Unicode Line Breaking Algorithm:
365                          * (at least approximately)
366                          *
367                          * .,:; are class IS: breakpoints
368                          *      except when adjacent to digits
369                          * /    is class SY: a breakpoint
370                          *      except when followed by a digit.
371                          * -    is class HY: a breakpoint
372                          *      except when followed by a digit.
373                          *
374                          * Ideographs are class ID: breakpoints when adjacent,
375                          * except for NS (non-starters), which can be broken
376                          * after but not before.
377                          */
378 
379                         if (c == CHAR_SPACE || c == CHAR_TAB ||
380                             ((c == CHAR_DOT || c == CHAR_COMMA ||
381                                     c == CHAR_COLON || c == CHAR_SEMICOLON) &&
382                              (j - 1 < here || !Character.isDigit(chs[j - 1 - paraStart])) &&
383                              (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
384                             ((c == CHAR_SLASH || c == CHAR_HYPHEN) &&
385                              (j + 1 >= spanEnd || !Character.isDigit(chs[j + 1 - paraStart]))) ||
386                             (c >= CHAR_FIRST_CJK && isIdeographic(c, true) &&
387                              j + 1 < spanEnd && isIdeographic(chs[j + 1 - paraStart], false))) {
388                             okWidth = w;
389                             ok = j + 1;
390 
391                             if (fitTop < okTop)
392                                 okTop = fitTop;
393                             if (fitAscent < okAscent)
394                                 okAscent = fitAscent;
395                             if (fitDescent > okDescent)
396                                 okDescent = fitDescent;
397                             if (fitBottom > okBottom)
398                                 okBottom = fitBottom;
399                         }
400                     } else {
401                             final boolean moreChars = (j + 1 < spanEnd);
402                             if (ok != here) {
403                                 // Log.e("text", "output ok " + here + " to " +ok);
404 
405                                 while (ok < spanEnd && chs[ok - paraStart] == CHAR_SPACE) {
406                                     ok++;
407                                 }
408 
409                                 v = out(source,
410                                         here, ok,
411                                         okAscent, okDescent, okTop, okBottom,
412                                         v,
413                                         spacingmult, spacingadd, chooseHt,
414                                         chooseHtv, fm, hasTabOrEmoji,
415                                         needMultiply, paraStart, chdirs, dir, easy,
416                                         ok == bufEnd, includepad, trackpad,
417                                         chs, widths, paraStart,
418                                         ellipsize, ellipsizedWidth, okWidth,
419                                         paint, moreChars);
420 
421                                 here = ok;
422                             } else if (fit != here) {
423                                 // Log.e("text", "output fit " + here + " to " +fit);
424                                 v = out(source,
425                                         here, fit,
426                                         fitAscent, fitDescent,
427                                         fitTop, fitBottom,
428                                         v,
429                                         spacingmult, spacingadd, chooseHt,
430                                         chooseHtv, fm, hasTabOrEmoji,
431                                         needMultiply, paraStart, chdirs, dir, easy,
432                                         fit == bufEnd, includepad, trackpad,
433                                         chs, widths, paraStart,
434                                         ellipsize, ellipsizedWidth, fitWidth,
435                                         paint, moreChars);
436 
437                                 here = fit;
438                             } else {
439                                 // Log.e("text", "output one " + here + " to " +(here + 1));
440                                 // XXX not sure why the existing fm wasn't ok.
441                                 // measureText(paint, mWorkPaint,
442                                 //             source, here, here + 1, fm, tab,
443                                 //             null);
444 
445                                 v = out(source,
446                                         here, here+1,
447                                         fm.ascent, fm.descent,
448                                         fm.top, fm.bottom,
449                                         v,
450                                         spacingmult, spacingadd, chooseHt,
451                                         chooseHtv, fm, hasTabOrEmoji,
452                                         needMultiply, paraStart, chdirs, dir, easy,
453                                         here + 1 == bufEnd, includepad,
454                                         trackpad,
455                                         chs, widths, paraStart,
456                                         ellipsize, ellipsizedWidth,
457                                         widths[here - paraStart], paint, moreChars);
458 
459                                 here = here + 1;
460                             }
461 
462                         if (here < spanStart) {
463                             // didn't output all the text for this span
464                             // we've measured the raw widths, though, so
465                             // just reset the start point
466                             j = nextSpanStart = here;
467                         } else {
468                             j = here - 1;    // continue looping
469                         }
470 
471                         ok = fit = here;
472                         w = 0;
473                         fitAscent = fitDescent = fitTop = fitBottom = 0;
474                         okAscent = okDescent = okTop = okBottom = 0;
475 
476                         if (--firstWidthLineLimit <= 0) {
477                             width = restWidth;
478                         }
479                     }
480                     if (mLineCount >= mMaximumVisibleLineCount) {
481                         break;
482                     }
483                 }
484             }
485 
486             if (paraEnd != here && mLineCount < mMaximumVisibleLineCount) {
487                 if ((fitTop | fitBottom | fitDescent | fitAscent) == 0) {
488                     paint.getFontMetricsInt(fm);
489 
490                     fitTop = fm.top;
491                     fitBottom = fm.bottom;
492                     fitAscent = fm.ascent;
493                     fitDescent = fm.descent;
494                 }
495 
496                 // Log.e("text", "output rest " + here + " to " + end);
497 
498                 v = out(source,
499                         here, paraEnd, fitAscent, fitDescent,
500                         fitTop, fitBottom,
501                         v,
502                         spacingmult, spacingadd, chooseHt,
503                         chooseHtv, fm, hasTabOrEmoji,
504                         needMultiply, paraStart, chdirs, dir, easy,
505                         paraEnd == bufEnd, includepad, trackpad,
506                         chs, widths, paraStart,
507                         ellipsize, ellipsizedWidth, w, paint, paraEnd != bufEnd);
508             }
509 
510             paraStart = paraEnd;
511 
512             if (paraEnd == bufEnd)
513                 break;
514         }
515 
516         if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
517                 mLineCount < mMaximumVisibleLineCount) {
518             // Log.e("text", "output last " + bufEnd);
519 
520             paint.getFontMetricsInt(fm);
521 
522             v = out(source,
523                     bufEnd, bufEnd, fm.ascent, fm.descent,
524                     fm.top, fm.bottom,
525                     v,
526                     spacingmult, spacingadd, null,
527                     null, fm, false,
528                     needMultiply, bufEnd, null, DEFAULT_DIR, true,
529                     true, includepad, trackpad,
530                     null, null, bufStart,
531                     ellipsize, ellipsizedWidth, 0, paint, false);
532         }
533     }
534 
535     /**
536      * Returns true if the specified character is one of those specified
537      * as being Ideographic (class ID) by the Unicode Line Breaking Algorithm
538      * (http://www.unicode.org/unicode/reports/tr14/), and is therefore OK
539      * to break between a pair of.
540      *
541      * @param includeNonStarters also return true for category NS
542      *                           (non-starters), which can be broken
543      *                           after but not before.
544      */
isIdeographic(char c, boolean includeNonStarters)545     private static final boolean isIdeographic(char c, boolean includeNonStarters) {
546         if (c >= '\u2E80' && c <= '\u2FFF') {
547             return true; // CJK, KANGXI RADICALS, DESCRIPTION SYMBOLS
548         }
549         if (c == '\u3000') {
550             return true; // IDEOGRAPHIC SPACE
551         }
552         if (c >= '\u3040' && c <= '\u309F') {
553             if (!includeNonStarters) {
554                 switch (c) {
555                 case '\u3041': //  # HIRAGANA LETTER SMALL A
556                 case '\u3043': //  # HIRAGANA LETTER SMALL I
557                 case '\u3045': //  # HIRAGANA LETTER SMALL U
558                 case '\u3047': //  # HIRAGANA LETTER SMALL E
559                 case '\u3049': //  # HIRAGANA LETTER SMALL O
560                 case '\u3063': //  # HIRAGANA LETTER SMALL TU
561                 case '\u3083': //  # HIRAGANA LETTER SMALL YA
562                 case '\u3085': //  # HIRAGANA LETTER SMALL YU
563                 case '\u3087': //  # HIRAGANA LETTER SMALL YO
564                 case '\u308E': //  # HIRAGANA LETTER SMALL WA
565                 case '\u3095': //  # HIRAGANA LETTER SMALL KA
566                 case '\u3096': //  # HIRAGANA LETTER SMALL KE
567                 case '\u309B': //  # KATAKANA-HIRAGANA VOICED SOUND MARK
568                 case '\u309C': //  # KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
569                 case '\u309D': //  # HIRAGANA ITERATION MARK
570                 case '\u309E': //  # HIRAGANA VOICED ITERATION MARK
571                     return false;
572                 }
573             }
574             return true; // Hiragana (except small characters)
575         }
576         if (c >= '\u30A0' && c <= '\u30FF') {
577             if (!includeNonStarters) {
578                 switch (c) {
579                 case '\u30A0': //  # KATAKANA-HIRAGANA DOUBLE HYPHEN
580                 case '\u30A1': //  # KATAKANA LETTER SMALL A
581                 case '\u30A3': //  # KATAKANA LETTER SMALL I
582                 case '\u30A5': //  # KATAKANA LETTER SMALL U
583                 case '\u30A7': //  # KATAKANA LETTER SMALL E
584                 case '\u30A9': //  # KATAKANA LETTER SMALL O
585                 case '\u30C3': //  # KATAKANA LETTER SMALL TU
586                 case '\u30E3': //  # KATAKANA LETTER SMALL YA
587                 case '\u30E5': //  # KATAKANA LETTER SMALL YU
588                 case '\u30E7': //  # KATAKANA LETTER SMALL YO
589                 case '\u30EE': //  # KATAKANA LETTER SMALL WA
590                 case '\u30F5': //  # KATAKANA LETTER SMALL KA
591                 case '\u30F6': //  # KATAKANA LETTER SMALL KE
592                 case '\u30FB': //  # KATAKANA MIDDLE DOT
593                 case '\u30FC': //  # KATAKANA-HIRAGANA PROLONGED SOUND MARK
594                 case '\u30FD': //  # KATAKANA ITERATION MARK
595                 case '\u30FE': //  # KATAKANA VOICED ITERATION MARK
596                     return false;
597                 }
598             }
599             return true; // Katakana (except small characters)
600         }
601         if (c >= '\u3400' && c <= '\u4DB5') {
602             return true; // CJK UNIFIED IDEOGRAPHS EXTENSION A
603         }
604         if (c >= '\u4E00' && c <= '\u9FBB') {
605             return true; // CJK UNIFIED IDEOGRAPHS
606         }
607         if (c >= '\uF900' && c <= '\uFAD9') {
608             return true; // CJK COMPATIBILITY IDEOGRAPHS
609         }
610         if (c >= '\uA000' && c <= '\uA48F') {
611             return true; // YI SYLLABLES
612         }
613         if (c >= '\uA490' && c <= '\uA4CF') {
614             return true; // YI RADICALS
615         }
616         if (c >= '\uFE62' && c <= '\uFE66') {
617             return true; // SMALL PLUS SIGN to SMALL EQUALS SIGN
618         }
619         if (c >= '\uFF10' && c <= '\uFF19') {
620             return true; // WIDE DIGITS
621         }
622 
623         return false;
624     }
625 
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, int pstart, byte[] chdirs, int dir, boolean easy, boolean last, boolean includePad, boolean trackPad, char[] chs, float[] widths, int widthStart, TextUtils.TruncateAt ellipsize, float ellipsisWidth, float textWidth, TextPaint paint, boolean moreChars)626     private int out(CharSequence text, int start, int end,
627                       int above, int below, int top, int bottom, int v,
628                       float spacingmult, float spacingadd,
629                       LineHeightSpan[] chooseHt, int[] chooseHtv,
630                       Paint.FontMetricsInt fm, boolean hasTabOrEmoji,
631                       boolean needMultiply, int pstart, byte[] chdirs,
632                       int dir, boolean easy, boolean last,
633                       boolean includePad, boolean trackPad,
634                       char[] chs, float[] widths, int widthStart,
635                       TextUtils.TruncateAt ellipsize, float ellipsisWidth,
636                       float textWidth, TextPaint paint, boolean moreChars) {
637         int j = mLineCount;
638         int off = j * mColumns;
639         int want = off + mColumns + TOP;
640         int[] lines = mLines;
641 
642         if (want >= lines.length) {
643             int nlen = ArrayUtils.idealIntArraySize(want + 1);
644             int[] grow = new int[nlen];
645             System.arraycopy(lines, 0, grow, 0, lines.length);
646             mLines = grow;
647             lines = grow;
648 
649             Directions[] grow2 = new Directions[nlen];
650             System.arraycopy(mLineDirections, 0, grow2, 0,
651                              mLineDirections.length);
652             mLineDirections = grow2;
653         }
654 
655         if (chooseHt != null) {
656             fm.ascent = above;
657             fm.descent = below;
658             fm.top = top;
659             fm.bottom = bottom;
660 
661             for (int i = 0; i < chooseHt.length; i++) {
662                 if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
663                     ((LineHeightSpan.WithDensity) chooseHt[i]).
664                         chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
665 
666                 } else {
667                     chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
668                 }
669             }
670 
671             above = fm.ascent;
672             below = fm.descent;
673             top = fm.top;
674             bottom = fm.bottom;
675         }
676 
677         if (j == 0) {
678             if (trackPad) {
679                 mTopPadding = top - above;
680             }
681 
682             if (includePad) {
683                 above = top;
684             }
685         }
686         if (last) {
687             if (trackPad) {
688                 mBottomPadding = bottom - below;
689             }
690 
691             if (includePad) {
692                 below = bottom;
693             }
694         }
695 
696         int extra;
697 
698         if (needMultiply) {
699             double ex = (below - above) * (spacingmult - 1) + spacingadd;
700             if (ex >= 0) {
701                 extra = (int)(ex + EXTRA_ROUNDING);
702             } else {
703                 extra = -(int)(-ex + EXTRA_ROUNDING);
704             }
705         } else {
706             extra = 0;
707         }
708 
709         lines[off + START] = start;
710         lines[off + TOP] = v;
711         lines[off + DESCENT] = below + extra;
712 
713         v += (below - above) + extra;
714         lines[off + mColumns + START] = end;
715         lines[off + mColumns + TOP] = v;
716 
717         if (hasTabOrEmoji)
718             lines[off + TAB] |= TAB_MASK;
719 
720         lines[off + DIR] |= dir << DIR_SHIFT;
721         Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
722         // easy means all chars < the first RTL, so no emoji, no nothing
723         // XXX a run with no text or all spaces is easy but might be an empty
724         // RTL paragraph.  Make sure easy is false if this is the case.
725         if (easy) {
726             mLineDirections[j] = linedirs;
727         } else {
728             mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
729                     start - widthStart, end - start);
730         }
731 
732         if (ellipsize != null) {
733             // If there is only one line, then do any type of ellipsis except when it is MARQUEE
734             // if there are multiple lines, just allow END ellipsis on the last line
735             boolean firstLine = (j == 0);
736             boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
737             boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);
738 
739             boolean doEllipsis = (firstLine && !moreChars &&
740                                 ellipsize != TextUtils.TruncateAt.MARQUEE) ||
741                         (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
742                                 ellipsize == TextUtils.TruncateAt.END);
743             if (doEllipsis) {
744                 calculateEllipsis(start, end, widths, widthStart,
745                         ellipsisWidth, ellipsize, j,
746                         textWidth, paint, forceEllipsis);
747             }
748         }
749 
750         mLineCount++;
751         return v;
752     }
753 
calculateEllipsis(int lineStart, int lineEnd, float[] widths, int widthStart, float avail, TextUtils.TruncateAt where, int line, float textWidth, TextPaint paint, boolean forceEllipsis)754     private void calculateEllipsis(int lineStart, int lineEnd,
755                                    float[] widths, int widthStart,
756                                    float avail, TextUtils.TruncateAt where,
757                                    int line, float textWidth, TextPaint paint,
758                                    boolean forceEllipsis) {
759         if (textWidth <= avail && !forceEllipsis) {
760             // Everything fits!
761             mLines[mColumns * line + ELLIPSIS_START] = 0;
762             mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
763             return;
764         }
765 
766         float ellipsisWidth = paint.measureText(
767                 (where == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL);
768         int ellipsisStart = 0;
769         int ellipsisCount = 0;
770         int len = lineEnd - lineStart;
771 
772         // We only support start ellipsis on a single line
773         if (where == TextUtils.TruncateAt.START) {
774             if (mMaximumVisibleLineCount == 1) {
775                 float sum = 0;
776                 int i;
777 
778                 for (i = len; i >= 0; i--) {
779                     float w = widths[i - 1 + lineStart - widthStart];
780 
781                     if (w + sum + ellipsisWidth > avail) {
782                         break;
783                     }
784 
785                     sum += w;
786                 }
787 
788                 ellipsisStart = 0;
789                 ellipsisCount = i;
790             } else {
791                 if (Log.isLoggable(TAG, Log.WARN)) {
792                     Log.w(TAG, "Start Ellipsis only supported with one line");
793                 }
794             }
795         } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
796                 where == TextUtils.TruncateAt.END_SMALL) {
797             float sum = 0;
798             int i;
799 
800             for (i = 0; i < len; i++) {
801                 float w = widths[i + lineStart - widthStart];
802 
803                 if (w + sum + ellipsisWidth > avail) {
804                     break;
805                 }
806 
807                 sum += w;
808             }
809 
810             ellipsisStart = i;
811             ellipsisCount = len - i;
812             if (forceEllipsis && ellipsisCount == 0 && len > 0) {
813                 ellipsisStart = len - 1;
814                 ellipsisCount = 1;
815             }
816         } else {
817             // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
818             if (mMaximumVisibleLineCount == 1) {
819                 float lsum = 0, rsum = 0;
820                 int left = 0, right = len;
821 
822                 float ravail = (avail - ellipsisWidth) / 2;
823                 for (right = len; right >= 0; right--) {
824                     float w = widths[right - 1 + lineStart - widthStart];
825 
826                     if (w + rsum > ravail) {
827                         break;
828                     }
829 
830                     rsum += w;
831                 }
832 
833                 float lavail = avail - ellipsisWidth - rsum;
834                 for (left = 0; left < right; left++) {
835                     float w = widths[left + lineStart - widthStart];
836 
837                     if (w + lsum > lavail) {
838                         break;
839                     }
840 
841                     lsum += w;
842                 }
843 
844                 ellipsisStart = left;
845                 ellipsisCount = right - left;
846             } else {
847                 if (Log.isLoggable(TAG, Log.WARN)) {
848                     Log.w(TAG, "Middle Ellipsis only supported with one line");
849                 }
850             }
851         }
852 
853         mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
854         mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
855     }
856 
857     // Override the base class so we can directly access our members,
858     // rather than relying on member functions.
859     // The logic mirrors that of Layout.getLineForVertical
860     // FIXME: It may be faster to do a linear search for layouts without many lines.
861     @Override
getLineForVertical(int vertical)862     public int getLineForVertical(int vertical) {
863         int high = mLineCount;
864         int low = -1;
865         int guess;
866         int[] lines = mLines;
867         while (high - low > 1) {
868             guess = (high + low) >> 1;
869             if (lines[mColumns * guess + TOP] > vertical){
870                 high = guess;
871             } else {
872                 low = guess;
873             }
874         }
875         if (low < 0) {
876             return 0;
877         } else {
878             return low;
879         }
880     }
881 
882     @Override
getLineCount()883     public int getLineCount() {
884         return mLineCount;
885     }
886 
887     @Override
getLineTop(int line)888     public int getLineTop(int line) {
889         int top = mLines[mColumns * line + TOP];
890         if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount &&
891                 line != mLineCount) {
892             top += getBottomPadding();
893         }
894         return top;
895     }
896 
897     @Override
getLineDescent(int line)898     public int getLineDescent(int line) {
899         int descent = mLines[mColumns * line + DESCENT];
900         if (mMaximumVisibleLineCount > 0 && line >= mMaximumVisibleLineCount - 1 && // -1 intended
901                 line != mLineCount) {
902             descent += getBottomPadding();
903         }
904         return descent;
905     }
906 
907     @Override
getLineStart(int line)908     public int getLineStart(int line) {
909         return mLines[mColumns * line + START] & START_MASK;
910     }
911 
912     @Override
getParagraphDirection(int line)913     public int getParagraphDirection(int line) {
914         return mLines[mColumns * line + DIR] >> DIR_SHIFT;
915     }
916 
917     @Override
getLineContainsTab(int line)918     public boolean getLineContainsTab(int line) {
919         return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
920     }
921 
922     @Override
getLineDirections(int line)923     public final Directions getLineDirections(int line) {
924         return mLineDirections[line];
925     }
926 
927     @Override
getTopPadding()928     public int getTopPadding() {
929         return mTopPadding;
930     }
931 
932     @Override
getBottomPadding()933     public int getBottomPadding() {
934         return mBottomPadding;
935     }
936 
937     @Override
getEllipsisCount(int line)938     public int getEllipsisCount(int line) {
939         if (mColumns < COLUMNS_ELLIPSIZE) {
940             return 0;
941         }
942 
943         return mLines[mColumns * line + ELLIPSIS_COUNT];
944     }
945 
946     @Override
getEllipsisStart(int line)947     public int getEllipsisStart(int line) {
948         if (mColumns < COLUMNS_ELLIPSIZE) {
949             return 0;
950         }
951 
952         return mLines[mColumns * line + ELLIPSIS_START];
953     }
954 
955     @Override
getEllipsizedWidth()956     public int getEllipsizedWidth() {
957         return mEllipsizedWidth;
958     }
959 
prepare()960     void prepare() {
961         mMeasured = MeasuredText.obtain();
962     }
963 
finish()964     void finish() {
965         mMeasured = MeasuredText.recycle(mMeasured);
966     }
967 
968     private int mLineCount;
969     private int mTopPadding, mBottomPadding;
970     private int mColumns;
971     private int mEllipsizedWidth;
972 
973     private static final int COLUMNS_NORMAL = 3;
974     private static final int COLUMNS_ELLIPSIZE = 5;
975     private static final int START = 0;
976     private static final int DIR = START;
977     private static final int TAB = START;
978     private static final int TOP = 1;
979     private static final int DESCENT = 2;
980     private static final int ELLIPSIS_START = 3;
981     private static final int ELLIPSIS_COUNT = 4;
982 
983     private int[] mLines;
984     private Directions[] mLineDirections;
985     private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
986 
987     private static final int START_MASK = 0x1FFFFFFF;
988     private static final int DIR_SHIFT  = 30;
989     private static final int TAB_MASK   = 0x20000000;
990 
991     private static final int TAB_INCREMENT = 20; // same as Layout, but that's private
992 
993     private static final char CHAR_FIRST_CJK = '\u2E80';
994 
995     private static final char CHAR_NEW_LINE = '\n';
996     private static final char CHAR_TAB = '\t';
997     private static final char CHAR_SPACE = ' ';
998     private static final char CHAR_DOT = '.';
999     private static final char CHAR_COMMA = ',';
1000     private static final char CHAR_COLON = ':';
1001     private static final char CHAR_SEMICOLON = ';';
1002     private static final char CHAR_SLASH = '/';
1003     private static final char CHAR_HYPHEN = '-';
1004 
1005     private static final double EXTRA_ROUNDING = 0.5;
1006 
1007     private static final String ELLIPSIS_NORMAL = "\u2026"; // this is "..."
1008     private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // this is ".."
1009 
1010     private static final int CHAR_FIRST_HIGH_SURROGATE = 0xD800;
1011     private static final int CHAR_LAST_LOW_SURROGATE = 0xDFFF;
1012 
1013     /*
1014      * This is reused across calls to generate()
1015      */
1016     private MeasuredText mMeasured;
1017     private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
1018 }
1019