/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jecelyin.android.v2.text; import android.graphics.Canvas; import android.graphics.Paint; import android.text.Spanned; import android.text.TextPaint; import android.text.style.CharacterStyle; import android.text.style.MetricAffectingSpan; import android.text.style.ReplacementSpan; /** * This class provides static methods for drawing and measuring styled text, * like {@link android.text.Spanned} object with * {@link android.text.style.ReplacementSpan}. * * @hide */ public class Styled { /** * Draws and/or measures a uniform run of text on a single line. No span of * interest should start or end in the middle of this run (if not * drawing, character spans that don't affect metrics can be ignored). * Neither should the run direction change in the middle of the run. * * <p>The x position is the leading edge of the text. In a right-to-left * paragraph, this will be to the right of the text to be drawn. Paint * should not have an Align value other than LEFT or positioning will get * confused. * * <p>On return, workPaint will reflect the original paint plus any * modifications made by character styles on the run. * * <p>The returned width is signed and will be < 0 if the paragraph * direction is right-to-left. */ private static float drawUniformRun(Canvas canvas, Spanned text, int start, int end, int dir, boolean runIsRtl, float x, int top, int y, int bottom, Paint.FontMetricsInt fmi, TextPaint paint, TextPaint workPaint, boolean needWidth) { boolean haveWidth = false; float ret = 0; CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class); ReplacementSpan replacement = null; // XXX: This shouldn't be modifying paint, only workPaint. // However, the members belonging to TextPaint should have default // values anyway. Better to ensure this in the Layout constructor. paint.bgColor = 0; paint.baselineShift = 0; workPaint.set(paint); if (spans.length > 0) { for (int i = 0; i < spans.length; i++) { CharacterStyle span = spans[i]; if (span instanceof ReplacementSpan) { replacement = (ReplacementSpan)span; } else { span.updateDrawState(workPaint); } } } if (replacement == null) { CharSequence tmp; int tmpstart, tmpend; if (runIsRtl) { tmp = TextUtils.getReverse(text, start, end); tmpstart = 0; // XXX: assumes getReverse doesn't change the length of the text tmpend = end - start; } else { tmp = text; tmpstart = start; tmpend = end; } if (fmi != null) { workPaint.getFontMetricsInt(fmi); } if (canvas != null) { if (workPaint.bgColor != 0) { int c = workPaint.getColor(); Paint.Style s = workPaint.getStyle(); workPaint.setColor(workPaint.bgColor); workPaint.setStyle(Paint.Style.FILL); if (!haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); haveWidth = true; } if (dir == Layout.DIR_RIGHT_TO_LEFT) canvas.drawRect(x - ret, top, x, bottom, workPaint); else canvas.drawRect(x, top, x + ret, bottom, workPaint); workPaint.setStyle(s); workPaint.setColor(c); } if (dir == Layout.DIR_RIGHT_TO_LEFT) { if (!haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); haveWidth = true; } canvas.drawText(tmp, tmpstart, tmpend, x - ret, y + workPaint.baselineShift, workPaint); } else { if (needWidth) { if (!haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); haveWidth = true; } } canvas.drawText(tmp, tmpstart, tmpend, x, y + workPaint.baselineShift, workPaint); } } else { if (needWidth && !haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); haveWidth = true; } } } else { ret = replacement.getSize(workPaint, text, start, end, fmi); if (canvas != null) { if (dir == Layout.DIR_RIGHT_TO_LEFT) replacement.draw(canvas, text, start, end, x - ret, top, y, bottom, workPaint); else replacement.draw(canvas, text, start, end, x, top, y, bottom, workPaint); } } if (dir == Layout.DIR_RIGHT_TO_LEFT) return -ret; else return ret; } /** * Returns the advance widths for a uniform left-to-right run of text with * no style changes in the middle of the run. If any style is replacement * text, the first character will get the width of the replacement and the * remaining characters will get a width of 0. * * @param paint the paint, will not be modified * @param workPaint a paint to modify; on return will reflect the original * paint plus the effect of all spans on the run * @param text the text * @param start the start of the run * @param end the limit of the run * @param widths array to receive the advance widths of the characters. Must * be at least a large as (end - start). * @param fmi FontMetrics information; can be null * @return the actual number of widths returned */ public static int getTextWidths(TextPaint paint, TextPaint workPaint, Spanned text, int start, int end, float[] widths, Paint.FontMetricsInt fmi) { MetricAffectingSpan[] spans = text.getSpans(start, end, MetricAffectingSpan.class); ReplacementSpan replacement = null; workPaint.set(paint); for (int i = 0; i < spans.length; i++) { MetricAffectingSpan span = spans[i]; if (span instanceof ReplacementSpan) { replacement = (ReplacementSpan)span; } else { span.updateMeasureState(workPaint); } } if (replacement == null) { workPaint.getFontMetricsInt(fmi); workPaint.getTextWidths(text, start, end, widths); } else { int wid = replacement.getSize(workPaint, text, start, end, fmi); if (end > start) { widths[0] = wid; for (int i = start + 1; i < end; i++) widths[i - start] = 0; } } return end - start; } /** * Renders and/or measures a directional run of text on a single line. * Unlike {@link #drawUniformRun}, this can render runs that cross style * boundaries. Returns the signed advance width, if requested. * * <p>The x position is the leading edge of the text. In a right-to-left * paragraph, this will be to the right of the text to be drawn. Paint * should not have an Align value other than LEFT or positioning will get * confused. * * <p>This optimizes for unstyled text and so workPaint might not be * modified by this call. * * <p>The returned advance width will be < 0 if the paragraph * direction is right-to-left. */ private static float drawDirectionalRun(Canvas canvas, CharSequence text, int start, int end, int dir, boolean runIsRtl, float x, int top, int y, int bottom, Paint.FontMetricsInt fmi, TextPaint paint, TextPaint workPaint, boolean needWidth) { // XXX: It looks like all calls to this API match dir and runIsRtl, so // having both parameters is redundant and confusing. // fast path for unstyled text if (!(text instanceof Spanned)) { float ret = 0; if (runIsRtl) { CharSequence tmp = TextUtils.getReverse(text, start, end); // XXX: this assumes getReverse doesn't tweak the length of // the text int tmpend = end - start; if (canvas != null || needWidth) ret = paint.measureText(tmp, 0, tmpend); if (canvas != null) canvas.drawText(tmp, 0, tmpend, x - ret, y, paint); } else { if (needWidth) ret = paint.measureText(text, start, end); if (canvas != null) canvas.drawText(text, start, end, x, y, paint); } if (fmi != null) { paint.getFontMetricsInt(fmi); } return ret * dir; // Layout.DIR_RIGHT_TO_LEFT == -1 } float ox = x; int minAscent = 0, maxDescent = 0, minTop = 0, maxBottom = 0; Spanned sp = (Spanned) text; Class<?> division; if (canvas == null) division = MetricAffectingSpan.class; else division = CharacterStyle.class; int next; for (int i = start; i < end; i = next) { next = sp.nextSpanTransition(i, end, division); // XXX: if dir and runIsRtl were not the same, this would draw // spans in the wrong order, but no one appears to call it this // way. x += drawUniformRun(canvas, sp, i, next, dir, runIsRtl, x, top, y, bottom, fmi, paint, workPaint, needWidth || next != end); if (fmi != null) { if (fmi.ascent < minAscent) minAscent = fmi.ascent; if (fmi.descent > maxDescent) maxDescent = fmi.descent; if (fmi.top < minTop) minTop = fmi.top; if (fmi.bottom > maxBottom) maxBottom = fmi.bottom; } } if (fmi != null) { if (start == end) { paint.getFontMetricsInt(fmi); } else { fmi.ascent = minAscent; fmi.descent = maxDescent; fmi.top = minTop; fmi.bottom = maxBottom; } } return x - ox; } /** * Draws a unidirectional run of text on a single line, and optionally * returns the signed advance. Unlike drawDirectionalRun, the paragraph * direction and run direction can be different. */ /* package */ static float drawText(Canvas canvas, CharSequence text, int start, int end, int dir, boolean runIsRtl, float x, int top, int y, int bottom, TextPaint paint, TextPaint workPaint, boolean needWidth) { // XXX this logic is (dir == DIR_LEFT_TO_RIGHT) == runIsRtl if ((dir == Layout.DIR_RIGHT_TO_LEFT && !runIsRtl) || (runIsRtl && dir == Layout.DIR_LEFT_TO_RIGHT)) { // TODO: this needs the real direction float ch = drawDirectionalRun(null, text, start, end, Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, null, paint, workPaint, true); ch *= dir; // DIR_RIGHT_TO_LEFT == -1 drawDirectionalRun(canvas, text, start, end, -dir, runIsRtl, x + ch, top, y, bottom, null, paint, workPaint, true); return ch; } return drawDirectionalRun(canvas, text, start, end, dir, runIsRtl, x, top, y, bottom, null, paint, workPaint, needWidth); } /** * Draws a run of text on a single line, with its * origin at (x,y), in the specified Paint. The origin is interpreted based * on the Align setting in the Paint. * * This method considers style information in the text (e.g. even when text * is an instance of {@link android.text.Spanned}, this method correctly * draws the text). See also * {@link android.graphics.Canvas#drawText(CharSequence, int, int, float, * float, Paint)} and * {@link android.graphics.Canvas#drawRect(float, float, float, float, * Paint)}. * * @param canvas The target canvas * @param text The text to be drawn * @param start The index of the first character in text to draw * @param end (end - 1) is the index of the last character in text to draw * @param direction The direction of the text. This must be * {@link android.text.Layout#DIR_LEFT_TO_RIGHT} or * {@link android.text.Layout#DIR_RIGHT_TO_LEFT}. * @param x The x-coordinate of origin for where to draw the text * @param top The top side of the rectangle to be drawn * @param y The y-coordinate of origin for where to draw the text * @param bottom The bottom side of the rectangle to be drawn * @param paint The main {@link TextPaint} object. * @param workPaint The {@link TextPaint} object used for temporal * workspace. * @param needWidth If true, this method returns the width of drawn text * @return Width of the drawn text if needWidth is true */ public static float drawText(Canvas canvas, CharSequence text, int start, int end, int direction, float x, int top, int y, int bottom, TextPaint paint, TextPaint workPaint, boolean needWidth) { // For safety. direction = direction >= 0 ? Layout.DIR_LEFT_TO_RIGHT : Layout.DIR_RIGHT_TO_LEFT; // Hide runIsRtl parameter since it is meaningless for external // developers. // XXX: the runIsRtl probably ought to be the same as direction, then // this could draw rtl text. return drawText(canvas, text, start, end, direction, false, x, top, y, bottom, paint, workPaint, needWidth); } /** * Returns the width of a run of left-to-right text on a single line, * considering style information in the text (e.g. even when text is an * instance of {@link android.text.Spanned}, this method correctly measures * the width of the text). * * @param paint the main {@link TextPaint} object; will not be modified * @param workPaint the {@link TextPaint} object available for modification; * will not necessarily be used * @param text the text to measure * @param start the index of the first character to start measuring * @param end 1 beyond the index of the last character to measure * @param fmi FontMetrics information; can be null * @return The width of the text */ public static float measureText(TextPaint paint, TextPaint workPaint, CharSequence text, int start, int end, Paint.FontMetricsInt fmi) { return drawDirectionalRun(null, text, start, end, Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, fmi, paint, workPaint, true); } }