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 package android.text; 17 18 import android.graphics.Canvas; 19 import android.graphics.Paint; 20 import android.text.style.CharacterStyle; 21 import android.text.style.MetricAffectingSpan; 22 import 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 */ 31 public 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 */ 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)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 */ getTextWidths(TextPaint paint, TextPaint workPaint, Spanned text, int start, int end, float[] widths, Paint.FontMetricsInt fmi)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 */ 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)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 */ 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)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 */ 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)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 */ measureText(TextPaint paint, TextPaint workPaint, CharSequence text, int start, int end, Paint.FontMetricsInt fmi)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