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
17package android.text;
18
19import android.graphics.Canvas;
20import android.graphics.Paint;
21import android.graphics.Path;
22import 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 */
37public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback {
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
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     */
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     */
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
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
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
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     */
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     */
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     */
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     */
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
301    public int getHeight() {
302        return mBottom;
303    }
304
305    @Override
306    public int getLineCount() {
307        return 1;
308    }
309
310    @Override
311    public int getLineTop(int line) {
312        if (line == 0)
313            return 0;
314        else
315            return mBottom;
316    }
317
318    @Override
319    public int getLineDescent(int line) {
320        return mDesc;
321    }
322
323    @Override
324    public int getLineStart(int line) {
325        if (line == 0)
326            return 0;
327        else
328            return getText().length();
329    }
330
331    @Override
332    public int getParagraphDirection(int line) {
333        return DIR_LEFT_TO_RIGHT;
334    }
335
336    @Override
337    public boolean getLineContainsTab(int line) {
338        return false;
339    }
340
341    @Override
342    public float getLineMax(int line) {
343        return mMax;
344    }
345
346    @Override
347    public float getLineWidth(int line) {
348        return (line == 0 ? mMax : 0);
349    }
350
351    @Override
352    public final Directions getLineDirections(int line) {
353        return Layout.DIRS_ALL_LEFT_TO_RIGHT;
354    }
355
356    @Override
357    public int getTopPadding() {
358        return mTopPadding;
359    }
360
361    @Override
362    public int getBottomPadding() {
363        return mBottomPadding;
364    }
365
366    @Override
367    public int getEllipsisCount(int line) {
368        return mEllipsizedCount;
369    }
370
371    @Override
372    public int getEllipsisStart(int line) {
373        return mEllipsizedStart;
374    }
375
376    @Override
377    public int getEllipsizedWidth() {
378        return mEllipsizedWidth;
379    }
380
381    // Override draw so it will be faster.
382    @Override
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     */
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
411        @Override public String toString() {
412            return super.toString() + " width=" + width;
413        }
414
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