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            TextLine line = TextLine.obtain();
212            line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
213                    Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
214            mMax = (int) FloatMath.ceil(line.metrics(null));
215            TextLine.recycle(line);
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, TextDirectionHeuristics.FIRSTSTRONG_LTR, null);
230    }
231
232    /**
233     * Returns null if not boring; the width, ascent, and descent if boring.
234     * @hide
235     */
236    public static Metrics isBoring(CharSequence text,
237                                   TextPaint paint,
238                                   TextDirectionHeuristic textDir) {
239        return isBoring(text, paint, textDir, null);
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     */
247    public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) {
248        return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics);
249    }
250
251    /**
252     * Returns null if not boring; the width, ascent, and descent in the
253     * provided Metrics object (or a new one if the provided one was null)
254     * if boring.
255     * @hide
256     */
257    public static Metrics isBoring(CharSequence text, TextPaint paint,
258            TextDirectionHeuristic textDir, Metrics metrics) {
259        char[] temp = TextUtils.obtain(500);
260        int length = text.length();
261        boolean boring = true;
262
263        outer:
264        for (int i = 0; i < length; i += 500) {
265            int j = i + 500;
266
267            if (j > length)
268                j = length;
269
270            TextUtils.getChars(text, i, j, temp, 0);
271
272            int n = j - i;
273
274            for (int a = 0; a < n; a++) {
275                char c = temp[a];
276
277                if (c == '\n' || c == '\t' || c >= FIRST_RIGHT_TO_LEFT) {
278                    boring = false;
279                    break outer;
280                }
281            }
282
283            if (textDir != null && textDir.isRtl(temp, 0, n)) {
284               boring = false;
285               break outer;
286            }
287        }
288
289        TextUtils.recycle(temp);
290
291        if (boring && text instanceof Spanned) {
292            Spanned sp = (Spanned) text;
293            Object[] styles = sp.getSpans(0, length, 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            }
304
305            TextLine line = TextLine.obtain();
306            line.set(paint, text, 0, length, Layout.DIR_LEFT_TO_RIGHT,
307                    Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
308            fm.width = (int) FloatMath.ceil(line.metrics(fm));
309            TextLine.recycle(line);
310
311            return fm;
312        } else {
313            return null;
314        }
315    }
316
317    @Override
318    public int getHeight() {
319        return mBottom;
320    }
321
322    @Override
323    public int getLineCount() {
324        return 1;
325    }
326
327    @Override
328    public int getLineTop(int line) {
329        if (line == 0)
330            return 0;
331        else
332            return mBottom;
333    }
334
335    @Override
336    public int getLineDescent(int line) {
337        return mDesc;
338    }
339
340    @Override
341    public int getLineStart(int line) {
342        if (line == 0)
343            return 0;
344        else
345            return getText().length();
346    }
347
348    @Override
349    public int getParagraphDirection(int line) {
350        return DIR_LEFT_TO_RIGHT;
351    }
352
353    @Override
354    public boolean getLineContainsTab(int line) {
355        return false;
356    }
357
358    @Override
359    public float getLineMax(int line) {
360        return mMax;
361    }
362
363    @Override
364    public final Directions getLineDirections(int line) {
365        return Layout.DIRS_ALL_LEFT_TO_RIGHT;
366    }
367
368    @Override
369    public int getTopPadding() {
370        return mTopPadding;
371    }
372
373    @Override
374    public int getBottomPadding() {
375        return mBottomPadding;
376    }
377
378    @Override
379    public int getEllipsisCount(int line) {
380        return mEllipsizedCount;
381    }
382
383    @Override
384    public int getEllipsisStart(int line) {
385        return mEllipsizedStart;
386    }
387
388    @Override
389    public int getEllipsizedWidth() {
390        return mEllipsizedWidth;
391    }
392
393    // Override draw so it will be faster.
394    @Override
395    public void draw(Canvas c, Path highlight, Paint highlightpaint,
396                     int cursorOffset) {
397        if (mDirect != null && highlight == null) {
398            c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
399        } else {
400            super.draw(c, highlight, highlightpaint, cursorOffset);
401        }
402    }
403
404    /**
405     * Callback for the ellipsizer to report what region it ellipsized.
406     */
407    public void ellipsized(int start, int end) {
408        mEllipsizedStart = start;
409        mEllipsizedCount = end - start;
410    }
411
412    private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
413
414    private String mDirect;
415    private Paint mPaint;
416
417    /* package */ int mBottom, mDesc;   // for Direct
418    private int mTopPadding, mBottomPadding;
419    private float mMax;
420    private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
421
422    private static final TextPaint sTemp =
423                                new TextPaint();
424
425    public static class Metrics extends Paint.FontMetricsInt {
426        public int width;
427
428        @Override public String toString() {
429            return super.toString() + " width=" + width;
430        }
431    }
432}
433