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 if boring.
225     * @hide
226     */
227    public static Metrics isBoring(CharSequence text,
228                                   TextPaint paint,
229                                   TextDirectionHeuristic textDir) {
230        return isBoring(text, paint, textDir, null);
231    }
232
233    /**
234     * Returns null if not boring; the width, ascent, and descent in the
235     * provided Metrics object (or a new one if the provided one was null)
236     * if boring.
237     */
238    public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) {
239        return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics);
240    }
241
242    /**
243     * Returns null if not boring; the width, ascent, and descent in the
244     * provided Metrics object (or a new one if the provided one was null)
245     * if boring.
246     * @hide
247     */
248    public static Metrics isBoring(CharSequence text, TextPaint paint,
249            TextDirectionHeuristic textDir, Metrics metrics) {
250        final int MAX_BUF_LEN = 500;
251        final char[] buffer = TextUtils.obtain(MAX_BUF_LEN);
252        final int textLength = text.length();
253        boolean boring = true;
254
255        outer:
256        for (int start = 0; start < textLength; start += MAX_BUF_LEN) {
257            final int end = Math.min(start + MAX_BUF_LEN, textLength);
258
259            // No need to worry about getting half codepoints, since we reject surrogate code units
260            // as non-boring as soon we see one.
261            TextUtils.getChars(text, start, end, buffer, 0);
262
263            final int len = end - start;
264            for (int i = 0; i < len; i++) {
265                final char c = buffer[i];
266
267                if (c == '\n' || c == '\t' ||
268                        (c >= 0x0590 && c <= 0x08FF) ||  // RTL scripts
269                        c == 0x200F ||  // Bidi format character
270                        (c >= 0x202A && c <= 0x202E) ||  // Bidi format characters
271                        (c >= 0x2066 && c <= 0x2069) ||  // Bidi format characters
272                        (c >= 0xD800 && c <= 0xDFFF) ||  // surrogate pairs
273                        (c >= 0xFB1D && c <= 0xFDFF) ||  // Hebrew and Arabic presentation forms
274                        (c >= 0xFE70 && c <= 0xFEFE) // Arabic presentation forms
275                   ) {
276                    boring = false;
277                    break outer;
278                }
279            }
280
281            // TODO: This looks a little suspicious, and in some cases can result in O(n^2)
282            // run time. Consider moving outside the loop.
283            if (textDir != null && textDir.isRtl(buffer, 0, len)) {
284               boring = false;
285               break outer;
286            }
287        }
288
289        TextUtils.recycle(buffer);
290
291        if (boring && text instanceof Spanned) {
292            Spanned sp = (Spanned) text;
293            Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);
294            if (styles.length > 0) {
295                boring = false;
296            }
297        }
298
299        if (boring) {
300            Metrics fm = metrics;
301            if (fm == null) {
302                fm = new Metrics();
303            } else {
304                fm.reset();
305            }
306
307            TextLine line = TextLine.obtain();
308            line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
309                    Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
310            fm.width = (int) Math.ceil(line.metrics(fm));
311            TextLine.recycle(line);
312
313            return fm;
314        } else {
315            return null;
316        }
317    }
318
319    @Override
320    public int getHeight() {
321        return mBottom;
322    }
323
324    @Override
325    public int getLineCount() {
326        return 1;
327    }
328
329    @Override
330    public int getLineTop(int line) {
331        if (line == 0)
332            return 0;
333        else
334            return mBottom;
335    }
336
337    @Override
338    public int getLineDescent(int line) {
339        return mDesc;
340    }
341
342    @Override
343    public int getLineStart(int line) {
344        if (line == 0)
345            return 0;
346        else
347            return getText().length();
348    }
349
350    @Override
351    public int getParagraphDirection(int line) {
352        return DIR_LEFT_TO_RIGHT;
353    }
354
355    @Override
356    public boolean getLineContainsTab(int line) {
357        return false;
358    }
359
360    @Override
361    public float getLineMax(int line) {
362        return mMax;
363    }
364
365    @Override
366    public float getLineWidth(int line) {
367        return (line == 0 ? mMax : 0);
368    }
369
370    @Override
371    public final Directions getLineDirections(int line) {
372        return Layout.DIRS_ALL_LEFT_TO_RIGHT;
373    }
374
375    @Override
376    public int getTopPadding() {
377        return mTopPadding;
378    }
379
380    @Override
381    public int getBottomPadding() {
382        return mBottomPadding;
383    }
384
385    @Override
386    public int getEllipsisCount(int line) {
387        return mEllipsizedCount;
388    }
389
390    @Override
391    public int getEllipsisStart(int line) {
392        return mEllipsizedStart;
393    }
394
395    @Override
396    public int getEllipsizedWidth() {
397        return mEllipsizedWidth;
398    }
399
400    // Override draw so it will be faster.
401    @Override
402    public void draw(Canvas c, Path highlight, Paint highlightpaint,
403                     int cursorOffset) {
404        if (mDirect != null && highlight == null) {
405            c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
406        } else {
407            super.draw(c, highlight, highlightpaint, cursorOffset);
408        }
409    }
410
411    /**
412     * Callback for the ellipsizer to report what region it ellipsized.
413     */
414    public void ellipsized(int start, int end) {
415        mEllipsizedStart = start;
416        mEllipsizedCount = end - start;
417    }
418
419    private String mDirect;
420    private Paint mPaint;
421
422    /* package */ int mBottom, mDesc;   // for Direct
423    private int mTopPadding, mBottomPadding;
424    private float mMax;
425    private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
426
427    public static class Metrics extends Paint.FontMetricsInt {
428        public int width;
429
430        @Override public String toString() {
431            return super.toString() + " width=" + width;
432        }
433
434        private void reset() {
435            top = 0;
436            bottom = 0;
437            ascent = 0;
438            descent = 0;
439            width = 0;
440            leading = 0;
441        }
442    }
443}
444