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        if (spacingmult != 1 || spacingadd != 0) {
192            spacing = (int)(spacing * spacingmult + spacingadd + 0.5f);
193        }
194
195        mBottom = spacing;
196
197        if (includepad) {
198            mDesc = spacing + metrics.top;
199        } else {
200            mDesc = spacing + metrics.ascent;
201        }
202
203        if (trustWidth) {
204            mMax = metrics.width;
205        } else {
206            /*
207             * If we have ellipsized, we have to actually calculate the
208             * width because the width that was passed in was for the
209             * full text, not the ellipsized form.
210             */
211            synchronized (sTemp) {
212                mMax = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp,
213                                                source, 0, source.length(),
214                                                null)));
215            }
216        }
217
218        if (includepad) {
219            mTopPadding = metrics.top - metrics.ascent;
220            mBottomPadding = metrics.bottom - metrics.descent;
221        }
222    }
223
224    /**
225     * Returns null if not boring; the width, ascent, and descent if boring.
226     */
227    public static Metrics isBoring(CharSequence text,
228                                   TextPaint paint) {
229        return isBoring(text, paint, null);
230    }
231
232    /**
233     * Returns null if not boring; the width, ascent, and descent in the
234     * provided Metrics object (or a new one if the provided one was null)
235     * if boring.
236     */
237    public static Metrics isBoring(CharSequence text, TextPaint paint,
238                                   Metrics metrics) {
239        char[] temp = TextUtils.obtain(500);
240        int len = text.length();
241        boolean boring = true;
242
243        outer:
244        for (int i = 0; i < len; i += 500) {
245            int j = i + 500;
246
247            if (j > len)
248                j = len;
249
250            TextUtils.getChars(text, i, j, temp, 0);
251
252            int n = j - i;
253
254            for (int a = 0; a < n; a++) {
255                char c = temp[a];
256
257                if (c == '\n' || c == '\t' || c >= FIRST_RIGHT_TO_LEFT) {
258                    boring = false;
259                    break outer;
260                }
261            }
262        }
263
264        TextUtils.recycle(temp);
265
266        if (boring && text instanceof Spanned) {
267            Spanned sp = (Spanned) text;
268            Object[] styles = sp.getSpans(0, text.length(), ParagraphStyle.class);
269            if (styles.length > 0) {
270                boring = false;
271            }
272        }
273
274        if (boring) {
275            Metrics fm = metrics;
276            if (fm == null) {
277                fm = new Metrics();
278            }
279
280            int wid;
281
282            synchronized (sTemp) {
283                wid = (int) (FloatMath.ceil(Styled.measureText(paint, sTemp,
284                                                text, 0, text.length(), fm)));
285            }
286            fm.width = wid;
287            return fm;
288        } else {
289            return null;
290        }
291    }
292
293    @Override public int getHeight() {
294        return mBottom;
295    }
296
297    @Override public int getLineCount() {
298        return 1;
299    }
300
301    @Override public int getLineTop(int line) {
302        if (line == 0)
303            return 0;
304        else
305            return mBottom;
306    }
307
308    @Override public int getLineDescent(int line) {
309        return mDesc;
310    }
311
312    @Override public int getLineStart(int line) {
313        if (line == 0)
314            return 0;
315        else
316            return getText().length();
317    }
318
319    @Override public int getParagraphDirection(int line) {
320        return DIR_LEFT_TO_RIGHT;
321    }
322
323    @Override public boolean getLineContainsTab(int line) {
324        return false;
325    }
326
327    @Override public float getLineMax(int line) {
328        return mMax;
329    }
330
331    @Override public final Directions getLineDirections(int line) {
332        return Layout.DIRS_ALL_LEFT_TO_RIGHT;
333    }
334
335    public int getTopPadding() {
336        return mTopPadding;
337    }
338
339    public int getBottomPadding() {
340        return mBottomPadding;
341    }
342
343    @Override
344    public int getEllipsisCount(int line) {
345        return mEllipsizedCount;
346    }
347
348    @Override
349    public int getEllipsisStart(int line) {
350        return mEllipsizedStart;
351    }
352
353    @Override
354    public int getEllipsizedWidth() {
355        return mEllipsizedWidth;
356    }
357
358    // Override draw so it will be faster.
359    @Override
360    public void draw(Canvas c, Path highlight, Paint highlightpaint,
361                     int cursorOffset) {
362        if (mDirect != null && highlight == null) {
363            c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
364        } else {
365            super.draw(c, highlight, highlightpaint, cursorOffset);
366        }
367    }
368
369    /**
370     * Callback for the ellipsizer to report what region it ellipsized.
371     */
372    public void ellipsized(int start, int end) {
373        mEllipsizedStart = start;
374        mEllipsizedCount = end - start;
375    }
376
377    private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
378
379    private String mDirect;
380    private Paint mPaint;
381
382    /* package */ int mBottom, mDesc;   // for Direct
383    private int mTopPadding, mBottomPadding;
384    private float mMax;
385    private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
386
387    private static final TextPaint sTemp =
388                                new TextPaint();
389
390    public static class Metrics extends Paint.FontMetricsInt {
391        public int width;
392
393        @Override public String toString() {
394            return super.toString() + " width=" + width;
395        }
396    }
397}
398