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 17 package android.text; 18 19 import android.graphics.Canvas; 20 import android.graphics.Paint; 21 import android.graphics.Path; 22 import android.text.style.ParagraphStyle; 23 24 /** 25 * A BoringLayout is a very simple Layout implementation for text that 26 * fits on a single line and is all left-to-right characters. 27 * You will probably never want to make one of these yourself; 28 * if you do, be sure to call {@link #isBoring} first to make sure 29 * the text meets the criteria. 30 * <p>This class is used by widgets to control text layout. You should not need 31 * to use this class directly unless you are implementing your own widget 32 * or custom display object, in which case 33 * you are encouraged to use a Layout instead of calling 34 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) 35 * Canvas.drawText()} directly.</p> 36 */ 37 public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback { 38 39 /** 40 * Utility function to construct a BoringLayout instance. 41 * 42 * @param source the text to render 43 * @param paint the default paint for the layout 44 * @param outerWidth the wrapping width for the text 45 * @param align whether to left, right, or center the text 46 * @param spacingMult this value is no longer used by BoringLayout 47 * @param spacingAdd this value is no longer used by BoringLayout 48 * @param metrics {@code #Metrics} instance that contains information about FontMetrics and 49 * line width 50 * @param includePad set whether to include extra space beyond font ascent and descent which is 51 * needed to avoid clipping in some scripts 52 */ 53 public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth, 54 Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, 55 boolean includePad) { 56 return new BoringLayout(source, paint, outerWidth, align, spacingMult, spacingAdd, metrics, 57 includePad); 58 } 59 60 /** 61 * Utility function to construct a BoringLayout instance. 62 * 63 * @param source the text to render 64 * @param paint the default paint for the layout 65 * @param outerWidth the wrapping width for the text 66 * @param align whether to left, right, or center the text 67 * @param spacingmult this value is no longer used by BoringLayout 68 * @param spacingadd this value is no longer used by BoringLayout 69 * @param metrics {@code #Metrics} instance that contains information about FontMetrics and 70 * line width 71 * @param includePad set whether to include extra space beyond font ascent and descent which is 72 * needed to avoid clipping in some scripts 73 * @param ellipsize whether to ellipsize the text if width of the text is longer than the 74 * requested width 75 * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is 76 * {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is 77 * not used, {@code outerWidth} is used instead 78 */ 79 public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth, 80 Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, 81 boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 82 return new BoringLayout(source, paint, outerWidth, align, spacingmult, spacingadd, metrics, 83 includePad, ellipsize, ellipsizedWidth); 84 } 85 86 /** 87 * Returns a BoringLayout for the specified text, potentially reusing 88 * this one if it is already suitable. The caller must make sure that 89 * no one is still using this Layout. 90 * 91 * @param source the text to render 92 * @param paint the default paint for the layout 93 * @param outerwidth the wrapping width for the text 94 * @param align whether to left, right, or center the text 95 * @param spacingMult this value is no longer used by BoringLayout 96 * @param spacingAdd this value is no longer used by BoringLayout 97 * @param metrics {@code #Metrics} instance that contains information about FontMetrics and 98 * line width 99 * @param includePad set whether to include extra space beyond font ascent and descent which is 100 * needed to avoid clipping in some scripts 101 */ 102 public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerwidth, 103 Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, 104 boolean includePad) { 105 replaceWith(source, paint, outerwidth, align, spacingMult, spacingAdd); 106 107 mEllipsizedWidth = outerwidth; 108 mEllipsizedStart = 0; 109 mEllipsizedCount = 0; 110 111 init(source, paint, align, metrics, includePad, true); 112 return this; 113 } 114 115 /** 116 * Returns a BoringLayout for the specified text, potentially reusing 117 * this one if it is already suitable. The caller must make sure that 118 * no one is still using this Layout. 119 * 120 * @param source the text to render 121 * @param paint the default paint for the layout 122 * @param outerWidth the wrapping width for the text 123 * @param align whether to left, right, or center the text 124 * @param spacingMult this value is no longer used by BoringLayout 125 * @param spacingAdd this value is no longer used by BoringLayout 126 * @param metrics {@code #Metrics} instance that contains information about FontMetrics and 127 * line width 128 * @param includePad set whether to include extra space beyond font ascent and descent which is 129 * needed to avoid clipping in some scripts 130 * @param ellipsize whether to ellipsize the text if width of the text is longer than the 131 * requested width 132 * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is 133 * {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is 134 * not used, {@code outerwidth} is used instead 135 */ 136 public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerWidth, 137 Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, 138 boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 139 boolean trust; 140 141 if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) { 142 replaceWith(source, paint, outerWidth, align, spacingMult, spacingAdd); 143 144 mEllipsizedWidth = outerWidth; 145 mEllipsizedStart = 0; 146 mEllipsizedCount = 0; 147 trust = true; 148 } else { 149 replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this), 150 paint, outerWidth, align, spacingMult, spacingAdd); 151 152 mEllipsizedWidth = ellipsizedWidth; 153 trust = false; 154 } 155 156 init(getText(), paint, align, metrics, includePad, trust); 157 return this; 158 } 159 160 /** 161 * @param source the text to render 162 * @param paint the default paint for the layout 163 * @param outerwidth the wrapping width for the text 164 * @param align whether to left, right, or center the text 165 * @param spacingMult this value is no longer used by BoringLayout 166 * @param spacingAdd this value is no longer used by BoringLayout 167 * @param metrics {@code #Metrics} instance that contains information about FontMetrics and 168 * line width 169 * @param includePad set whether to include extra space beyond font ascent and descent which is 170 * needed to avoid clipping in some scripts 171 */ 172 public BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align, 173 float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad) { 174 super(source, paint, outerwidth, align, spacingMult, spacingAdd); 175 176 mEllipsizedWidth = outerwidth; 177 mEllipsizedStart = 0; 178 mEllipsizedCount = 0; 179 180 init(source, paint, align, metrics, includePad, true); 181 } 182 183 /** 184 * 185 * @param source the text to render 186 * @param paint the default paint for the layout 187 * @param outerWidth the wrapping width for the text 188 * @param align whether to left, right, or center the text 189 * @param spacingMult this value is no longer used by BoringLayout 190 * @param spacingAdd this value is no longer used by BoringLayout 191 * @param metrics {@code #Metrics} instance that contains information about FontMetrics and 192 * line width 193 * @param includePad set whether to include extra space beyond font ascent and descent which is 194 * needed to avoid clipping in some scripts 195 * @param ellipsize whether to ellipsize the text if width of the text is longer than the 196 * requested {@code outerwidth} 197 * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is 198 * {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is 199 * not used, {@code outerwidth} is used instead 200 */ 201 public BoringLayout(CharSequence source, TextPaint paint, int outerWidth, Alignment align, 202 float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad, 203 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 204 /* 205 * It is silly to have to call super() and then replaceWith(), 206 * but we can't use "this" for the callback until the call to 207 * super() finishes. 208 */ 209 super(source, paint, outerWidth, align, spacingMult, spacingAdd); 210 211 boolean trust; 212 213 if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) { 214 mEllipsizedWidth = outerWidth; 215 mEllipsizedStart = 0; 216 mEllipsizedCount = 0; 217 trust = true; 218 } else { 219 replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this), 220 paint, outerWidth, align, spacingMult, spacingAdd); 221 222 mEllipsizedWidth = ellipsizedWidth; 223 trust = false; 224 } 225 226 init(getText(), paint, align, metrics, includePad, trust); 227 } 228 229 /* package */ void init(CharSequence source, TextPaint paint, Alignment align, 230 BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth) { 231 int spacing; 232 233 if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) { 234 mDirect = source.toString(); 235 } else { 236 mDirect = null; 237 } 238 239 mPaint = paint; 240 241 if (includePad) { 242 spacing = metrics.bottom - metrics.top; 243 mDesc = metrics.bottom; 244 } else { 245 spacing = metrics.descent - metrics.ascent; 246 mDesc = metrics.descent; 247 } 248 249 mBottom = spacing; 250 251 if (trustWidth) { 252 mMax = metrics.width; 253 } else { 254 /* 255 * If we have ellipsized, we have to actually calculate the 256 * width because the width that was passed in was for the 257 * full text, not the ellipsized form. 258 */ 259 TextLine line = TextLine.obtain(); 260 line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT, 261 Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); 262 mMax = (int) Math.ceil(line.metrics(null)); 263 TextLine.recycle(line); 264 } 265 266 if (includePad) { 267 mTopPadding = metrics.top - metrics.ascent; 268 mBottomPadding = metrics.bottom - metrics.descent; 269 } 270 } 271 272 /** 273 * Returns null if not boring; the width, ascent, and descent if boring. 274 */ 275 public static Metrics isBoring(CharSequence text, TextPaint paint) { 276 return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null); 277 } 278 279 /** 280 * Returns null if not boring; the width, ascent, and descent in the 281 * provided Metrics object (or a new one if the provided one was null) 282 * if boring. 283 */ 284 public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) { 285 return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics); 286 } 287 288 /** 289 * Returns true if the text contains any RTL characters, bidi format characters, or surrogate 290 * code units. 291 */ 292 private static boolean hasAnyInterestingChars(CharSequence text, int textLength) { 293 final int MAX_BUF_LEN = 500; 294 final char[] buffer = TextUtils.obtain(MAX_BUF_LEN); 295 try { 296 for (int start = 0; start < textLength; start += MAX_BUF_LEN) { 297 final int end = Math.min(start + MAX_BUF_LEN, textLength); 298 299 // No need to worry about getting half codepoints, since we consider surrogate code 300 // units "interesting" as soon we see one. 301 TextUtils.getChars(text, start, end, buffer, 0); 302 303 final int len = end - start; 304 for (int i = 0; i < len; i++) { 305 final char c = buffer[i]; 306 if (c == '\n' || c == '\t' || TextUtils.couldAffectRtl(c)) { 307 return true; 308 } 309 } 310 } 311 return false; 312 } finally { 313 TextUtils.recycle(buffer); 314 } 315 } 316 317 /** 318 * Returns null if not boring; the width, ascent, and descent in the 319 * provided Metrics object (or a new one if the provided one was null) 320 * if boring. 321 * @hide 322 */ 323 public static Metrics isBoring(CharSequence text, TextPaint paint, 324 TextDirectionHeuristic textDir, Metrics metrics) { 325 final int textLength = text.length(); 326 if (hasAnyInterestingChars(text, textLength)) { 327 return null; // There are some interesting characters. Not boring. 328 } 329 if (textDir != null && textDir.isRtl(text, 0, textLength)) { 330 return null; // The heuristic considers the whole text RTL. Not boring. 331 } 332 if (text instanceof Spanned) { 333 Spanned sp = (Spanned) text; 334 Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class); 335 if (styles.length > 0) { 336 return null; // There are some PargraphStyle spans. Not boring. 337 } 338 } 339 340 Metrics fm = metrics; 341 if (fm == null) { 342 fm = new Metrics(); 343 } else { 344 fm.reset(); 345 } 346 347 TextLine line = TextLine.obtain(); 348 line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT, 349 Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); 350 fm.width = (int) Math.ceil(line.metrics(fm)); 351 TextLine.recycle(line); 352 353 return fm; 354 } 355 356 @Override 357 public int getHeight() { 358 return mBottom; 359 } 360 361 @Override 362 public int getLineCount() { 363 return 1; 364 } 365 366 @Override 367 public int getLineTop(int line) { 368 if (line == 0) 369 return 0; 370 else 371 return mBottom; 372 } 373 374 @Override 375 public int getLineDescent(int line) { 376 return mDesc; 377 } 378 379 @Override 380 public int getLineStart(int line) { 381 if (line == 0) 382 return 0; 383 else 384 return getText().length(); 385 } 386 387 @Override 388 public int getParagraphDirection(int line) { 389 return DIR_LEFT_TO_RIGHT; 390 } 391 392 @Override 393 public boolean getLineContainsTab(int line) { 394 return false; 395 } 396 397 @Override 398 public float getLineMax(int line) { 399 return mMax; 400 } 401 402 @Override 403 public float getLineWidth(int line) { 404 return (line == 0 ? mMax : 0); 405 } 406 407 @Override 408 public final Directions getLineDirections(int line) { 409 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 410 } 411 412 @Override 413 public int getTopPadding() { 414 return mTopPadding; 415 } 416 417 @Override 418 public int getBottomPadding() { 419 return mBottomPadding; 420 } 421 422 @Override 423 public int getEllipsisCount(int line) { 424 return mEllipsizedCount; 425 } 426 427 @Override 428 public int getEllipsisStart(int line) { 429 return mEllipsizedStart; 430 } 431 432 @Override 433 public int getEllipsizedWidth() { 434 return mEllipsizedWidth; 435 } 436 437 // Override draw so it will be faster. 438 @Override 439 public void draw(Canvas c, Path highlight, Paint highlightpaint, 440 int cursorOffset) { 441 if (mDirect != null && highlight == null) { 442 c.drawText(mDirect, 0, mBottom - mDesc, mPaint); 443 } else { 444 super.draw(c, highlight, highlightpaint, cursorOffset); 445 } 446 } 447 448 /** 449 * Callback for the ellipsizer to report what region it ellipsized. 450 */ 451 public void ellipsized(int start, int end) { 452 mEllipsizedStart = start; 453 mEllipsizedCount = end - start; 454 } 455 456 private String mDirect; 457 private Paint mPaint; 458 459 /* package */ int mBottom, mDesc; // for Direct 460 private int mTopPadding, mBottomPadding; 461 private float mMax; 462 private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount; 463 464 public static class Metrics extends Paint.FontMetricsInt { 465 public int width; 466 467 @Override public String toString() { 468 return super.toString() + " width=" + width; 469 } 470 471 private void reset() { 472 top = 0; 473 bottom = 0; 474 ascent = 0; 475 descent = 0; 476 width = 0; 477 leading = 0; 478 } 479 } 480 } 481