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