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         } else {
187             spacing = metrics.descent - metrics.ascent;
188         }
189 
190         mBottom = spacing;
191 
192         if (includepad) {
193             mDesc = spacing + metrics.top;
194         } else {
195             mDesc = spacing + metrics.ascent;
196         }
197 
198         if (trustWidth) {
199             mMax = metrics.width;
200         } else {
201             /*
202              * If we have ellipsized, we have to actually calculate the
203              * width because the width that was passed in was for the
204              * full text, not the ellipsized form.
205              */
206             TextLine line = TextLine.obtain();
207             line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
208                     Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
209             mMax = (int) Math.ceil(line.metrics(null));
210             TextLine.recycle(line);
211         }
212 
213         if (includepad) {
214             mTopPadding = metrics.top - metrics.ascent;
215             mBottomPadding = metrics.bottom - metrics.descent;
216         }
217     }
218 
219     /**
220      * Returns null if not boring; the width, ascent, and descent if boring.
221      */
isBoring(CharSequence text, TextPaint paint)222     public static Metrics isBoring(CharSequence text,
223                                    TextPaint paint) {
224         return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null);
225     }
226 
227     /**
228      * Returns null if not boring; the width, ascent, and descent if boring.
229      * @hide
230      */
isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir)231     public static Metrics isBoring(CharSequence text,
232                                    TextPaint paint,
233                                    TextDirectionHeuristic textDir) {
234         return isBoring(text, paint, textDir, null);
235     }
236 
237     /**
238      * Returns null if not boring; the width, ascent, and descent in the
239      * provided Metrics object (or a new one if the provided one was null)
240      * if boring.
241      */
isBoring(CharSequence text, TextPaint paint, Metrics metrics)242     public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) {
243         return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics);
244     }
245 
246     /**
247      * Returns null if not boring; the width, ascent, and descent in the
248      * provided Metrics object (or a new one if the provided one was null)
249      * if boring.
250      * @hide
251      */
isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir, Metrics metrics)252     public static Metrics isBoring(CharSequence text, TextPaint paint,
253             TextDirectionHeuristic textDir, Metrics metrics) {
254         char[] temp = TextUtils.obtain(500);
255         int length = text.length();
256         boolean boring = true;
257 
258         outer:
259         for (int i = 0; i < length; i += 500) {
260             int j = i + 500;
261 
262             if (j > length)
263                 j = length;
264 
265             TextUtils.getChars(text, i, j, temp, 0);
266 
267             int n = j - i;
268 
269             for (int a = 0; a < n; a++) {
270                 char c = temp[a];
271 
272                 if (c == '\n' || c == '\t' ||
273                         (c >= 0x0590 && c <= 0x08FF) ||  // RTL scripts
274                         c == 0x200F ||  // Bidi format character
275                         (c >= 0x202A && c <= 0x202E) ||  // Bidi format characters
276                         (c >= 0x2066 && c <= 0x2069) ||  // Bidi format characters
277                         (c >= 0xD800 && c <= 0xDFFF) ||  // surrogate pairs
278                         (c >= 0xFB1D && c <= 0xFDFF) ||  // Hebrew and Arabic presentation forms
279                         (c >= 0xFE70 && c <= 0xFEFE) // Arabic presentation forms
280                    ) {
281                     boring = false;
282                     break outer;
283                 }
284             }
285 
286             if (textDir != null && textDir.isRtl(temp, 0, n)) {
287                boring = false;
288                break outer;
289             }
290         }
291 
292         TextUtils.recycle(temp);
293 
294         if (boring && text instanceof Spanned) {
295             Spanned sp = (Spanned) text;
296             Object[] styles = sp.getSpans(0, length, ParagraphStyle.class);
297             if (styles.length > 0) {
298                 boring = false;
299             }
300         }
301 
302         if (boring) {
303             Metrics fm = metrics;
304             if (fm == null) {
305                 fm = new Metrics();
306             }
307 
308             TextLine line = TextLine.obtain();
309             line.set(paint, text, 0, length, Layout.DIR_LEFT_TO_RIGHT,
310                     Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
311             fm.width = (int) Math.ceil(line.metrics(fm));
312             TextLine.recycle(line);
313 
314             return fm;
315         } else {
316             return null;
317         }
318     }
319 
320     @Override
getHeight()321     public int getHeight() {
322         return mBottom;
323     }
324 
325     @Override
getLineCount()326     public int getLineCount() {
327         return 1;
328     }
329 
330     @Override
getLineTop(int line)331     public int getLineTop(int line) {
332         if (line == 0)
333             return 0;
334         else
335             return mBottom;
336     }
337 
338     @Override
getLineDescent(int line)339     public int getLineDescent(int line) {
340         return mDesc;
341     }
342 
343     @Override
getLineStart(int line)344     public int getLineStart(int line) {
345         if (line == 0)
346             return 0;
347         else
348             return getText().length();
349     }
350 
351     @Override
getParagraphDirection(int line)352     public int getParagraphDirection(int line) {
353         return DIR_LEFT_TO_RIGHT;
354     }
355 
356     @Override
getLineContainsTab(int line)357     public boolean getLineContainsTab(int line) {
358         return false;
359     }
360 
361     @Override
getLineMax(int line)362     public float getLineMax(int line) {
363         return mMax;
364     }
365 
366     @Override
getLineDirections(int line)367     public final Directions getLineDirections(int line) {
368         return Layout.DIRS_ALL_LEFT_TO_RIGHT;
369     }
370 
371     @Override
getTopPadding()372     public int getTopPadding() {
373         return mTopPadding;
374     }
375 
376     @Override
getBottomPadding()377     public int getBottomPadding() {
378         return mBottomPadding;
379     }
380 
381     @Override
getEllipsisCount(int line)382     public int getEllipsisCount(int line) {
383         return mEllipsizedCount;
384     }
385 
386     @Override
getEllipsisStart(int line)387     public int getEllipsisStart(int line) {
388         return mEllipsizedStart;
389     }
390 
391     @Override
getEllipsizedWidth()392     public int getEllipsizedWidth() {
393         return mEllipsizedWidth;
394     }
395 
396     // Override draw so it will be faster.
397     @Override
draw(Canvas c, Path highlight, Paint highlightpaint, int cursorOffset)398     public void draw(Canvas c, Path highlight, Paint highlightpaint,
399                      int cursorOffset) {
400         if (mDirect != null && highlight == null) {
401             c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
402         } else {
403             super.draw(c, highlight, highlightpaint, cursorOffset);
404         }
405     }
406 
407     /**
408      * Callback for the ellipsizer to report what region it ellipsized.
409      */
ellipsized(int start, int end)410     public void ellipsized(int start, int end) {
411         mEllipsizedStart = start;
412         mEllipsizedCount = end - start;
413     }
414 
415     private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
416 
417     private String mDirect;
418     private Paint mPaint;
419 
420     /* package */ int mBottom, mDesc;   // for Direct
421     private int mTopPadding, mBottomPadding;
422     private float mMax;
423     private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
424 
425     private static final TextPaint sTemp =
426                                 new TextPaint();
427 
428     public static class Metrics extends Paint.FontMetricsInt {
429         public int width;
430 
toString()431         @Override public String toString() {
432             return super.toString() + " width=" + width;
433         }
434     }
435 }
436