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 { make(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad)38 public static BoringLayout make(CharSequence source, 39 TextPaint paint, int outerwidth, 40 Alignment align, 41 float spacingmult, float spacingadd, 42 BoringLayout.Metrics metrics, boolean includepad) { 43 return new BoringLayout(source, paint, outerwidth, align, 44 spacingmult, spacingadd, metrics, 45 includepad); 46 } 47 make(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)48 public static BoringLayout make(CharSequence source, 49 TextPaint paint, int outerwidth, 50 Alignment align, 51 float spacingmult, float spacingadd, 52 BoringLayout.Metrics metrics, boolean includepad, 53 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 54 return new BoringLayout(source, paint, outerwidth, align, 55 spacingmult, spacingadd, metrics, 56 includepad, ellipsize, ellipsizedWidth); 57 } 58 59 /** 60 * Returns a BoringLayout for the specified text, potentially reusing 61 * this one if it is already suitable. The caller must make sure that 62 * no one is still using this Layout. 63 */ replaceOrMake(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad)64 public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, 65 int outerwidth, Alignment align, 66 float spacingmult, float spacingadd, 67 BoringLayout.Metrics metrics, 68 boolean includepad) { 69 replaceWith(source, paint, outerwidth, align, spacingmult, 70 spacingadd); 71 72 mEllipsizedWidth = outerwidth; 73 mEllipsizedStart = 0; 74 mEllipsizedCount = 0; 75 76 init(source, paint, outerwidth, align, spacingmult, spacingadd, 77 metrics, includepad, true); 78 return this; 79 } 80 81 /** 82 * Returns a BoringLayout for the specified text, potentially reusing 83 * this one if it is already suitable. The caller must make sure that 84 * no one is still using this Layout. 85 */ replaceOrMake(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)86 public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, 87 int outerwidth, Alignment align, 88 float spacingmult, float spacingadd, 89 BoringLayout.Metrics metrics, 90 boolean includepad, 91 TextUtils.TruncateAt ellipsize, 92 int ellipsizedWidth) { 93 boolean trust; 94 95 if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) { 96 replaceWith(source, paint, outerwidth, align, spacingmult, 97 spacingadd); 98 99 mEllipsizedWidth = outerwidth; 100 mEllipsizedStart = 0; 101 mEllipsizedCount = 0; 102 trust = true; 103 } else { 104 replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, 105 ellipsize, true, this), 106 paint, outerwidth, align, spacingmult, 107 spacingadd); 108 109 mEllipsizedWidth = ellipsizedWidth; 110 trust = false; 111 } 112 113 init(getText(), paint, outerwidth, align, spacingmult, spacingadd, 114 metrics, includepad, trust); 115 return this; 116 } 117 BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad)118 public BoringLayout(CharSequence source, 119 TextPaint paint, int outerwidth, 120 Alignment align, 121 float spacingmult, float spacingadd, 122 BoringLayout.Metrics metrics, boolean includepad) { 123 super(source, paint, outerwidth, align, spacingmult, spacingadd); 124 125 mEllipsizedWidth = outerwidth; 126 mEllipsizedStart = 0; 127 mEllipsizedCount = 0; 128 129 init(source, paint, outerwidth, align, spacingmult, spacingadd, 130 metrics, includepad, true); 131 } 132 BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)133 public BoringLayout(CharSequence source, 134 TextPaint paint, int outerwidth, 135 Alignment align, 136 float spacingmult, float spacingadd, 137 BoringLayout.Metrics metrics, boolean includepad, 138 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { 139 /* 140 * It is silly to have to call super() and then replaceWith(), 141 * but we can't use "this" for the callback until the call to 142 * super() finishes. 143 */ 144 super(source, paint, outerwidth, align, spacingmult, spacingadd); 145 146 boolean trust; 147 148 if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) { 149 mEllipsizedWidth = outerwidth; 150 mEllipsizedStart = 0; 151 mEllipsizedCount = 0; 152 trust = true; 153 } else { 154 replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, 155 ellipsize, true, this), 156 paint, outerwidth, align, spacingmult, 157 spacingadd); 158 159 160 mEllipsizedWidth = ellipsizedWidth; 161 trust = false; 162 } 163 164 init(getText(), paint, outerwidth, align, spacingmult, spacingadd, 165 metrics, includepad, trust); 166 } 167 init(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, boolean trustWidth)168 /* package */ void init(CharSequence source, 169 TextPaint paint, int outerwidth, 170 Alignment align, 171 float spacingmult, float spacingadd, 172 BoringLayout.Metrics metrics, boolean includepad, 173 boolean trustWidth) { 174 int spacing; 175 176 if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) { 177 mDirect = source.toString(); 178 } else { 179 mDirect = null; 180 } 181 182 mPaint = paint; 183 184 if (includepad) { 185 spacing = metrics.bottom - metrics.top; 186 mDesc = metrics.bottom; 187 } else { 188 spacing = metrics.descent - metrics.ascent; 189 mDesc = metrics.descent; 190 } 191 192 mBottom = spacing; 193 194 if (trustWidth) { 195 mMax = metrics.width; 196 } else { 197 /* 198 * If we have ellipsized, we have to actually calculate the 199 * width because the width that was passed in was for the 200 * full text, not the ellipsized form. 201 */ 202 TextLine line = TextLine.obtain(); 203 line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT, 204 Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); 205 mMax = (int) Math.ceil(line.metrics(null)); 206 TextLine.recycle(line); 207 } 208 209 if (includepad) { 210 mTopPadding = metrics.top - metrics.ascent; 211 mBottomPadding = metrics.bottom - metrics.descent; 212 } 213 } 214 215 /** 216 * Returns null if not boring; the width, ascent, and descent if boring. 217 */ isBoring(CharSequence text, TextPaint paint)218 public static Metrics isBoring(CharSequence text, 219 TextPaint paint) { 220 return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null); 221 } 222 223 /** 224 * Returns null if not boring; the width, ascent, and descent if boring. 225 * @hide 226 */ isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir)227 public static Metrics isBoring(CharSequence text, 228 TextPaint paint, 229 TextDirectionHeuristic textDir) { 230 return isBoring(text, paint, textDir, null); 231 } 232 233 /** 234 * Returns null if not boring; the width, ascent, and descent in the 235 * provided Metrics object (or a new one if the provided one was null) 236 * if boring. 237 */ isBoring(CharSequence text, TextPaint paint, Metrics metrics)238 public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) { 239 return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics); 240 } 241 242 /** 243 * Returns null if not boring; the width, ascent, and descent in the 244 * provided Metrics object (or a new one if the provided one was null) 245 * if boring. 246 * @hide 247 */ isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir, Metrics metrics)248 public static Metrics isBoring(CharSequence text, TextPaint paint, 249 TextDirectionHeuristic textDir, Metrics metrics) { 250 final int MAX_BUF_LEN = 500; 251 final char[] buffer = TextUtils.obtain(MAX_BUF_LEN); 252 final int textLength = text.length(); 253 boolean boring = true; 254 255 outer: 256 for (int start = 0; start < textLength; start += MAX_BUF_LEN) { 257 final int end = Math.min(start + MAX_BUF_LEN, textLength); 258 259 // No need to worry about getting half codepoints, since we reject surrogate code units 260 // as non-boring as soon we see one. 261 TextUtils.getChars(text, start, end, buffer, 0); 262 263 final int len = end - start; 264 for (int i = 0; i < len; i++) { 265 final char c = buffer[i]; 266 267 if (c == '\n' || c == '\t' || 268 (c >= 0x0590 && c <= 0x08FF) || // RTL scripts 269 c == 0x200F || // Bidi format character 270 (c >= 0x202A && c <= 0x202E) || // Bidi format characters 271 (c >= 0x2066 && c <= 0x2069) || // Bidi format characters 272 (c >= 0xD800 && c <= 0xDFFF) || // surrogate pairs 273 (c >= 0xFB1D && c <= 0xFDFF) || // Hebrew and Arabic presentation forms 274 (c >= 0xFE70 && c <= 0xFEFE) // Arabic presentation forms 275 ) { 276 boring = false; 277 break outer; 278 } 279 } 280 281 // TODO: This looks a little suspicious, and in some cases can result in O(n^2) 282 // run time. Consider moving outside the loop. 283 if (textDir != null && textDir.isRtl(buffer, 0, len)) { 284 boring = false; 285 break outer; 286 } 287 } 288 289 TextUtils.recycle(buffer); 290 291 if (boring && text instanceof Spanned) { 292 Spanned sp = (Spanned) text; 293 Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class); 294 if (styles.length > 0) { 295 boring = false; 296 } 297 } 298 299 if (boring) { 300 Metrics fm = metrics; 301 if (fm == null) { 302 fm = new Metrics(); 303 } else { 304 fm.reset(); 305 } 306 307 TextLine line = TextLine.obtain(); 308 line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT, 309 Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); 310 fm.width = (int) Math.ceil(line.metrics(fm)); 311 TextLine.recycle(line); 312 313 return fm; 314 } else { 315 return null; 316 } 317 } 318 319 @Override getHeight()320 public int getHeight() { 321 return mBottom; 322 } 323 324 @Override getLineCount()325 public int getLineCount() { 326 return 1; 327 } 328 329 @Override getLineTop(int line)330 public int getLineTop(int line) { 331 if (line == 0) 332 return 0; 333 else 334 return mBottom; 335 } 336 337 @Override getLineDescent(int line)338 public int getLineDescent(int line) { 339 return mDesc; 340 } 341 342 @Override getLineStart(int line)343 public int getLineStart(int line) { 344 if (line == 0) 345 return 0; 346 else 347 return getText().length(); 348 } 349 350 @Override getParagraphDirection(int line)351 public int getParagraphDirection(int line) { 352 return DIR_LEFT_TO_RIGHT; 353 } 354 355 @Override getLineContainsTab(int line)356 public boolean getLineContainsTab(int line) { 357 return false; 358 } 359 360 @Override getLineMax(int line)361 public float getLineMax(int line) { 362 return mMax; 363 } 364 365 @Override getLineWidth(int line)366 public float getLineWidth(int line) { 367 return (line == 0 ? mMax : 0); 368 } 369 370 @Override getLineDirections(int line)371 public final Directions getLineDirections(int line) { 372 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 373 } 374 375 @Override getTopPadding()376 public int getTopPadding() { 377 return mTopPadding; 378 } 379 380 @Override getBottomPadding()381 public int getBottomPadding() { 382 return mBottomPadding; 383 } 384 385 @Override getEllipsisCount(int line)386 public int getEllipsisCount(int line) { 387 return mEllipsizedCount; 388 } 389 390 @Override getEllipsisStart(int line)391 public int getEllipsisStart(int line) { 392 return mEllipsizedStart; 393 } 394 395 @Override getEllipsizedWidth()396 public int getEllipsizedWidth() { 397 return mEllipsizedWidth; 398 } 399 400 // Override draw so it will be faster. 401 @Override draw(Canvas c, Path highlight, Paint highlightpaint, int cursorOffset)402 public void draw(Canvas c, Path highlight, Paint highlightpaint, 403 int cursorOffset) { 404 if (mDirect != null && highlight == null) { 405 c.drawText(mDirect, 0, mBottom - mDesc, mPaint); 406 } else { 407 super.draw(c, highlight, highlightpaint, cursorOffset); 408 } 409 } 410 411 /** 412 * Callback for the ellipsizer to report what region it ellipsized. 413 */ ellipsized(int start, int end)414 public void ellipsized(int start, int end) { 415 mEllipsizedStart = start; 416 mEllipsizedCount = end - start; 417 } 418 419 private String mDirect; 420 private Paint mPaint; 421 422 /* package */ int mBottom, mDesc; // for Direct 423 private int mTopPadding, mBottomPadding; 424 private float mMax; 425 private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount; 426 427 public static class Metrics extends Paint.FontMetricsInt { 428 public int width; 429 toString()430 @Override public String toString() { 431 return super.toString() + " width=" + width; 432 } 433 reset()434 private void reset() { 435 top = 0; 436 bottom = 0; 437 ascent = 0; 438 descent = 0; 439 width = 0; 440 leading = 0; 441 } 442 } 443 } 444