blob: bfa0ab61f3e4d200e17adaabd8c3aeec110d26c1 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
Doug Felt4e0c5e52010-03-15 16:56:02 -070019import com.android.internal.util.ArrayUtils;
20
The Android Open Source Project10592532009-03-18 17:39:46 -070021import android.graphics.Bitmap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.graphics.Paint;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import android.text.style.LeadingMarginSpan;
24import android.text.style.LineHeightSpan;
25import android.text.style.MetricAffectingSpan;
26import android.text.style.ReplacementSpan;
Doug Felt4e0c5e52010-03-15 16:56:02 -070027import android.util.Log;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028
29/**
30 * StaticLayout is a Layout for text that will not be edited after it
31 * is laid out. Use {@link DynamicLayout} for text that may change.
32 * <p>This is used by widgets to control text layout. You should not need
33 * to use this class directly unless you are implementing your own widget
34 * or custom display object, or would be tempted to call
Doug Felt4e0c5e52010-03-15 16:56:02 -070035 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int,
36 * float, float, android.graphics.Paint)
37 * Canvas.drawText()} directly.</p>
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038 */
39public class
40StaticLayout
41extends Layout
42{
43 public StaticLayout(CharSequence source, TextPaint paint,
44 int width,
45 Alignment align, float spacingmult, float spacingadd,
46 boolean includepad) {
47 this(source, 0, source.length(), paint, width, align,
48 spacingmult, spacingadd, includepad);
49 }
50
51 public StaticLayout(CharSequence source, int bufstart, int bufend,
52 TextPaint paint, int outerwidth,
53 Alignment align,
54 float spacingmult, float spacingadd,
55 boolean includepad) {
56 this(source, bufstart, bufend, paint, outerwidth, align,
57 spacingmult, spacingadd, includepad, null, 0);
58 }
59
60 public StaticLayout(CharSequence source, int bufstart, int bufend,
61 TextPaint paint, int outerwidth,
62 Alignment align,
63 float spacingmult, float spacingadd,
64 boolean includepad,
65 TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
66 super((ellipsize == null)
Doug Felt4e0c5e52010-03-15 16:56:02 -070067 ? source
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080068 : (source instanceof Spanned)
69 ? new SpannedEllipsizer(source)
70 : new Ellipsizer(source),
71 paint, outerwidth, align, spacingmult, spacingadd);
72
73 /*
74 * This is annoying, but we can't refer to the layout until
75 * superclass construction is finished, and the superclass
76 * constructor wants the reference to the display text.
Doug Felt4e0c5e52010-03-15 16:56:02 -070077 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080078 * This will break if the superclass constructor ever actually
79 * cares about the content instead of just holding the reference.
80 */
81 if (ellipsize != null) {
82 Ellipsizer e = (Ellipsizer) getText();
83
84 e.mLayout = this;
85 e.mWidth = ellipsizedWidth;
86 e.mMethod = ellipsize;
87 mEllipsizedWidth = ellipsizedWidth;
88
89 mColumns = COLUMNS_ELLIPSIZE;
90 } else {
91 mColumns = COLUMNS_NORMAL;
92 mEllipsizedWidth = outerwidth;
93 }
94
95 mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
96 mLineDirections = new Directions[
97 ArrayUtils.idealIntArraySize(2 * mColumns)];
98
99 generate(source, bufstart, bufend, paint, outerwidth, align,
100 spacingmult, spacingadd, includepad, includepad,
101 ellipsize != null, ellipsizedWidth, ellipsize);
102
103 mChdirs = null;
104 mChs = null;
105 mWidths = null;
106 mFontMetricsInt = null;
107 }
108
109 /* package */ StaticLayout(boolean ellipsize) {
110 super(null, null, 0, null, 0, 0);
111
112 mColumns = COLUMNS_ELLIPSIZE;
113 mLines = new int[ArrayUtils.idealIntArraySize(2 * mColumns)];
114 mLineDirections = new Directions[
115 ArrayUtils.idealIntArraySize(2 * mColumns)];
116 }
117
118 /* package */ void generate(CharSequence source, int bufstart, int bufend,
119 TextPaint paint, int outerwidth,
120 Alignment align,
121 float spacingmult, float spacingadd,
122 boolean includepad, boolean trackpad,
123 boolean breakOnlyAtSpaces,
124 float ellipsizedWidth, TextUtils.TruncateAt where) {
125 mLineCount = 0;
126
127 int v = 0;
128 boolean needMultiply = (spacingmult != 1 || spacingadd != 0);
129
130 Paint.FontMetricsInt fm = mFontMetricsInt;
131 int[] choosehtv = null;
132
133 int end = TextUtils.indexOf(source, '\n', bufstart, bufend);
134 int bufsiz = end >= 0 ? end - bufstart : bufend - bufstart;
135 boolean first = true;
136
137 if (mChdirs == null) {
138 mChdirs = new byte[ArrayUtils.idealByteArraySize(bufsiz + 1)];
139 mChs = new char[ArrayUtils.idealCharArraySize(bufsiz + 1)];
140 mWidths = new float[ArrayUtils.idealIntArraySize((bufsiz + 1) * 2)];
141 }
142
143 byte[] chdirs = mChdirs;
144 char[] chs = mChs;
145 float[] widths = mWidths;
146
147 AlteredCharSequence alter = null;
148 Spanned spanned = null;
149
150 if (source instanceof Spanned)
151 spanned = (Spanned) source;
152
153 int DEFAULT_DIR = DIR_LEFT_TO_RIGHT; // XXX
154
155 for (int start = bufstart; start <= bufend; start = end) {
156 if (first)
157 first = false;
158 else
159 end = TextUtils.indexOf(source, '\n', start, bufend);
160
161 if (end < 0)
162 end = bufend;
163 else
164 end++;
165
Mark Wagner7b5676e2009-10-16 11:44:23 -0700166 int firstWidthLineCount = 1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800167 int firstwidth = outerwidth;
168 int restwidth = outerwidth;
169
170 LineHeightSpan[] chooseht = null;
171
172 if (spanned != null) {
173 LeadingMarginSpan[] sp;
174
175 sp = spanned.getSpans(start, end, LeadingMarginSpan.class);
176 for (int i = 0; i < sp.length; i++) {
Mark Wagner7b5676e2009-10-16 11:44:23 -0700177 LeadingMarginSpan lms = sp[i];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800178 firstwidth -= sp[i].getLeadingMargin(true);
179 restwidth -= sp[i].getLeadingMargin(false);
Mark Wagner7b5676e2009-10-16 11:44:23 -0700180 if (lms instanceof LeadingMarginSpan.LeadingMarginSpan2) {
181 firstWidthLineCount = ((LeadingMarginSpan.LeadingMarginSpan2)lms).getLeadingMarginLineCount();
182 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800183 }
184
185 chooseht = spanned.getSpans(start, end, LineHeightSpan.class);
186
187 if (chooseht.length != 0) {
188 if (choosehtv == null ||
189 choosehtv.length < chooseht.length) {
190 choosehtv = new int[ArrayUtils.idealIntArraySize(
191 chooseht.length)];
192 }
193
194 for (int i = 0; i < chooseht.length; i++) {
195 int o = spanned.getSpanStart(chooseht[i]);
196
197 if (o < start) {
198 // starts in this layout, before the
199 // current paragraph
200
Doug Felt4e0c5e52010-03-15 16:56:02 -0700201 choosehtv[i] = getLineTop(getLineForOffset(o));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800202 } else {
203 // starts in this paragraph
204
205 choosehtv[i] = v;
206 }
207 }
208 }
209 }
210
211 if (end - start > chdirs.length) {
212 chdirs = new byte[ArrayUtils.idealByteArraySize(end - start)];
213 mChdirs = chdirs;
214 }
215 if (end - start > chs.length) {
216 chs = new char[ArrayUtils.idealCharArraySize(end - start)];
217 mChs = chs;
218 }
219 if ((end - start) * 2 > widths.length) {
220 widths = new float[ArrayUtils.idealIntArraySize((end - start) * 2)];
221 mWidths = widths;
222 }
223
224 TextUtils.getChars(source, start, end, chs, 0);
225 final int n = end - start;
226
227 boolean easy = true;
228 boolean altered = false;
Doug Felt9f7a4442010-03-01 12:45:56 -0800229 int dir = DEFAULT_DIR; // XXX pass value in
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800230
231 for (int i = 0; i < n; i++) {
232 if (chs[i] >= FIRST_RIGHT_TO_LEFT) {
233 easy = false;
234 break;
235 }
236 }
237
Eric Fischerde61f782010-03-10 10:55:58 -0800238 // Ensure that none of the underlying characters are treated
239 // as viable breakpoints, and that the entire run gets the
240 // same bidi direction.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800241
Eric Fischerde61f782010-03-10 10:55:58 -0800242 if (source instanceof Spanned) {
243 Spanned sp = (Spanned) source;
244 ReplacementSpan[] spans = sp.getSpans(start, end, ReplacementSpan.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800245
Eric Fischerde61f782010-03-10 10:55:58 -0800246 for (int y = 0; y < spans.length; y++) {
247 int a = sp.getSpanStart(spans[y]);
248 int b = sp.getSpanEnd(spans[y]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800249
Eric Fischerde61f782010-03-10 10:55:58 -0800250 for (int x = a; x < b; x++) {
251 chs[x - start] = '\uFFFC';
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800252 }
253 }
Eric Fischerde61f782010-03-10 10:55:58 -0800254 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800255
Eric Fischerde61f782010-03-10 10:55:58 -0800256 if (!easy) {
Doug Felt20178d62010-02-22 13:39:01 -0800257 // XXX put override flags, etc. into chdirs
Doug Felt9f7a4442010-03-01 12:45:56 -0800258 // XXX supply dir rather than force
259 dir = AndroidBidi.bidi(DIR_REQUEST_DEFAULT_LTR, chs, chdirs, n, false);
Doug Felt20178d62010-02-22 13:39:01 -0800260
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800261 // Do mirroring for right-to-left segments
262
263 for (int i = 0; i < n; i++) {
264 if (chdirs[i] == Character.DIRECTIONALITY_RIGHT_TO_LEFT) {
265 int j;
266
267 for (j = i; j < n; j++) {
268 if (chdirs[j] !=
269 Character.DIRECTIONALITY_RIGHT_TO_LEFT)
270 break;
271 }
272
273 if (AndroidCharacter.mirror(chs, i, j - i))
274 altered = true;
275
276 i = j - 1;
277 }
278 }
279 }
280
281 CharSequence sub;
282
283 if (altered) {
284 if (alter == null)
285 alter = AlteredCharSequence.make(source, chs, start, end);
286 else
287 alter.update(chs, start, end);
288
289 sub = alter;
290 } else {
291 sub = source;
292 }
293
294 int width = firstwidth;
295
296 float w = 0;
297 int here = start;
298
299 int ok = start;
300 float okwidth = w;
301 int okascent = 0, okdescent = 0, oktop = 0, okbottom = 0;
302
303 int fit = start;
304 float fitwidth = w;
305 int fitascent = 0, fitdescent = 0, fittop = 0, fitbottom = 0;
306
307 boolean tab = false;
308
309 int next;
310 for (int i = start; i < end; i = next) {
311 if (spanned == null)
312 next = end;
313 else
314 next = spanned.nextSpanTransition(i, end,
315 MetricAffectingSpan.
316 class);
317
318 if (spanned == null) {
319 paint.getTextWidths(sub, i, next, widths);
320 System.arraycopy(widths, 0, widths,
321 end - start + (i - start), next - i);
Doug Felt4e0c5e52010-03-15 16:56:02 -0700322
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800323 paint.getFontMetricsInt(fm);
324 } else {
325 mWorkPaint.baselineShift = 0;
326
327 Styled.getTextWidths(paint, mWorkPaint,
328 spanned, i, next,
329 widths, fm);
330 System.arraycopy(widths, 0, widths,
331 end - start + (i - start), next - i);
332
333 if (mWorkPaint.baselineShift < 0) {
334 fm.ascent += mWorkPaint.baselineShift;
335 fm.top += mWorkPaint.baselineShift;
336 } else {
337 fm.descent += mWorkPaint.baselineShift;
338 fm.bottom += mWorkPaint.baselineShift;
339 }
340 }
341
342 int fmtop = fm.top;
343 int fmbottom = fm.bottom;
344 int fmascent = fm.ascent;
345 int fmdescent = fm.descent;
346
347 if (false) {
348 StringBuilder sb = new StringBuilder();
349 for (int j = i; j < next; j++) {
350 sb.append(widths[j - start + (end - start)]);
351 sb.append(' ');
352 }
353
354 Log.e("text", sb.toString());
355 }
356
357 for (int j = i; j < next; j++) {
358 char c = chs[j - start];
359 float before = w;
360
The Android Open Source Project10592532009-03-18 17:39:46 -0700361 if (c == '\n') {
362 ;
363 } else if (c == '\t') {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800364 w = Layout.nextTab(sub, start, end, w, null);
365 tab = true;
The Android Open Source Project10592532009-03-18 17:39:46 -0700366 } else if (c >= 0xD800 && c <= 0xDFFF && j + 1 < next) {
367 int emoji = Character.codePointAt(chs, j - start);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800368
The Android Open Source Project10592532009-03-18 17:39:46 -0700369 if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
370 Bitmap bm = EMOJI_FACTORY.
371 getBitmapFromAndroidPua(emoji);
372
373 if (bm != null) {
Eric Fischer423f0e42009-03-27 18:04:12 -0700374 Paint whichPaint;
375
376 if (spanned == null) {
377 whichPaint = paint;
378 } else {
379 whichPaint = mWorkPaint;
380 }
381
Doug Felt4e0c5e52010-03-15 16:56:02 -0700382 float wid = bm.getWidth() *
Eric Fischer423f0e42009-03-27 18:04:12 -0700383 -whichPaint.ascent() /
384 bm.getHeight();
385
386 w += wid;
The Android Open Source Project10592532009-03-18 17:39:46 -0700387 tab = true;
388 j++;
389 } else {
390 w += widths[j - start + (end - start)];
391 }
392 } else {
393 w += widths[j - start + (end - start)];
394 }
395 } else {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800396 w += widths[j - start + (end - start)];
397 }
398
399 // Log.e("text", "was " + before + " now " + w + " after " + c + " within " + width);
400
401 if (w <= width) {
402 fitwidth = w;
403 fit = j + 1;
404
405 if (fmtop < fittop)
406 fittop = fmtop;
407 if (fmascent < fitascent)
408 fitascent = fmascent;
409 if (fmdescent > fitdescent)
410 fitdescent = fmdescent;
411 if (fmbottom > fitbottom)
412 fitbottom = fmbottom;
413
414 /*
415 * From the Unicode Line Breaking Algorithm:
416 * (at least approximately)
Doug Felt4e0c5e52010-03-15 16:56:02 -0700417 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800418 * .,:; are class IS: breakpoints
419 * except when adjacent to digits
420 * / is class SY: a breakpoint
421 * except when followed by a digit.
422 * - is class HY: a breakpoint
423 * except when followed by a digit.
424 *
Eric Fischer549d7242009-03-31 14:19:47 -0700425 * Ideographs are class ID: breakpoints when adjacent,
426 * except for NS (non-starters), which can be broken
427 * after but not before.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800428 */
429
430 if (c == ' ' || c == '\t' ||
431 ((c == '.' || c == ',' || c == ':' || c == ';') &&
432 (j - 1 < here || !Character.isDigit(chs[j - 1 - start])) &&
433 (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) ||
434 ((c == '/' || c == '-') &&
435 (j + 1 >= next || !Character.isDigit(chs[j + 1 - start]))) ||
Eric Fischer549d7242009-03-31 14:19:47 -0700436 (c >= FIRST_CJK && isIdeographic(c, true) &&
437 j + 1 < next && isIdeographic(chs[j + 1 - start], false))) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800438 okwidth = w;
439 ok = j + 1;
440
441 if (fittop < oktop)
442 oktop = fittop;
443 if (fitascent < okascent)
444 okascent = fitascent;
445 if (fitdescent > okdescent)
446 okdescent = fitdescent;
447 if (fitbottom > okbottom)
448 okbottom = fitbottom;
449 }
450 } else if (breakOnlyAtSpaces) {
451 if (ok != here) {
452 // Log.e("text", "output ok " + here + " to " +ok);
453
454 while (ok < next && chs[ok - start] == ' ') {
455 ok++;
456 }
457
458 v = out(source,
459 here, ok,
460 okascent, okdescent, oktop, okbottom,
461 v,
462 spacingmult, spacingadd, chooseht,
463 choosehtv, fm, tab,
464 needMultiply, start, chdirs, dir, easy,
465 ok == bufend, includepad, trackpad,
466 widths, start, end - start,
467 where, ellipsizedWidth, okwidth,
468 paint);
469
470 here = ok;
471 } else {
472 // Act like it fit even though it didn't.
473
474 fitwidth = w;
475 fit = j + 1;
476
477 if (fmtop < fittop)
478 fittop = fmtop;
479 if (fmascent < fitascent)
480 fitascent = fmascent;
481 if (fmdescent > fitdescent)
482 fitdescent = fmdescent;
483 if (fmbottom > fitbottom)
484 fitbottom = fmbottom;
485 }
486 } else {
487 if (ok != here) {
488 // Log.e("text", "output ok " + here + " to " +ok);
489
490 while (ok < next && chs[ok - start] == ' ') {
491 ok++;
492 }
493
494 v = out(source,
495 here, ok,
496 okascent, okdescent, oktop, okbottom,
497 v,
498 spacingmult, spacingadd, chooseht,
499 choosehtv, fm, tab,
500 needMultiply, start, chdirs, dir, easy,
501 ok == bufend, includepad, trackpad,
502 widths, start, end - start,
503 where, ellipsizedWidth, okwidth,
504 paint);
505
506 here = ok;
507 } else if (fit != here) {
508 // Log.e("text", "output fit " + here + " to " +fit);
509 v = out(source,
510 here, fit,
511 fitascent, fitdescent,
512 fittop, fitbottom,
513 v,
514 spacingmult, spacingadd, chooseht,
515 choosehtv, fm, tab,
516 needMultiply, start, chdirs, dir, easy,
517 fit == bufend, includepad, trackpad,
518 widths, start, end - start,
519 where, ellipsizedWidth, fitwidth,
520 paint);
521
522 here = fit;
523 } else {
524 // Log.e("text", "output one " + here + " to " +(here + 1));
525 measureText(paint, mWorkPaint,
526 source, here, here + 1, fm, tab,
527 null);
528
529 v = out(source,
530 here, here+1,
531 fm.ascent, fm.descent,
532 fm.top, fm.bottom,
533 v,
534 spacingmult, spacingadd, chooseht,
535 choosehtv, fm, tab,
536 needMultiply, start, chdirs, dir, easy,
537 here + 1 == bufend, includepad,
538 trackpad,
539 widths, start, end - start,
540 where, ellipsizedWidth,
541 widths[here - start], paint);
542
543 here = here + 1;
544 }
545
546 if (here < i) {
547 j = next = here; // must remeasure
548 } else {
549 j = here - 1; // continue looping
550 }
551
552 ok = fit = here;
553 w = 0;
554 fitascent = fitdescent = fittop = fitbottom = 0;
555 okascent = okdescent = oktop = okbottom = 0;
556
Mark Wagner7b5676e2009-10-16 11:44:23 -0700557 if (--firstWidthLineCount <= 0) {
558 width = restwidth;
559 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800560 }
561 }
562 }
563
564 if (end != here) {
565 if ((fittop | fitbottom | fitdescent | fitascent) == 0) {
566 paint.getFontMetricsInt(fm);
567
568 fittop = fm.top;
569 fitbottom = fm.bottom;
570 fitascent = fm.ascent;
571 fitdescent = fm.descent;
572 }
573
574 // Log.e("text", "output rest " + here + " to " + end);
575
576 v = out(source,
577 here, end, fitascent, fitdescent,
578 fittop, fitbottom,
579 v,
580 spacingmult, spacingadd, chooseht,
581 choosehtv, fm, tab,
582 needMultiply, start, chdirs, dir, easy,
583 end == bufend, includepad, trackpad,
584 widths, start, end - start,
585 where, ellipsizedWidth, w, paint);
586 }
587
588 start = end;
589
590 if (end == bufend)
591 break;
592 }
593
594 if (bufend == bufstart || source.charAt(bufend - 1) == '\n') {
595 // Log.e("text", "output last " + bufend);
596
597 paint.getFontMetricsInt(fm);
598
599 v = out(source,
600 bufend, bufend, fm.ascent, fm.descent,
601 fm.top, fm.bottom,
602 v,
603 spacingmult, spacingadd, null,
604 null, fm, false,
605 needMultiply, bufend, chdirs, DEFAULT_DIR, true,
606 true, includepad, trackpad,
607 widths, bufstart, 0,
608 where, ellipsizedWidth, 0, paint);
609 }
610 }
611
612 private static final char FIRST_CJK = '\u2E80';
613 /**
614 * Returns true if the specified character is one of those specified
615 * as being Ideographic (class ID) by the Unicode Line Breaking Algorithm
616 * (http://www.unicode.org/unicode/reports/tr14/), and is therefore OK
617 * to break between a pair of.
Eric Fischer549d7242009-03-31 14:19:47 -0700618 *
619 * @param includeNonStarters also return true for category NS
620 * (non-starters), which can be broken
621 * after but not before.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800622 */
Eric Fischer549d7242009-03-31 14:19:47 -0700623 private static final boolean isIdeographic(char c, boolean includeNonStarters) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800624 if (c >= '\u2E80' && c <= '\u2FFF') {
625 return true; // CJK, KANGXI RADICALS, DESCRIPTION SYMBOLS
626 }
627 if (c == '\u3000') {
628 return true; // IDEOGRAPHIC SPACE
629 }
630 if (c >= '\u3040' && c <= '\u309F') {
Eric Fischer549d7242009-03-31 14:19:47 -0700631 if (!includeNonStarters) {
632 switch (c) {
633 case '\u3041': // # HIRAGANA LETTER SMALL A
634 case '\u3043': // # HIRAGANA LETTER SMALL I
635 case '\u3045': // # HIRAGANA LETTER SMALL U
636 case '\u3047': // # HIRAGANA LETTER SMALL E
637 case '\u3049': // # HIRAGANA LETTER SMALL O
638 case '\u3063': // # HIRAGANA LETTER SMALL TU
639 case '\u3083': // # HIRAGANA LETTER SMALL YA
640 case '\u3085': // # HIRAGANA LETTER SMALL YU
641 case '\u3087': // # HIRAGANA LETTER SMALL YO
642 case '\u308E': // # HIRAGANA LETTER SMALL WA
643 case '\u3095': // # HIRAGANA LETTER SMALL KA
644 case '\u3096': // # HIRAGANA LETTER SMALL KE
645 case '\u309B': // # KATAKANA-HIRAGANA VOICED SOUND MARK
646 case '\u309C': // # KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
647 case '\u309D': // # HIRAGANA ITERATION MARK
648 case '\u309E': // # HIRAGANA VOICED ITERATION MARK
649 return false;
650 }
651 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800652 return true; // Hiragana (except small characters)
653 }
654 if (c >= '\u30A0' && c <= '\u30FF') {
Eric Fischer549d7242009-03-31 14:19:47 -0700655 if (!includeNonStarters) {
656 switch (c) {
657 case '\u30A0': // # KATAKANA-HIRAGANA DOUBLE HYPHEN
658 case '\u30A1': // # KATAKANA LETTER SMALL A
659 case '\u30A3': // # KATAKANA LETTER SMALL I
660 case '\u30A5': // # KATAKANA LETTER SMALL U
661 case '\u30A7': // # KATAKANA LETTER SMALL E
662 case '\u30A9': // # KATAKANA LETTER SMALL O
663 case '\u30C3': // # KATAKANA LETTER SMALL TU
664 case '\u30E3': // # KATAKANA LETTER SMALL YA
665 case '\u30E5': // # KATAKANA LETTER SMALL YU
666 case '\u30E7': // # KATAKANA LETTER SMALL YO
667 case '\u30EE': // # KATAKANA LETTER SMALL WA
668 case '\u30F5': // # KATAKANA LETTER SMALL KA
669 case '\u30F6': // # KATAKANA LETTER SMALL KE
670 case '\u30FB': // # KATAKANA MIDDLE DOT
671 case '\u30FC': // # KATAKANA-HIRAGANA PROLONGED SOUND MARK
672 case '\u30FD': // # KATAKANA ITERATION MARK
673 case '\u30FE': // # KATAKANA VOICED ITERATION MARK
674 return false;
675 }
676 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800677 return true; // Katakana (except small characters)
678 }
679 if (c >= '\u3400' && c <= '\u4DB5') {
680 return true; // CJK UNIFIED IDEOGRAPHS EXTENSION A
681 }
682 if (c >= '\u4E00' && c <= '\u9FBB') {
683 return true; // CJK UNIFIED IDEOGRAPHS
684 }
685 if (c >= '\uF900' && c <= '\uFAD9') {
686 return true; // CJK COMPATIBILITY IDEOGRAPHS
687 }
688 if (c >= '\uA000' && c <= '\uA48F') {
689 return true; // YI SYLLABLES
690 }
691 if (c >= '\uA490' && c <= '\uA4CF') {
692 return true; // YI RADICALS
693 }
694 if (c >= '\uFE62' && c <= '\uFE66') {
695 return true; // SMALL PLUS SIGN to SMALL EQUALS SIGN
696 }
697 if (c >= '\uFF10' && c <= '\uFF19') {
698 return true; // WIDE DIGITS
699 }
700
701 return false;
702 }
703
704/*
705 private static void dump(byte[] data, int count, String label) {
706 if (false) {
707 System.out.print(label);
708
709 for (int i = 0; i < count; i++)
710 System.out.print(" " + data[i]);
711
712 System.out.println();
713 }
714 }
715*/
716
717 private static int getFit(TextPaint paint,
718 TextPaint workPaint,
719 CharSequence text, int start, int end,
720 float wid) {
721 int high = end + 1, low = start - 1, guess;
722
723 while (high - low > 1) {
724 guess = (high + low) / 2;
725
726 if (measureText(paint, workPaint,
727 text, start, guess, null, true, null) > wid)
728 high = guess;
729 else
730 low = guess;
731 }
732
733 if (low < start)
734 return start;
735 else
736 return low;
737 }
738
739 private int out(CharSequence text, int start, int end,
740 int above, int below, int top, int bottom, int v,
741 float spacingmult, float spacingadd,
742 LineHeightSpan[] chooseht, int[] choosehtv,
743 Paint.FontMetricsInt fm, boolean tab,
744 boolean needMultiply, int pstart, byte[] chdirs,
745 int dir, boolean easy, boolean last,
746 boolean includepad, boolean trackpad,
747 float[] widths, int widstart, int widoff,
748 TextUtils.TruncateAt ellipsize, float ellipsiswidth,
749 float textwidth, TextPaint paint) {
750 int j = mLineCount;
751 int off = j * mColumns;
752 int want = off + mColumns + TOP;
753 int[] lines = mLines;
754
755 // Log.e("text", "line " + start + " to " + end + (last ? "===" : ""));
756
757 if (want >= lines.length) {
758 int nlen = ArrayUtils.idealIntArraySize(want + 1);
759 int[] grow = new int[nlen];
760 System.arraycopy(lines, 0, grow, 0, lines.length);
761 mLines = grow;
762 lines = grow;
763
764 Directions[] grow2 = new Directions[nlen];
765 System.arraycopy(mLineDirections, 0, grow2, 0,
766 mLineDirections.length);
767 mLineDirections = grow2;
768 }
769
770 if (chooseht != null) {
771 fm.ascent = above;
772 fm.descent = below;
773 fm.top = top;
774 fm.bottom = bottom;
775
776 for (int i = 0; i < chooseht.length; i++) {
Eric Fischera9f1dd02009-08-12 15:00:10 -0700777 if (chooseht[i] instanceof LineHeightSpan.WithDensity) {
778 ((LineHeightSpan.WithDensity) chooseht[i]).
779 chooseHeight(text, start, end, choosehtv[i], v, fm, paint);
780
781 } else {
782 chooseht[i].chooseHeight(text, start, end, choosehtv[i], v, fm);
783 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800784 }
785
786 above = fm.ascent;
787 below = fm.descent;
788 top = fm.top;
789 bottom = fm.bottom;
790 }
791
792 if (j == 0) {
793 if (trackpad) {
794 mTopPadding = top - above;
795 }
796
797 if (includepad) {
798 above = top;
799 }
800 }
801 if (last) {
802 if (trackpad) {
803 mBottomPadding = bottom - below;
804 }
805
806 if (includepad) {
807 below = bottom;
808 }
809 }
810
811 int extra;
812
813 if (needMultiply) {
Doug Felt10657582010-02-22 11:19:01 -0800814 double ex = (below - above) * (spacingmult - 1) + spacingadd;
815 if (ex >= 0) {
816 extra = (int)(ex + 0.5);
817 } else {
818 extra = -(int)(-ex + 0.5);
819 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800820 } else {
821 extra = 0;
822 }
823
824 lines[off + START] = start;
825 lines[off + TOP] = v;
826 lines[off + DESCENT] = below + extra;
827
828 v += (below - above) + extra;
829 lines[off + mColumns + START] = end;
830 lines[off + mColumns + TOP] = v;
831
832 if (tab)
833 lines[off + TAB] |= TAB_MASK;
834
Doug Felt9f7a4442010-03-01 12:45:56 -0800835 lines[off + DIR] |= dir << DIR_SHIFT;
836 Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT;
837 // easy means all chars < the first RTL, so no emoji, no nothing
Doug Felt4e0c5e52010-03-15 16:56:02 -0700838 // XXX a run with no text or all spaces is easy but might be an empty
Doug Felt9f7a4442010-03-01 12:45:56 -0800839 // RTL paragraph. Make sure easy is false if this is the case.
840 if (easy) {
841 mLineDirections[j] = linedirs;
842 } else {
843 int startOff = start - pstart;
844 int baseLevel = dir == DIR_LEFT_TO_RIGHT ? 0 : 1;
845 int curLevel = chdirs[startOff];
846 int minLevel = curLevel;
847 int runCount = 1;
848 for (int i = start + 1; i < end; ++i) {
849 int level = chdirs[i - pstart];
850 if (level != curLevel) {
851 curLevel = level;
852 ++runCount;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800853 }
854 }
Doug Felt4e0c5e52010-03-15 16:56:02 -0700855
Doug Felt9f7a4442010-03-01 12:45:56 -0800856 // add final run for trailing counter-directional whitespace
857 int visEnd = end;
858 if ((curLevel & 1) != (baseLevel & 1)) {
859 // look for visible end
860 while (--visEnd >= start) {
861 char ch = text.charAt(visEnd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800862
Doug Felt9f7a4442010-03-01 12:45:56 -0800863 if (ch == '\n') {
864 --visEnd;
865 break;
866 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800867
Doug Felt9f7a4442010-03-01 12:45:56 -0800868 if (ch != ' ' && ch != '\t') {
869 break;
870 }
871 }
872 ++visEnd;
873 if (visEnd != end) {
874 ++runCount;
875 }
876 }
Doug Felt4e0c5e52010-03-15 16:56:02 -0700877
Doug Felt9f7a4442010-03-01 12:45:56 -0800878 if (runCount == 1 && minLevel == baseLevel) {
879 if ((minLevel & 1) != 0) {
880 linedirs = DIRS_ALL_RIGHT_TO_LEFT;
881 }
882 // we're done, only one run on this line
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800883 } else {
Doug Felt9f7a4442010-03-01 12:45:56 -0800884 int[] ld = new int[runCount * 2];
885 int maxLevel = minLevel;
886 int levelBits = minLevel << RUN_LEVEL_SHIFT;
887 {
888 // Start of first pair is always 0, we write
889 // length then start at each new run, and the
890 // last run length after we're done.
891 int n = 1;
892 int prev = start;
893 curLevel = minLevel;
894 for (int i = start; i < visEnd; ++i) {
895 int level = chdirs[i - pstart];
896 if (level != curLevel) {
897 curLevel = level;
898 if (level > maxLevel) {
899 maxLevel = level;
900 } else if (level < minLevel) {
901 minLevel = level;
902 }
903 // XXX ignore run length limit of 2^RUN_LEVEL_SHIFT
904 ld[n++] = (i - prev) | levelBits;
905 ld[n++] = i - start;
906 levelBits = curLevel << RUN_LEVEL_SHIFT;
907 prev = i;
908 }
909 }
910 ld[n] = (visEnd - prev) | levelBits;
911 if (visEnd < end) {
912 ld[++n] = visEnd - start;
913 ld[++n] = (end - visEnd) | (baseLevel << RUN_LEVEL_SHIFT);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800914 }
915 }
916
Doug Felt9f7a4442010-03-01 12:45:56 -0800917 // See if we need to swap any runs.
Doug Felt4e0c5e52010-03-15 16:56:02 -0700918 // If the min level run direction doesn't match the base
Doug Felt9f7a4442010-03-01 12:45:56 -0800919 // direction, we always need to swap (at this point
920 // we have more than one run).
921 // Otherwise, we don't need to swap the lowest level.
Doug Felt4e0c5e52010-03-15 16:56:02 -0700922 // Since there are no logically adjacent runs at the same
Doug Felt9f7a4442010-03-01 12:45:56 -0800923 // level, if the max level is the same as the (new) min
924 // level, we have a series of alternating levels that
925 // is already in order, so there's no more to do.
Doug Felt4e0c5e52010-03-15 16:56:02 -0700926 //
Doug Felt9f7a4442010-03-01 12:45:56 -0800927 boolean swap;
928 if ((minLevel & 1) == baseLevel) {
929 minLevel += 1;
930 swap = maxLevel > minLevel;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800931 } else {
Doug Felt9f7a4442010-03-01 12:45:56 -0800932 swap = runCount > 1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800933 }
Doug Felt9f7a4442010-03-01 12:45:56 -0800934 if (swap) {
935 for (int level = maxLevel - 1; level >= minLevel; --level) {
936 for (int i = 0; i < ld.length; i += 2) {
937 if (chdirs[startOff + ld[i]] >= level) {
938 int e = i + 2;
939 while (e < ld.length && chdirs[startOff + ld[e]] >= level) {
940 e += 2;
941 }
942 for (int low = i, hi = e - 2; low < hi; low += 2, hi -= 2) {
943 int x = ld[low]; ld[low] = ld[hi]; ld[hi] = x;
944 x = ld[low+1]; ld[low+1] = ld[hi+1]; ld[hi+1] = x;
945 }
946 i = e + 2;
947 }
948 }
949 }
950 }
951 linedirs = new Directions(ld);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800952 }
953
954 mLineDirections[j] = linedirs;
955
956 // If ellipsize is in marquee mode, do not apply ellipsis on the first line
957 if (ellipsize != null && (ellipsize != TextUtils.TruncateAt.MARQUEE || j != 0)) {
958 calculateEllipsis(start, end, widths, widstart, widoff,
959 ellipsiswidth, ellipsize, j,
960 textwidth, paint);
961 }
962 }
963
964 mLineCount++;
965 return v;
966 }
967
968 private void calculateEllipsis(int linestart, int lineend,
969 float[] widths, int widstart, int widoff,
970 float avail, TextUtils.TruncateAt where,
971 int line, float textwidth, TextPaint paint) {
972 int len = lineend - linestart;
973
974 if (textwidth <= avail) {
975 // Everything fits!
976 mLines[mColumns * line + ELLIPSIS_START] = 0;
977 mLines[mColumns * line + ELLIPSIS_COUNT] = 0;
978 return;
979 }
980
981 float ellipsiswid = paint.measureText("\u2026");
982 int ellipsisStart, ellipsisCount;
983
984 if (where == TextUtils.TruncateAt.START) {
985 float sum = 0;
986 int i;
987
988 for (i = len; i >= 0; i--) {
989 float w = widths[i - 1 + linestart - widstart + widoff];
990
991 if (w + sum + ellipsiswid > avail) {
992 break;
993 }
994
995 sum += w;
996 }
997
998 ellipsisStart = 0;
999 ellipsisCount = i;
1000 } else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE) {
1001 float sum = 0;
1002 int i;
1003
1004 for (i = 0; i < len; i++) {
1005 float w = widths[i + linestart - widstart + widoff];
1006
1007 if (w + sum + ellipsiswid > avail) {
1008 break;
1009 }
1010
1011 sum += w;
1012 }
1013
1014 ellipsisStart = i;
1015 ellipsisCount = len - i;
1016 } else /* where = TextUtils.TruncateAt.MIDDLE */ {
1017 float lsum = 0, rsum = 0;
1018 int left = 0, right = len;
1019
1020 float ravail = (avail - ellipsiswid) / 2;
1021 for (right = len; right >= 0; right--) {
1022 float w = widths[right - 1 + linestart - widstart + widoff];
1023
1024 if (w + rsum > ravail) {
1025 break;
1026 }
1027
1028 rsum += w;
1029 }
1030
1031 float lavail = avail - ellipsiswid - rsum;
1032 for (left = 0; left < right; left++) {
1033 float w = widths[left + linestart - widstart + widoff];
1034
1035 if (w + lsum > lavail) {
1036 break;
1037 }
1038
1039 lsum += w;
1040 }
1041
1042 ellipsisStart = left;
1043 ellipsisCount = right - left;
1044 }
1045
1046 mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
1047 mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
1048 }
1049
1050 // Override the baseclass so we can directly access our members,
1051 // rather than relying on member functions.
1052 // The logic mirrors that of Layout.getLineForVertical
1053 // FIXME: It may be faster to do a linear search for layouts without many lines.
1054 public int getLineForVertical(int vertical) {
1055 int high = mLineCount;
1056 int low = -1;
1057 int guess;
1058 int[] lines = mLines;
1059 while (high - low > 1) {
1060 guess = (high + low) >> 1;
1061 if (lines[mColumns * guess + TOP] > vertical){
1062 high = guess;
1063 } else {
1064 low = guess;
1065 }
1066 }
1067 if (low < 0) {
1068 return 0;
1069 } else {
1070 return low;
1071 }
1072 }
1073
1074 public int getLineCount() {
1075 return mLineCount;
1076 }
1077
1078 public int getLineTop(int line) {
Doug Felt4e0c5e52010-03-15 16:56:02 -07001079 return mLines[mColumns * line + TOP];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001080 }
1081
1082 public int getLineDescent(int line) {
Doug Felt4e0c5e52010-03-15 16:56:02 -07001083 return mLines[mColumns * line + DESCENT];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001084 }
1085
1086 public int getLineStart(int line) {
1087 return mLines[mColumns * line + START] & START_MASK;
1088 }
1089
1090 public int getParagraphDirection(int line) {
1091 return mLines[mColumns * line + DIR] >> DIR_SHIFT;
1092 }
1093
1094 public boolean getLineContainsTab(int line) {
1095 return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
1096 }
1097
1098 public final Directions getLineDirections(int line) {
1099 return mLineDirections[line];
1100 }
1101
1102 public int getTopPadding() {
1103 return mTopPadding;
1104 }
1105
1106 public int getBottomPadding() {
1107 return mBottomPadding;
1108 }
1109
1110 @Override
1111 public int getEllipsisCount(int line) {
1112 if (mColumns < COLUMNS_ELLIPSIZE) {
1113 return 0;
1114 }
1115
1116 return mLines[mColumns * line + ELLIPSIS_COUNT];
1117 }
1118
1119 @Override
1120 public int getEllipsisStart(int line) {
1121 if (mColumns < COLUMNS_ELLIPSIZE) {
1122 return 0;
1123 }
1124
1125 return mLines[mColumns * line + ELLIPSIS_START];
1126 }
1127
1128 @Override
1129 public int getEllipsizedWidth() {
1130 return mEllipsizedWidth;
1131 }
1132
1133 private int mLineCount;
1134 private int mTopPadding, mBottomPadding;
1135 private int mColumns;
1136 private int mEllipsizedWidth;
1137
1138 private static final int COLUMNS_NORMAL = 3;
1139 private static final int COLUMNS_ELLIPSIZE = 5;
1140 private static final int START = 0;
1141 private static final int DIR = START;
1142 private static final int TAB = START;
1143 private static final int TOP = 1;
1144 private static final int DESCENT = 2;
1145 private static final int ELLIPSIS_START = 3;
1146 private static final int ELLIPSIS_COUNT = 4;
1147
1148 private int[] mLines;
1149 private Directions[] mLineDirections;
1150
1151 private static final int START_MASK = 0x1FFFFFFF;
1152 private static final int DIR_MASK = 0xC0000000;
1153 private static final int DIR_SHIFT = 30;
1154 private static final int TAB_MASK = 0x20000000;
1155
1156 private static final char FIRST_RIGHT_TO_LEFT = '\u0590';
1157
1158 /*
1159 * These are reused across calls to generate()
1160 */
1161 private byte[] mChdirs;
1162 private char[] mChs;
1163 private float[] mWidths;
1164 private Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
1165}