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.Canvas;
20 import android.graphics.Paint;
21 import android.graphics.Path;
22 import android.text.style.ParagraphStyle;
23 
24 /**
25  * A BoringLayout is a very simple Layout implementation for text that
26  * fits on a single line and is all left-to-right characters.
27  * You will probably never want to make one of these yourself;
28  * if you do, be sure to call {@link #isBoring} first to make sure
29  * the text meets the criteria.
30  * <p>This class is used by widgets to control text layout. You should not need
31  * to use this class directly unless you are implementing your own widget
32  * or custom display object, in which case
33  * you are encouraged to use a Layout instead of calling
34  * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
35  *  Canvas.drawText()} directly.</p>
36  */
37 public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback {
make(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad)38     public static BoringLayout make(CharSequence source,
39                         TextPaint paint, int outerwidth,
40                         Alignment align,
41                         float spacingmult, float spacingadd,
42                         BoringLayout.Metrics metrics, boolean includepad) {
43         return new BoringLayout(source, paint, outerwidth, align,
44                                 spacingmult, spacingadd, metrics,
45                                 includepad);
46     }
47 
make(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)48     public static BoringLayout make(CharSequence source,
49                         TextPaint paint, int outerwidth,
50                         Alignment align,
51                         float spacingmult, float spacingadd,
52                         BoringLayout.Metrics metrics, boolean includepad,
53                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
54         return new BoringLayout(source, paint, outerwidth, align,
55                                 spacingmult, spacingadd, metrics,
56                                 includepad, ellipsize, ellipsizedWidth);
57     }
58 
59     /**
60      * Returns a BoringLayout for the specified text, potentially reusing
61      * this one if it is already suitable.  The caller must make sure that
62      * no one is still using this Layout.
63      */
replaceOrMake(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad)64     public BoringLayout replaceOrMake(CharSequence source, TextPaint paint,
65                                       int outerwidth, Alignment align,
66                                       float spacingmult, float spacingadd,
67                                       BoringLayout.Metrics metrics,
68                                       boolean includepad) {
69         replaceWith(source, paint, outerwidth, align, spacingmult,
70                     spacingadd);
71 
72         mEllipsizedWidth = outerwidth;
73         mEllipsizedStart = 0;
74         mEllipsizedCount = 0;
75 
76         init(source, paint, outerwidth, align, spacingmult, spacingadd,
77              metrics, includepad, true);
78         return this;
79     }
80 
81     /**
82      * Returns a BoringLayout for the specified text, potentially reusing
83      * this one if it is already suitable.  The caller must make sure that
84      * no one is still using this Layout.
85      */
replaceOrMake(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)86     public BoringLayout replaceOrMake(CharSequence source, TextPaint paint,
87                                       int outerwidth, Alignment align,
88                                       float spacingmult, float spacingadd,
89                                       BoringLayout.Metrics metrics,
90                                       boolean includepad,
91                                       TextUtils.TruncateAt ellipsize,
92                                       int ellipsizedWidth) {
93         boolean trust;
94 
95         if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
96             replaceWith(source, paint, outerwidth, align, spacingmult,
97                         spacingadd);
98 
99             mEllipsizedWidth = outerwidth;
100             mEllipsizedStart = 0;
101             mEllipsizedCount = 0;
102             trust = true;
103         } else {
104             replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
105                                            ellipsize, true, this),
106                         paint, outerwidth, align, spacingmult,
107                         spacingadd);
108 
109             mEllipsizedWidth = ellipsizedWidth;
110             trust = false;
111         }
112 
113         init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
114              metrics, includepad, trust);
115         return this;
116     }
117 
BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad)118     public BoringLayout(CharSequence source,
119                         TextPaint paint, int outerwidth,
120                         Alignment align,
121                         float spacingmult, float spacingadd,
122                         BoringLayout.Metrics metrics, boolean includepad) {
123         super(source, paint, outerwidth, align, spacingmult, spacingadd);
124 
125         mEllipsizedWidth = outerwidth;
126         mEllipsizedStart = 0;
127         mEllipsizedCount = 0;
128 
129         init(source, paint, outerwidth, align, spacingmult, spacingadd,
130              metrics, includepad, true);
131     }
132 
BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)133     public BoringLayout(CharSequence source,
134                         TextPaint paint, int outerwidth,
135                         Alignment align,
136                         float spacingmult, float spacingadd,
137                         BoringLayout.Metrics metrics, boolean includepad,
138                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
139         /*
140          * It is silly to have to call super() and then replaceWith(),
141          * but we can't use "this" for the callback until the call to
142          * super() finishes.
143          */
144         super(source, paint, outerwidth, align, spacingmult, spacingadd);
145 
146         boolean trust;
147 
148         if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
149             mEllipsizedWidth = outerwidth;
150             mEllipsizedStart = 0;
151             mEllipsizedCount = 0;
152             trust = true;
153         } else {
154             replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
155                                            ellipsize, true, this),
156                         paint, outerwidth, align, spacingmult,
157                         spacingadd);
158 
159 
160             mEllipsizedWidth = ellipsizedWidth;
161             trust = false;
162         }
163 
164         init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
165              metrics, includepad, trust);
166     }
167 
init(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, boolean trustWidth)168     /* package */ void init(CharSequence source,
169                             TextPaint paint, int outerwidth,
170                             Alignment align,
171                             float spacingmult, float spacingadd,
172                             BoringLayout.Metrics metrics, boolean includepad,
173                             boolean trustWidth) {
174         int spacing;
175 
176         if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {
177             mDirect = source.toString();
178         } else {
179             mDirect = null;
180         }
181 
182         mPaint = paint;
183 
184         if (includepad) {
185             spacing = metrics.bottom - metrics.top;
186             mDesc = metrics.bottom;
187         } else {
188             spacing = metrics.descent - metrics.ascent;
189             mDesc = metrics.descent;
190         }
191 
192         mBottom = spacing;
193 
194         if (trustWidth) {
195             mMax = metrics.width;
196         } else {
197             /*
198              * If we have ellipsized, we have to actually calculate the
199              * width because the width that was passed in was for the
200              * full text, not the ellipsized form.
201              */
202             TextLine line = TextLine.obtain();
203             line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
204                     Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
205             mMax = (int) Math.ceil(line.metrics(null));
206             TextLine.recycle(line);
207         }
208 
209         if (includepad) {
210             mTopPadding = metrics.top - metrics.ascent;
211             mBottomPadding = metrics.bottom - metrics.descent;
212         }
213     }
214 
215     /**
216      * Returns null if not boring; the width, ascent, and descent if boring.
217      */
isBoring(CharSequence text, TextPaint paint)218     public static Metrics isBoring(CharSequence text,
219                                    TextPaint paint) {
220         return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null);
221     }
222 
223     /**
224      * Returns null if not boring; the width, ascent, and descent in the
225      * provided Metrics object (or a new one if the provided one was null)
226      * if boring.
227      */
isBoring(CharSequence text, TextPaint paint, Metrics metrics)228     public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) {
229         return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics);
230     }
231 
232     /**
233      * Returns true if the text contains any RTL characters, bidi format characters, or surrogate
234      * code units.
235      */
hasAnyInterestingChars(CharSequence text, int textLength)236     private static boolean hasAnyInterestingChars(CharSequence text, int textLength) {
237         final int MAX_BUF_LEN = 500;
238         final char[] buffer = TextUtils.obtain(MAX_BUF_LEN);
239         try {
240             for (int start = 0; start < textLength; start += MAX_BUF_LEN) {
241                 final int end = Math.min(start + MAX_BUF_LEN, textLength);
242 
243                 // No need to worry about getting half codepoints, since we consider surrogate code
244                 // units "interesting" as soon we see one.
245                 TextUtils.getChars(text, start, end, buffer, 0);
246 
247                 final int len = end - start;
248                 for (int i = 0; i < len; i++) {
249                     final char c = buffer[i];
250                     if (c == '\n' || c == '\t' || TextUtils.couldAffectRtl(c)) {
251                         return true;
252                     }
253                 }
254             }
255             return false;
256         } finally {
257             TextUtils.recycle(buffer);
258         }
259     }
260 
261     /**
262      * Returns null if not boring; the width, ascent, and descent in the
263      * provided Metrics object (or a new one if the provided one was null)
264      * if boring.
265      * @hide
266      */
isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir, Metrics metrics)267     public static Metrics isBoring(CharSequence text, TextPaint paint,
268             TextDirectionHeuristic textDir, Metrics metrics) {
269         final int textLength = text.length();
270         if (hasAnyInterestingChars(text, textLength)) {
271            return null;  // There are some interesting characters. Not boring.
272         }
273         if (textDir != null && textDir.isRtl(text, 0, textLength)) {
274            return null;  // The heuristic considers the whole text RTL. Not boring.
275         }
276         if (text instanceof Spanned) {
277             Spanned sp = (Spanned) text;
278             Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);
279             if (styles.length > 0) {
280                 return null;  // There are some PargraphStyle spans. Not boring.
281             }
282         }
283 
284         Metrics fm = metrics;
285         if (fm == null) {
286             fm = new Metrics();
287         } else {
288             fm.reset();
289         }
290 
291         TextLine line = TextLine.obtain();
292         line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
293                 Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
294         fm.width = (int) Math.ceil(line.metrics(fm));
295         TextLine.recycle(line);
296 
297         return fm;
298     }
299 
300     @Override
getHeight()301     public int getHeight() {
302         return mBottom;
303     }
304 
305     @Override
getLineCount()306     public int getLineCount() {
307         return 1;
308     }
309 
310     @Override
getLineTop(int line)311     public int getLineTop(int line) {
312         if (line == 0)
313             return 0;
314         else
315             return mBottom;
316     }
317 
318     @Override
getLineDescent(int line)319     public int getLineDescent(int line) {
320         return mDesc;
321     }
322 
323     @Override
getLineStart(int line)324     public int getLineStart(int line) {
325         if (line == 0)
326             return 0;
327         else
328             return getText().length();
329     }
330 
331     @Override
getParagraphDirection(int line)332     public int getParagraphDirection(int line) {
333         return DIR_LEFT_TO_RIGHT;
334     }
335 
336     @Override
getLineContainsTab(int line)337     public boolean getLineContainsTab(int line) {
338         return false;
339     }
340 
341     @Override
getLineMax(int line)342     public float getLineMax(int line) {
343         return mMax;
344     }
345 
346     @Override
getLineWidth(int line)347     public float getLineWidth(int line) {
348         return (line == 0 ? mMax : 0);
349     }
350 
351     @Override
getLineDirections(int line)352     public final Directions getLineDirections(int line) {
353         return Layout.DIRS_ALL_LEFT_TO_RIGHT;
354     }
355 
356     @Override
getTopPadding()357     public int getTopPadding() {
358         return mTopPadding;
359     }
360 
361     @Override
getBottomPadding()362     public int getBottomPadding() {
363         return mBottomPadding;
364     }
365 
366     @Override
getEllipsisCount(int line)367     public int getEllipsisCount(int line) {
368         return mEllipsizedCount;
369     }
370 
371     @Override
getEllipsisStart(int line)372     public int getEllipsisStart(int line) {
373         return mEllipsizedStart;
374     }
375 
376     @Override
getEllipsizedWidth()377     public int getEllipsizedWidth() {
378         return mEllipsizedWidth;
379     }
380 
381     // Override draw so it will be faster.
382     @Override
draw(Canvas c, Path highlight, Paint highlightpaint, int cursorOffset)383     public void draw(Canvas c, Path highlight, Paint highlightpaint,
384                      int cursorOffset) {
385         if (mDirect != null && highlight == null) {
386             c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
387         } else {
388             super.draw(c, highlight, highlightpaint, cursorOffset);
389         }
390     }
391 
392     /**
393      * Callback for the ellipsizer to report what region it ellipsized.
394      */
ellipsized(int start, int end)395     public void ellipsized(int start, int end) {
396         mEllipsizedStart = start;
397         mEllipsizedCount = end - start;
398     }
399 
400     private String mDirect;
401     private Paint mPaint;
402 
403     /* package */ int mBottom, mDesc;   // for Direct
404     private int mTopPadding, mBottomPadding;
405     private float mMax;
406     private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
407 
408     public static class Metrics extends Paint.FontMetricsInt {
409         public int width;
410 
toString()411         @Override public String toString() {
412             return super.toString() + " width=" + width;
413         }
414 
reset()415         private void reset() {
416             top = 0;
417             bottom = 0;
418             ascent = 0;
419             descent = 0;
420             width = 0;
421             leading = 0;
422         }
423     }
424 }
425