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 in the 225 * provided Metrics object (or a new one if the provided one was null) 226 * if boring. 227 */ isBoring(CharSequence text, TextPaint paint, Metrics metrics)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 */ hasAnyInterestingChars(CharSequence text, int textLength)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 */ isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir, Metrics metrics)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 getHeight()301 public int getHeight() { 302 return mBottom; 303 } 304 305 @Override getLineCount()306 public int getLineCount() { 307 return 1; 308 } 309 310 @Override getLineTop(int line)311 public int getLineTop(int line) { 312 if (line == 0) 313 return 0; 314 else 315 return mBottom; 316 } 317 318 @Override getLineDescent(int line)319 public int getLineDescent(int line) { 320 return mDesc; 321 } 322 323 @Override getLineStart(int line)324 public int getLineStart(int line) { 325 if (line == 0) 326 return 0; 327 else 328 return getText().length(); 329 } 330 331 @Override getParagraphDirection(int line)332 public int getParagraphDirection(int line) { 333 return DIR_LEFT_TO_RIGHT; 334 } 335 336 @Override getLineContainsTab(int line)337 public boolean getLineContainsTab(int line) { 338 return false; 339 } 340 341 @Override getLineMax(int line)342 public float getLineMax(int line) { 343 return mMax; 344 } 345 346 @Override getLineWidth(int line)347 public float getLineWidth(int line) { 348 return (line == 0 ? mMax : 0); 349 } 350 351 @Override getLineDirections(int line)352 public final Directions getLineDirections(int line) { 353 return Layout.DIRS_ALL_LEFT_TO_RIGHT; 354 } 355 356 @Override getTopPadding()357 public int getTopPadding() { 358 return mTopPadding; 359 } 360 361 @Override getBottomPadding()362 public int getBottomPadding() { 363 return mBottomPadding; 364 } 365 366 @Override getEllipsisCount(int line)367 public int getEllipsisCount(int line) { 368 return mEllipsizedCount; 369 } 370 371 @Override getEllipsisStart(int line)372 public int getEllipsisStart(int line) { 373 return mEllipsizedStart; 374 } 375 376 @Override getEllipsizedWidth()377 public int getEllipsizedWidth() { 378 return mEllipsizedWidth; 379 } 380 381 // Override draw so it will be faster. 382 @Override draw(Canvas c, Path highlight, Paint highlightpaint, int cursorOffset)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 */ ellipsized(int start, int end)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 toString()411 @Override public String toString() { 412 return super.toString() + " width=" + width; 413 } 414 reset()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