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