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 { make(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad)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 make(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)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 */ replaceOrMake(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad)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 */ replaceOrMake(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)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 BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad)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 BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth)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 init(CharSequence source, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics, boolean includepad, boolean trustWidth)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 */ isBoring(CharSequence text, TextPaint paint)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 */ isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir)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 */ isBoring(CharSequence text, TextPaint paint, Metrics metrics)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 */ isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir, Metrics metrics)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 getHeight()318 public int getHeight() { 319 return mBottom; 320 } 321 322 @Override getLineCount()323 public int getLineCount() { 324 return 1; 325 } 326 327 @Override getLineTop(int line)328 public int getLineTop(int line) { 329 if (line == 0) 330 return 0; 331 else 332 return mBottom; 333 } 334 335 @Override getLineDescent(int line)336 public int getLineDescent(int line) { 337 return mDesc; 338 } 339 340 @Override getLineStart(int line)341 public int getLineStart(int line) { 342 if (line == 0) 343 return 0; 344 else 345 return getText().length(); 346 } 347 348 @Override getParagraphDirection(int line)349 public int getParagraphDirection(int line) { 350 return DIR_LEFT_TO_RIGHT; 351 } 352 353 @Override getLineContainsTab(int line)354 public boolean getLineContainsTab(int line) { 355 return false; 356 } 357 358 @Override getLineMax(int line)359 public float getLineMax(int line) { 360 return mMax; 361 } 362 363 @Override getLineDirections(int line)364 public final Directions getLineDirections(int line) { 365 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 366 } 367 368 @Override getTopPadding()369 public int getTopPadding() { 370 return mTopPadding; 371 } 372 373 @Override getBottomPadding()374 public int getBottomPadding() { 375 return mBottomPadding; 376 } 377 378 @Override getEllipsisCount(int line)379 public int getEllipsisCount(int line) { 380 return mEllipsizedCount; 381 } 382 383 @Override getEllipsisStart(int line)384 public int getEllipsisStart(int line) { 385 return mEllipsizedStart; 386 } 387 388 @Override getEllipsizedWidth()389 public int getEllipsizedWidth() { 390 return mEllipsizedWidth; 391 } 392 393 // Override draw so it will be faster. 394 @Override draw(Canvas c, Path highlight, Paint highlightpaint, int cursorOffset)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 */ ellipsized(int start, int end)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 toString()428 @Override public String toString() { 429 return super.toString() + " width=" + width; 430 } 431 } 432 } 433