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 17package android.text; 18 19import android.graphics.Canvas; 20import android.graphics.Paint; 21import android.graphics.Path; 22import 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 */ 37public 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 in the 225 * provided Metrics object (or a new one if the provided one was null) 226 * if boring. 227 */ 228 public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) { 229 return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics); 230 } 231 232 /** 233 * Returns true if the text contains any RTL characters, bidi format characters, or surrogate 234 * code units. 235 */ 236 private static boolean hasAnyInterestingChars(CharSequence text, int textLength) { 237 final int MAX_BUF_LEN = 500; 238 final char[] buffer = TextUtils.obtain(MAX_BUF_LEN); 239 try { 240 for (int start = 0; start < textLength; start += MAX_BUF_LEN) { 241 final int end = Math.min(start + MAX_BUF_LEN, textLength); 242 243 // No need to worry about getting half codepoints, since we consider surrogate code 244 // units "interesting" as soon we see one. 245 TextUtils.getChars(text, start, end, buffer, 0); 246 247 final int len = end - start; 248 for (int i = 0; i < len; i++) { 249 final char c = buffer[i]; 250 if (c == '\n' || c == '\t' || TextUtils.couldAffectRtl(c)) { 251 return true; 252 } 253 } 254 } 255 return false; 256 } finally { 257 TextUtils.recycle(buffer); 258 } 259 } 260 261 /** 262 * Returns null if not boring; the width, ascent, and descent in the 263 * provided Metrics object (or a new one if the provided one was null) 264 * if boring. 265 * @hide 266 */ 267 public static Metrics isBoring(CharSequence text, TextPaint paint, 268 TextDirectionHeuristic textDir, Metrics metrics) { 269 final int textLength = text.length(); 270 if (hasAnyInterestingChars(text, textLength)) { 271 return null; // There are some interesting characters. Not boring. 272 } 273 if (textDir != null && textDir.isRtl(text, 0, textLength)) { 274 return null; // The heuristic considers the whole text RTL. Not boring. 275 } 276 if (text instanceof Spanned) { 277 Spanned sp = (Spanned) text; 278 Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class); 279 if (styles.length > 0) { 280 return null; // There are some PargraphStyle spans. Not boring. 281 } 282 } 283 284 Metrics fm = metrics; 285 if (fm == null) { 286 fm = new Metrics(); 287 } else { 288 fm.reset(); 289 } 290 291 TextLine line = TextLine.obtain(); 292 line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT, 293 Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); 294 fm.width = (int) Math.ceil(line.metrics(fm)); 295 TextLine.recycle(line); 296 297 return fm; 298 } 299 300 @Override 301 public int getHeight() { 302 return mBottom; 303 } 304 305 @Override 306 public int getLineCount() { 307 return 1; 308 } 309 310 @Override 311 public int getLineTop(int line) { 312 if (line == 0) 313 return 0; 314 else 315 return mBottom; 316 } 317 318 @Override 319 public int getLineDescent(int line) { 320 return mDesc; 321 } 322 323 @Override 324 public int getLineStart(int line) { 325 if (line == 0) 326 return 0; 327 else 328 return getText().length(); 329 } 330 331 @Override 332 public int getParagraphDirection(int line) { 333 return DIR_LEFT_TO_RIGHT; 334 } 335 336 @Override 337 public boolean getLineContainsTab(int line) { 338 return false; 339 } 340 341 @Override 342 public float getLineMax(int line) { 343 return mMax; 344 } 345 346 @Override 347 public float getLineWidth(int line) { 348 return (line == 0 ? mMax : 0); 349 } 350 351 @Override 352 public final Directions getLineDirections(int line) { 353 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 354 } 355 356 @Override 357 public int getTopPadding() { 358 return mTopPadding; 359 } 360 361 @Override 362 public int getBottomPadding() { 363 return mBottomPadding; 364 } 365 366 @Override 367 public int getEllipsisCount(int line) { 368 return mEllipsizedCount; 369 } 370 371 @Override 372 public int getEllipsisStart(int line) { 373 return mEllipsizedStart; 374 } 375 376 @Override 377 public int getEllipsizedWidth() { 378 return mEllipsizedWidth; 379 } 380 381 // Override draw so it will be faster. 382 @Override 383 public void draw(Canvas c, Path highlight, Paint highlightpaint, 384 int cursorOffset) { 385 if (mDirect != null && highlight == null) { 386 c.drawText(mDirect, 0, mBottom - mDesc, mPaint); 387 } else { 388 super.draw(c, highlight, highlightpaint, cursorOffset); 389 } 390 } 391 392 /** 393 * Callback for the ellipsizer to report what region it ellipsized. 394 */ 395 public void ellipsized(int start, int end) { 396 mEllipsizedStart = start; 397 mEllipsizedCount = end - start; 398 } 399 400 private String mDirect; 401 private Paint mPaint; 402 403 /* package */ int mBottom, mDesc; // for Direct 404 private int mTopPadding, mBottomPadding; 405 private float mMax; 406 private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount; 407 408 public static class Metrics extends Paint.FontMetricsInt { 409 public int width; 410 411 @Override public String toString() { 412 return super.toString() + " width=" + width; 413 } 414 415 private void reset() { 416 top = 0; 417 bottom = 0; 418 ascent = 0; 419 descent = 0; 420 width = 0; 421 leading = 0; 422 } 423 } 424} 425