Styled.java revision 71b8dd71e49016e057c46a257f79162d186a3c3a
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 */
16package android.text;
17
18import android.graphics.Canvas;
19import android.graphics.Paint;
20import android.text.style.CharacterStyle;
21import android.text.style.MetricAffectingSpan;
22import android.text.style.ReplacementSpan;
23
24/**
25 * This class provides static methods for drawing and measuring styled text,
26 * like {@link android.text.Spanned} object with
27 * {@link android.text.style.ReplacementSpan}.
28 *
29 * @hide
30 */
31public class Styled
32{
33    /**
34     * Draws and/or measures a uniform run of text on a single line. No span of
35     * interest should start or end in the middle of this run (if not
36     * drawing, character spans that don't affect metrics can be ignored).
37     * Neither should the run direction change in the middle of the run.
38     *
39     * <p>The x position is the leading edge of the text. In a right-to-left
40     * paragraph, this will be to the right of the text to be drawn. Paint
41     * should not have an Align value other than LEFT or positioning will get
42     * confused.
43     *
44     * <p>On return, workPaint will reflect the original paint plus any
45     * modifications made by character styles on the run.
46     *
47     * <p>The returned width is signed and will be < 0 if the paragraph
48     * direction is right-to-left.
49     */
50    private static float drawUniformRun(Canvas canvas,
51                              Spanned text, int start, int end,
52                              int dir, boolean runIsRtl,
53                              float x, int top, int y, int bottom,
54                              Paint.FontMetricsInt fmi,
55                              TextPaint paint,
56                              TextPaint workPaint,
57                              boolean needWidth) {
58
59        boolean haveWidth = false;
60        float ret = 0;
61        CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class);
62
63        ReplacementSpan replacement = null;
64
65        // XXX: This shouldn't be modifying paint, only workPaint.
66        // However, the members belonging to TextPaint should have default
67        // values anyway.  Better to ensure this in the Layout constructor.
68        paint.bgColor = 0;
69        paint.baselineShift = 0;
70        workPaint.set(paint);
71
72		if (spans.length > 0) {
73			for (int i = 0; i < spans.length; i++) {
74				CharacterStyle span = spans[i];
75
76				if (span instanceof ReplacementSpan) {
77					replacement = (ReplacementSpan)span;
78				}
79				else {
80					span.updateDrawState(workPaint);
81				}
82			}
83		}
84
85        if (replacement == null) {
86            CharSequence tmp;
87            int tmpstart, tmpend;
88
89            if (runIsRtl) {
90                tmp = TextUtils.getReverse(text, start, end);
91                tmpstart = 0;
92                // XXX: assumes getReverse doesn't change the length of the text
93                tmpend = end - start;
94            } else {
95                tmp = text;
96                tmpstart = start;
97                tmpend = end;
98            }
99
100            if (fmi != null) {
101                workPaint.getFontMetricsInt(fmi);
102            }
103
104            if (canvas != null) {
105                if (workPaint.bgColor != 0) {
106                    int c = workPaint.getColor();
107                    Paint.Style s = workPaint.getStyle();
108                    workPaint.setColor(workPaint.bgColor);
109                    workPaint.setStyle(Paint.Style.FILL);
110
111                    if (!haveWidth) {
112                        ret = workPaint.measureText(tmp, tmpstart, tmpend);
113                        haveWidth = true;
114                    }
115
116                    if (dir == Layout.DIR_RIGHT_TO_LEFT)
117                        canvas.drawRect(x - ret, top, x, bottom, workPaint);
118                    else
119                        canvas.drawRect(x, top, x + ret, bottom, workPaint);
120
121                    workPaint.setStyle(s);
122                    workPaint.setColor(c);
123                }
124
125                if (dir == Layout.DIR_RIGHT_TO_LEFT) {
126                    if (!haveWidth) {
127                        ret = workPaint.measureText(tmp, tmpstart, tmpend);
128                        haveWidth = true;
129                    }
130
131                    canvas.drawText(tmp, tmpstart, tmpend,
132                                    x - ret, y + workPaint.baselineShift, workPaint);
133                } else {
134                    if (needWidth) {
135                        if (!haveWidth) {
136                            ret = workPaint.measureText(tmp, tmpstart, tmpend);
137                            haveWidth = true;
138                        }
139                    }
140
141                    canvas.drawText(tmp, tmpstart, tmpend,
142                                    x, y + workPaint.baselineShift, workPaint);
143                }
144            } else {
145                if (needWidth && !haveWidth) {
146                    ret = workPaint.measureText(tmp, tmpstart, tmpend);
147                    haveWidth = true;
148                }
149            }
150        } else {
151            ret = replacement.getSize(workPaint, text, start, end, fmi);
152
153            if (canvas != null) {
154                if (dir == Layout.DIR_RIGHT_TO_LEFT)
155                    replacement.draw(canvas, text, start, end,
156                                     x - ret, top, y, bottom, workPaint);
157                else
158                    replacement.draw(canvas, text, start, end,
159                                     x, top, y, bottom, workPaint);
160            }
161        }
162
163        if (dir == Layout.DIR_RIGHT_TO_LEFT)
164            return -ret;
165        else
166            return ret;
167    }
168
169    /**
170     * Returns the advance widths for a uniform left-to-right run of text with
171     * no style changes in the middle of the run. If any style is replacement
172     * text, the first character will get the width of the replacement and the
173     * remaining characters will get a width of 0.
174     *
175     * @param paint the paint, will not be modified
176     * @param workPaint a paint to modify; on return will reflect the original
177     *        paint plus the effect of all spans on the run
178     * @param text the text
179     * @param start the start of the run
180     * @param end the limit of the run
181     * @param widths array to receive the advance widths of the characters. Must
182     *        be at least a large as (end - start).
183     * @param fmi FontMetrics information; can be null
184     * @return the actual number of widths returned
185     */
186    public static int getTextWidths(TextPaint paint,
187                                    TextPaint workPaint,
188                                    Spanned text, int start, int end,
189                                    float[] widths, Paint.FontMetricsInt fmi) {
190        MetricAffectingSpan[] spans =
191            text.getSpans(start, end, MetricAffectingSpan.class);
192
193		ReplacementSpan replacement = null;
194        workPaint.set(paint);
195
196		for (int i = 0; i < spans.length; i++) {
197			MetricAffectingSpan span = spans[i];
198			if (span instanceof ReplacementSpan) {
199				replacement = (ReplacementSpan)span;
200			}
201			else {
202				span.updateMeasureState(workPaint);
203			}
204		}
205
206        if (replacement == null) {
207            workPaint.getFontMetricsInt(fmi);
208            workPaint.getTextWidths(text, start, end, widths);
209        } else {
210            int wid = replacement.getSize(workPaint, text, start, end, fmi);
211
212            if (end > start) {
213                widths[0] = wid;
214                for (int i = start + 1; i < end; i++)
215                    widths[i - start] = 0;
216            }
217        }
218        return end - start;
219    }
220
221    /**
222     * Renders and/or measures a directional run of text on a single line.
223     * Unlike {@link #drawUniformRun}, this can render runs that cross style
224     * boundaries.  Returns the signed advance width, if requested.
225     *
226     * <p>The x position is the leading edge of the text. In a right-to-left
227     * paragraph, this will be to the right of the text to be drawn. Paint
228     * should not have an Align value other than LEFT or positioning will get
229     * confused.
230     *
231     * <p>This optimizes for unstyled text and so workPaint might not be
232     * modified by this call.
233     *
234     * <p>The returned advance width will be < 0 if the paragraph
235     * direction is right-to-left.
236     */
237    private static float drawDirectionalRun(Canvas canvas,
238                                 CharSequence text, int start, int end,
239                                 int dir, boolean runIsRtl,
240                                 float x, int top, int y, int bottom,
241                                 Paint.FontMetricsInt fmi,
242                                 TextPaint paint,
243                                 TextPaint workPaint,
244                                 boolean needWidth) {
245
246        // XXX: It looks like all calls to this API match dir and runIsRtl, so
247        // having both parameters is redundant and confusing.
248
249        // fast path for unstyled text
250        if (!(text instanceof Spanned)) {
251            float ret = 0;
252
253            if (runIsRtl) {
254                CharSequence tmp = TextUtils.getReverse(text, start, end);
255                // XXX: this assumes getReverse doesn't tweak the length of
256                // the text
257                int tmpend = end - start;
258
259                if (canvas != null || needWidth)
260                    ret = paint.measureText(tmp, 0, tmpend);
261
262                if (canvas != null)
263                    canvas.drawText(tmp, 0, tmpend,
264                                    x - ret, y, paint);
265            } else {
266                if (needWidth)
267                    ret = paint.measureText(text, start, end);
268
269                if (canvas != null)
270                    canvas.drawText(text, start, end, x, y, paint);
271            }
272
273            if (fmi != null) {
274                paint.getFontMetricsInt(fmi);
275            }
276
277            return ret * dir;   // Layout.DIR_RIGHT_TO_LEFT == -1
278        }
279
280        float ox = x;
281        int minAscent = 0, maxDescent = 0, minTop = 0, maxBottom = 0;
282
283        Spanned sp = (Spanned) text;
284        Class<?> division;
285
286        if (canvas == null)
287            division = MetricAffectingSpan.class;
288        else
289            division = CharacterStyle.class;
290
291        int next;
292        for (int i = start; i < end; i = next) {
293            next = sp.nextSpanTransition(i, end, division);
294
295            // XXX: if dir and runIsRtl were not the same, this would draw
296            // spans in the wrong order, but no one appears to call it this
297            // way.
298            x += drawUniformRun(canvas, sp, i, next, dir, runIsRtl,
299                  x, top, y, bottom, fmi, paint, workPaint,
300                  needWidth || next != end);
301
302            if (fmi != null) {
303                if (fmi.ascent < minAscent)
304                    minAscent = fmi.ascent;
305                if (fmi.descent > maxDescent)
306                    maxDescent = fmi.descent;
307
308                if (fmi.top < minTop)
309                    minTop = fmi.top;
310                if (fmi.bottom > maxBottom)
311                    maxBottom = fmi.bottom;
312            }
313        }
314
315        if (fmi != null) {
316            if (start == end) {
317                paint.getFontMetricsInt(fmi);
318            } else {
319                fmi.ascent = minAscent;
320                fmi.descent = maxDescent;
321                fmi.top = minTop;
322                fmi.bottom = maxBottom;
323            }
324        }
325
326        return x - ox;
327    }
328
329    /**
330     * Draws a unidirectional run of text on a single line, and optionally
331     * returns the signed advance.  Unlike drawDirectionalRun, the paragraph
332     * direction and run direction can be different.
333     */
334    /* package */ static float drawText(Canvas canvas,
335                                       CharSequence text, int start, int end,
336                                       int dir, boolean runIsRtl,
337                                       float x, int top, int y, int bottom,
338                                       TextPaint paint,
339                                       TextPaint workPaint,
340                                       boolean needWidth) {
341        // XXX this logic is (dir == DIR_LEFT_TO_RIGHT) == runIsRtl
342        if ((dir == Layout.DIR_RIGHT_TO_LEFT && !runIsRtl) ||
343            (runIsRtl && dir == Layout.DIR_LEFT_TO_RIGHT)) {
344            // TODO: this needs the real direction
345            float ch = drawDirectionalRun(null, text, start, end,
346                    Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, null, paint,
347                    workPaint, true);
348
349            ch *= dir;  // DIR_RIGHT_TO_LEFT == -1
350            drawDirectionalRun(canvas, text, start, end, -dir,
351                    runIsRtl, x + ch, top, y, bottom, null, paint,
352                    workPaint, true);
353
354            return ch;
355        }
356
357        return drawDirectionalRun(canvas, text, start, end, dir, runIsRtl,
358                       x, top, y, bottom, null, paint, workPaint,
359                       needWidth);
360    }
361
362    /**
363     * Draws a run of text on a single line, with its
364     * origin at (x,y), in the specified Paint. The origin is interpreted based
365     * on the Align setting in the Paint.
366     *
367     * This method considers style information in the text (e.g. even when text
368     * is an instance of {@link android.text.Spanned}, this method correctly
369     * draws the text). See also
370     * {@link android.graphics.Canvas#drawText(CharSequence, int, int, float,
371     * float, Paint)} and
372     * {@link android.graphics.Canvas#drawRect(float, float, float, float,
373     * Paint)}.
374     *
375     * @param canvas The target canvas
376     * @param text The text to be drawn
377     * @param start The index of the first character in text to draw
378     * @param end (end - 1) is the index of the last character in text to draw
379     * @param direction The direction of the text. This must be
380     *        {@link android.text.Layout#DIR_LEFT_TO_RIGHT} or
381     *        {@link android.text.Layout#DIR_RIGHT_TO_LEFT}.
382     * @param x The x-coordinate of origin for where to draw the text
383     * @param top The top side of the rectangle to be drawn
384     * @param y The y-coordinate of origin for where to draw the text
385     * @param bottom The bottom side of the rectangle to be drawn
386     * @param paint The main {@link TextPaint} object.
387     * @param workPaint The {@link TextPaint} object used for temporal
388     *        workspace.
389     * @param needWidth If true, this method returns the width of drawn text
390     * @return Width of the drawn text if needWidth is true
391     */
392    public static float drawText(Canvas canvas,
393                                 CharSequence text, int start, int end,
394                                 int direction,
395                                 float x, int top, int y, int bottom,
396                                 TextPaint paint,
397                                 TextPaint workPaint,
398                                 boolean needWidth) {
399        // For safety.
400        direction = direction >= 0 ? Layout.DIR_LEFT_TO_RIGHT
401                : Layout.DIR_RIGHT_TO_LEFT;
402
403        // Hide runIsRtl parameter since it is meaningless for external
404        // developers.
405        // XXX: the runIsRtl probably ought to be the same as direction, then
406        // this could draw rtl text.
407        return drawText(canvas, text, start, end, direction, false,
408                        x, top, y, bottom, paint, workPaint, needWidth);
409    }
410
411    /**
412     * Returns the width of a run of left-to-right text on a single line,
413     * considering style information in the text (e.g. even when text is an
414     * instance of {@link android.text.Spanned}, this method correctly measures
415     * the width of the text).
416     *
417     * @param paint the main {@link TextPaint} object; will not be modified
418     * @param workPaint the {@link TextPaint} object available for modification;
419     *        will not necessarily be used
420     * @param text the text to measure
421     * @param start the index of the first character to start measuring
422     * @param end 1 beyond the index of the last character to measure
423     * @param fmi FontMetrics information; can be null
424     * @return The width of the text
425     */
426    public static float measureText(TextPaint paint,
427                                    TextPaint workPaint,
428                                    CharSequence text, int start, int end,
429                                    Paint.FontMetricsInt fmi) {
430        return drawDirectionalRun(null, text, start, end,
431                       Layout.DIR_LEFT_TO_RIGHT, false,
432                       0, 0, 0, 0, fmi, paint, workPaint, true);
433    }
434}
435