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 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 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 */ 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 */ 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 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 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 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 */ 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 */ 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 */ 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 */ 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 320 public int getHeight() { 321 return mBottom; 322 } 323 324 @Override 325 public int getLineCount() { 326 return 1; 327 } 328 329 @Override 330 public int getLineTop(int line) { 331 if (line == 0) 332 return 0; 333 else 334 return mBottom; 335 } 336 337 @Override 338 public int getLineDescent(int line) { 339 return mDesc; 340 } 341 342 @Override 343 public int getLineStart(int line) { 344 if (line == 0) 345 return 0; 346 else 347 return getText().length(); 348 } 349 350 @Override 351 public int getParagraphDirection(int line) { 352 return DIR_LEFT_TO_RIGHT; 353 } 354 355 @Override 356 public boolean getLineContainsTab(int line) { 357 return false; 358 } 359 360 @Override 361 public float getLineMax(int line) { 362 return mMax; 363 } 364 365 @Override 366 public float getLineWidth(int line) { 367 return (line == 0 ? mMax : 0); 368 } 369 370 @Override 371 public final Directions getLineDirections(int line) { 372 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 373 } 374 375 @Override 376 public int getTopPadding() { 377 return mTopPadding; 378 } 379 380 @Override 381 public int getBottomPadding() { 382 return mBottomPadding; 383 } 384 385 @Override 386 public int getEllipsisCount(int line) { 387 return mEllipsizedCount; 388 } 389 390 @Override 391 public int getEllipsisStart(int line) { 392 return mEllipsizedStart; 393 } 394 395 @Override 396 public int getEllipsizedWidth() { 397 return mEllipsizedWidth; 398 } 399 400 // Override draw so it will be faster. 401 @Override 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 */ 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 430 @Override public String toString() { 431 return super.toString() + " width=" + width; 432 } 433 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