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