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 */ make(CharSequence source, TextPaint paint, int outerWidth, Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad)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 */ make(CharSequence source, TextPaint paint, int outerWidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)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 */ replaceOrMake(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad)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 */ replaceOrMake(CharSequence source, TextPaint paint, int outerWidth, Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)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 */ BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad)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 */ BoringLayout(CharSequence source, TextPaint paint, int outerWidth, Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)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 init(CharSequence source, TextPaint paint, Alignment align, BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth)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 */ isBoring(CharSequence text, TextPaint paint)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 */ isBoring(CharSequence text, TextPaint paint, Metrics metrics)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 */ hasAnyInterestingChars(CharSequence text, int textLength)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 */ isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir, Metrics metrics)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 getHeight()357 public int getHeight() { 358 return mBottom; 359 } 360 361 @Override getLineCount()362 public int getLineCount() { 363 return 1; 364 } 365 366 @Override getLineTop(int line)367 public int getLineTop(int line) { 368 if (line == 0) 369 return 0; 370 else 371 return mBottom; 372 } 373 374 @Override getLineDescent(int line)375 public int getLineDescent(int line) { 376 return mDesc; 377 } 378 379 @Override getLineStart(int line)380 public int getLineStart(int line) { 381 if (line == 0) 382 return 0; 383 else 384 return getText().length(); 385 } 386 387 @Override getParagraphDirection(int line)388 public int getParagraphDirection(int line) { 389 return DIR_LEFT_TO_RIGHT; 390 } 391 392 @Override getLineContainsTab(int line)393 public boolean getLineContainsTab(int line) { 394 return false; 395 } 396 397 @Override getLineMax(int line)398 public float getLineMax(int line) { 399 return mMax; 400 } 401 402 @Override getLineWidth(int line)403 public float getLineWidth(int line) { 404 return (line == 0 ? mMax : 0); 405 } 406 407 @Override getLineDirections(int line)408 public final Directions getLineDirections(int line) { 409 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 410 } 411 412 @Override getTopPadding()413 public int getTopPadding() { 414 return mTopPadding; 415 } 416 417 @Override getBottomPadding()418 public int getBottomPadding() { 419 return mBottomPadding; 420 } 421 422 @Override getEllipsisCount(int line)423 public int getEllipsisCount(int line) { 424 return mEllipsizedCount; 425 } 426 427 @Override getEllipsisStart(int line)428 public int getEllipsisStart(int line) { 429 return mEllipsizedStart; 430 } 431 432 @Override getEllipsizedWidth()433 public int getEllipsizedWidth() { 434 return mEllipsizedWidth; 435 } 436 437 // Override draw so it will be faster. 438 @Override draw(Canvas c, Path highlight, Paint highlightpaint, int cursorOffset)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 */ ellipsized(int start, int end)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 toString()467 @Override public String toString() { 468 return super.toString() + " width=" + width; 469 } 470 reset()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