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