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;
23import android.util.FloatMath;
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 */
38public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback {
39    public static BoringLayout make(CharSequence source,
40                        TextPaint paint, int outerwidth,
41                        Alignment align,
42                        float spacingmult, float spacingadd,
43                        BoringLayout.Metrics metrics, boolean includepad) {
44        return new BoringLayout(source, paint, outerwidth, align,
45                                spacingmult, spacingadd, metrics,
46                                includepad);
47    }
48
49    public static BoringLayout make(CharSequence source,
50                        TextPaint paint, int outerwidth,
51                        Alignment align,
52                        float spacingmult, float spacingadd,
53                        BoringLayout.Metrics metrics, boolean includepad,
54                        TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
55        return new BoringLayout(source, paint, outerwidth, align,
56                                spacingmult, spacingadd, metrics,
57                                includepad, ellipsize, ellipsizedWidth);
58    }
59
60    /**
61     * Returns a BoringLayout for the specified text, potentially reusing
62     * this one if it is already suitable.  The caller must make sure that
63     * no one is still using this Layout.
64     */
65    public BoringLayout replaceOrMake(CharSequence source, TextPaint paint,
66                                      int outerwidth, Alignment align,
67                                      float spacingmult, float spacingadd,
68                                      BoringLayout.Metrics metrics,
69                                      boolean includepad) {
70        replaceWith(source, paint, outerwidth, align, spacingmult,
71                    spacingadd);
72
73        mEllipsizedWidth = outerwidth;
74        mEllipsizedStart = 0;
75        mEllipsizedCount = 0;
76
77        init(source, paint, outerwidth, align, spacingmult, spacingadd,
78             metrics, includepad, true);
79        return this;
80    }
81
82    /**
83     * Returns a BoringLayout for the specified text, potentially reusing
84     * this one if it is already suitable.  The caller must make sure that
85     * no one is still using this Layout.
86     */
87    public BoringLayout replaceOrMake(CharSequence source, TextPaint paint,
88                                      int outerwidth, Alignment align,
89                                      float spacingmult, float spacingadd,
90                                      BoringLayout.Metrics metrics,
91                                      boolean includepad,
92                                      TextUtils.TruncateAt ellipsize,
93                                      int ellipsizedWidth) {
94        boolean trust;
95
96        if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
97            replaceWith(source, paint, outerwidth, align, spacingmult,
98                        spacingadd);
99
100            mEllipsizedWidth = outerwidth;
101            mEllipsizedStart = 0;
102            mEllipsizedCount = 0;
103            trust = true;
104        } else {
105            replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
106                                           ellipsize, true, this),
107                        paint, outerwidth, align, spacingmult,
108                        spacingadd);
109
110            mEllipsizedWidth = ellipsizedWidth;
111            trust = false;
112        }
113
114        init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
115             metrics, includepad, trust);
116        return this;
117    }
118
119    public BoringLayout(CharSequence source,
120                        TextPaint paint, int outerwidth,
121                        Alignment align,
122                        float spacingmult, float spacingadd,
123                        BoringLayout.Metrics metrics, boolean includepad) {
124        super(source, paint, outerwidth, align, spacingmult, spacingadd);
125
126        mEllipsizedWidth = outerwidth;
127        mEllipsizedStart = 0;
128        mEllipsizedCount = 0;
129
130        init(source, paint, outerwidth, align, spacingmult, spacingadd,
131             metrics, includepad, true);
132    }
133
134    public BoringLayout(CharSequence source,
135                        TextPaint paint, int outerwidth,
136                        Alignment align,
137                        float spacingmult, float spacingadd,
138                        BoringLayout.Metrics metrics, boolean includepad,
139                        TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
140        /*
141         * It is silly to have to call super() and then replaceWith(),
142         * but we can't use "this" for the callback until the call to
143         * super() finishes.
144         */
145        super(source, paint, outerwidth, align, spacingmult, spacingadd);
146
147        boolean trust;
148
149        if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
150            mEllipsizedWidth = outerwidth;
151            mEllipsizedStart = 0;
152            mEllipsizedCount = 0;
153            trust = true;
154        } else {
155            replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth,
156                                           ellipsize, true, this),
157                        paint, outerwidth, align, spacingmult,
158                        spacingadd);
159
160
161            mEllipsizedWidth = ellipsizedWidth;
162            trust = false;
163        }
164
165        init(getText(), paint, outerwidth, align, spacingmult, spacingadd,
166             metrics, includepad, trust);
167    }
168
169    /* package */ void init(CharSequence source,
170                            TextPaint paint, int outerwidth,
171                            Alignment align,
172                            float spacingmult, float spacingadd,
173                            BoringLayout.Metrics metrics, boolean includepad,
174                            boolean trustWidth) {
175        int spacing;
176
177        if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {
178            mDirect = source.toString();
179        } else {
180            mDirect = null;
181        }
182
183        mPaint = paint;
184
185        if (includepad) {
186            spacing = metrics.bottom - metrics.top;
187        } else {
188            spacing = metrics.descent - metrics.ascent;
189        }
190
191        mBottom = spacing;
192
193        if (includepad) {
194            mDesc = spacing + metrics.top;
195        } else {
196            mDesc = spacing + metrics.ascent;
197        }
198
199        if (trustWidth) {
200            mMax = metrics.width;
201        } else {
202            /*
203             * If we have ellipsized, we have to actually calculate the
204             * width because the width that was passed in was for the
205             * full text, not the ellipsized form.
206             */
207            TextLine line = TextLine.obtain();
208            line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
209                    Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
210            mMax = (int) FloatMath.ceil(line.metrics(null));
211            TextLine.recycle(line);
212        }
213
214        if (includepad) {
215            mTopPadding = metrics.top - metrics.ascent;
216            mBottomPadding = metrics.bottom - metrics.descent;
217        }
218    }
219
220    /**
221     * Returns null if not boring; the width, ascent, and descent if boring.
222     */
223    public static Metrics isBoring(CharSequence text,
224                                   TextPaint paint) {
225        return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null);
226    }
227
228    /**
229     * Returns null if not boring; the width, ascent, and descent if boring.
230     * @hide
231     */
232    public static Metrics isBoring(CharSequence text,
233                                   TextPaint paint,
234                                   TextDirectionHeuristic textDir) {
235        return isBoring(text, paint, textDir, null);
236    }
237
238    /**
239     * Returns null if not boring; the width, ascent, and descent in the
240     * provided Metrics object (or a new one if the provided one was null)
241     * if boring.
242     */
243    public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) {
244        return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics);
245    }
246
247    /**
248     * Returns null if not boring; the width, ascent, and descent in the
249     * provided Metrics object (or a new one if the provided one was null)
250     * if boring.
251     * @hide
252     */
253    public static Metrics isBoring(CharSequence text, TextPaint paint,
254            TextDirectionHeuristic textDir, Metrics metrics) {
255        char[] temp = TextUtils.obtain(500);
256        int length = text.length();
257        boolean boring = true;
258
259        outer:
260        for (int i = 0; i < length; i += 500) {
261            int j = i + 500;
262
263            if (j > length)
264                j = length;
265
266            TextUtils.getChars(text, i, j, temp, 0);
267
268            int n = j - i;
269
270            for (int a = 0; a < n; a++) {
271                char c = temp[a];
272
273                if (c == '\n' || c == '\t' || c >= FIRST_RIGHT_TO_LEFT) {
274                    boring = false;
275                    break outer;
276                }
277            }
278
279            if (textDir != null && textDir.isRtl(temp, 0, n)) {
280               boring = false;
281               break outer;
282            }
283        }
284
285        TextUtils.recycle(temp);
286
287        if (boring && text instanceof Spanned) {
288            Spanned sp = (Spanned) text;
289            Object[] styles = sp.getSpans(0, length, ParagraphStyle.class);
290            if (styles.length > 0) {
291                boring = false;
292            }
293        }
294
295        if (boring) {
296            Metrics fm = metrics;
297            if (fm == null) {
298                fm = new Metrics();
299            }
300
301            TextLine line = TextLine.obtain();
302            line.set(paint, text, 0, length, Layout.DIR_LEFT_TO_RIGHT,
303                    Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
304            fm.width = (int) FloatMath.ceil(line.metrics(fm));
305            TextLine.recycle(line);
306
307            return fm;
308        } else {
309            return null;
310        }
311    }
312
313    @Override
314    public int getHeight() {
315        return mBottom;
316    }
317
318    @Override
319    public int getLineCount() {
320        return 1;
321    }
322
323    @Override
324    public int getLineTop(int line) {
325        if (line == 0)
326            return 0;
327        else
328            return mBottom;
329    }
330
331    @Override
332    public int getLineDescent(int line) {
333        return mDesc;
334    }
335
336    @Override
337    public int getLineStart(int line) {
338        if (line == 0)
339            return 0;
340        else
341            return getText().length();
342    }
343
344    @Override
345    public int getParagraphDirection(int line) {
346        return DIR_LEFT_TO_RIGHT;
347    }
348
349    @Override
350    public boolean getLineContainsTab(int line) {
351        return false;
352    }
353
354    @Override
355    public float getLineMax(int line) {
356        return mMax;
357    }
358
359    @Override
360    public final Directions getLineDirections(int line) {
361        return Layout.DIRS_ALL_LEFT_TO_RIGHT;
362    }
363
364    @Override
365    public int getTopPadding() {
366        return mTopPadding;
367    }
368
369    @Override
370    public int getBottomPadding() {
371        return mBottomPadding;
372    }
373
374    @Override
375    public int getEllipsisCount(int line) {
376        return mEllipsizedCount;
377    }
378
379    @Override
380    public int getEllipsisStart(int line) {
381        return mEllipsizedStart;
382    }
383
384    @Override
385    public int getEllipsizedWidth() {
386        return mEllipsizedWidth;
387    }
388
389    // Override draw so it will be faster.
390    @Override
391    public void draw(Canvas c, Path highlight, Paint highlightpaint,
392                     int cursorOffset) {
393        if (mDirect != null && highlight == null) {
394            c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
395        } else {
396            super.draw(c, highlight, highlightpaint, cursorOffset);
397        }
398    }
399
400    /**
401     * Callback for the ellipsizer to report what region it ellipsized.
402     */
403    public void ellipsized(int start, int end) {
404        mEllipsizedStart = start;
405        mEllipsizedCount = end - start;
406    }
407
408    private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
409
410    private String mDirect;
411    private Paint mPaint;
412
413    /* package */ int mBottom, mDesc;   // for Direct
414    private int mTopPadding, mBottomPadding;
415    private float mMax;
416    private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
417
418    private static final TextPaint sTemp =
419                                new TextPaint();
420
421    public static class Metrics extends Paint.FontMetricsInt {
422        public int width;
423
424        @Override public String toString() {
425            return super.toString() + " width=" + width;
426        }
427    }
428}
429