blob: 38ac9b7322796fa507081b806054b4ca7c4676be [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
The Android Open Source Project10592532009-03-18 17:39:46 -070019import android.emoji.EmojiFactory;
20import android.graphics.Bitmap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.Rect;
Eric Fischerc2d54f42009-03-27 15:52:38 -070024import android.graphics.RectF;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025import android.graphics.Path;
26import com.android.internal.util.ArrayUtils;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027
28import junit.framework.Assert;
29import android.text.style.*;
30import android.text.method.TextKeyListener;
31import android.view.KeyEvent;
32
33/**
34 * A base class that manages text layout in visual elements on
35 * the screen.
36 * <p>For text that will be edited, use a {@link DynamicLayout},
37 * which will be updated as the text changes.
38 * For text that will not change, use a {@link StaticLayout}.
39 */
40public abstract class Layout {
Dave Bort76c02262009-04-13 17:17:21 -070041 private static final boolean DEBUG = false;
Doug Felt71b8dd72010-02-16 17:27:09 -080042 private static final ParagraphStyle[] NO_PARA_SPANS =
43 ArrayUtils.emptyArray(ParagraphStyle.class);
Dave Bort76c02262009-04-13 17:17:21 -070044
The Android Open Source Project10592532009-03-18 17:39:46 -070045 /* package */ static final EmojiFactory EMOJI_FACTORY =
46 EmojiFactory.newAvailableInstance();
47 /* package */ static final int MIN_EMOJI, MAX_EMOJI;
48
49 static {
50 if (EMOJI_FACTORY != null) {
51 MIN_EMOJI = EMOJI_FACTORY.getMinimumAndroidPua();
52 MAX_EMOJI = EMOJI_FACTORY.getMaximumAndroidPua();
53 } else {
54 MIN_EMOJI = -1;
55 MAX_EMOJI = -1;
56 }
57 };
58
Eric Fischerc2d54f42009-03-27 15:52:38 -070059 private RectF mEmojiRect;
60
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080061 /**
Doug Felt71b8dd72010-02-16 17:27:09 -080062 * Return how wide a layout must be in order to display the
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063 * specified text with one line per paragraph.
64 */
65 public static float getDesiredWidth(CharSequence source,
66 TextPaint paint) {
67 return getDesiredWidth(source, 0, source.length(), paint);
68 }
69
70 /**
Doug Felt71b8dd72010-02-16 17:27:09 -080071 * Return how wide a layout must be in order to display the
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080072 * specified text slice with one line per paragraph.
73 */
74 public static float getDesiredWidth(CharSequence source,
75 int start, int end,
76 TextPaint paint) {
77 float need = 0;
78 TextPaint workPaint = new TextPaint();
79
80 int next;
81 for (int i = start; i <= end; i = next) {
82 next = TextUtils.indexOf(source, '\n', i, end);
83
84 if (next < 0)
85 next = end;
86
Doug Felt71b8dd72010-02-16 17:27:09 -080087 // note, omits trailing paragraph char
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080088 float w = measureText(paint, workPaint,
89 source, i, next, null, true, null);
90
91 if (w > need)
92 need = w;
93
94 next++;
95 }
96
97 return need;
98 }
99
100 /**
101 * Subclasses of Layout use this constructor to set the display text,
102 * width, and other standard properties.
Doug Felt71b8dd72010-02-16 17:27:09 -0800103 * @param text the text to render
104 * @param paint the default paint for the layout. Styles can override
105 * various attributes of the paint.
106 * @param width the wrapping width for the text.
107 * @param align whether to left, right, or center the text. Styles can
108 * override the alignment.
109 * @param spacingMult factor by which to scale the font size to get the
110 * default line spacing
111 * @param spacingAdd amount to add to the default line spacing
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800112 */
113 protected Layout(CharSequence text, TextPaint paint,
114 int width, Alignment align,
Doug Felt71b8dd72010-02-16 17:27:09 -0800115 float spacingMult, float spacingAdd) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800116 if (width < 0)
117 throw new IllegalArgumentException("Layout: " + width + " < 0");
118
119 mText = text;
120 mPaint = paint;
121 mWorkPaint = new TextPaint();
122 mWidth = width;
123 mAlignment = align;
Doug Felt71b8dd72010-02-16 17:27:09 -0800124 mSpacingMult = spacingMult;
125 mSpacingAdd = spacingAdd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800126 mSpannedText = text instanceof Spanned;
127 }
128
129 /**
130 * Replace constructor properties of this Layout with new ones. Be careful.
131 */
132 /* package */ void replaceWith(CharSequence text, TextPaint paint,
133 int width, Alignment align,
134 float spacingmult, float spacingadd) {
135 if (width < 0) {
136 throw new IllegalArgumentException("Layout: " + width + " < 0");
137 }
138
139 mText = text;
140 mPaint = paint;
141 mWidth = width;
142 mAlignment = align;
143 mSpacingMult = spacingmult;
144 mSpacingAdd = spacingadd;
145 mSpannedText = text instanceof Spanned;
146 }
147
148 /**
149 * Draw this Layout on the specified Canvas.
150 */
151 public void draw(Canvas c) {
152 draw(c, null, null, 0);
153 }
154
155 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800156 * Draw this Layout on the specified canvas, with the highlight path drawn
157 * between the background and the text.
158 *
159 * @param c the canvas
160 * @param highlight the path of the highlight or cursor; can be null
161 * @param highlightPaint the paint for the highlight
162 * @param cursorOffsetVertical the amount to temporarily translate the
163 * canvas while rendering the highlight
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800164 */
Doug Felt71b8dd72010-02-16 17:27:09 -0800165 public void draw(Canvas c, Path highlight, Paint highlightPaint,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800166 int cursorOffsetVertical) {
167 int dtop, dbottom;
168
169 synchronized (sTempRect) {
170 if (!c.getClipBounds(sTempRect)) {
171 return;
172 }
173
174 dtop = sTempRect.top;
175 dbottom = sTempRect.bottom;
176 }
177
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800178
179 int top = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800180 int bottom = getLineTop(getLineCount());
181
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800182 if (dtop > top) {
183 top = dtop;
184 }
185 if (dbottom < bottom) {
186 bottom = dbottom;
187 }
188
189 int first = getLineForVertical(top);
190 int last = getLineForVertical(bottom);
191
192 int previousLineBottom = getLineTop(first);
193 int previousLineEnd = getLineStart(first);
194
Doug Felt71b8dd72010-02-16 17:27:09 -0800195 TextPaint paint = mPaint;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800196 CharSequence buf = mText;
Doug Felt71b8dd72010-02-16 17:27:09 -0800197 int width = mWidth;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800198 boolean spannedText = mSpannedText;
199
Doug Felt71b8dd72010-02-16 17:27:09 -0800200 ParagraphStyle[] spans = NO_PARA_SPANS;
201 int spanend = 0;
202 int textLength = 0;
203
204 // First, draw LineBackgroundSpans.
205 // LineBackgroundSpans know nothing about the alignment or direction of
206 // the layout or line. XXX: Should they?
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800207 if (spannedText) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800208 textLength = buf.length();
209 for (int i = first; i <= last; i++) {
210 int start = previousLineEnd;
211 int end = getLineStart(i+1);
212 previousLineEnd = end;
213
214 int ltop = previousLineBottom;
215 int lbottom = getLineTop(i+1);
216 previousLineBottom = lbottom;
217 int lbaseline = lbottom - getLineDescent(i);
218
219 if (start >= spanend) {
220 Spanned sp = (Spanned) buf;
221 spanend = sp.nextSpanTransition(start, textLength,
222 LineBackgroundSpan.class);
223 spans = sp.getSpans(start, spanend,
224 LineBackgroundSpan.class);
225 }
226
227 for (int n = 0; n < spans.length; n++) {
228 LineBackgroundSpan back = (LineBackgroundSpan) spans[n];
229
Doug Felt71b8dd72010-02-16 17:27:09 -0800230 back.drawBackground(c, paint, 0, width,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800231 ltop, lbaseline, lbottom,
232 buf, start, end,
233 i);
234 }
235 }
236 // reset to their original values
237 spanend = 0;
238 previousLineBottom = getLineTop(first);
239 previousLineEnd = getLineStart(first);
Doug Felt71b8dd72010-02-16 17:27:09 -0800240 spans = NO_PARA_SPANS;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800241 }
242
243 // There can be a highlight even without spans if we are drawing
244 // a non-spanned transformation of a spanned editing buffer.
245 if (highlight != null) {
246 if (cursorOffsetVertical != 0) {
247 c.translate(0, cursorOffsetVertical);
248 }
249
Doug Felt71b8dd72010-02-16 17:27:09 -0800250 c.drawPath(highlight, highlightPaint);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800251
252 if (cursorOffsetVertical != 0) {
253 c.translate(0, -cursorOffsetVertical);
254 }
255 }
256
257 Alignment align = mAlignment;
258
Doug Felt71b8dd72010-02-16 17:27:09 -0800259 // Next draw the lines, one at a time.
260 // the baseline is the top of the following line minus the current
261 // line's descent.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800262 for (int i = first; i <= last; i++) {
263 int start = previousLineEnd;
264
265 previousLineEnd = getLineStart(i+1);
266 int end = getLineVisibleEnd(i, start, previousLineEnd);
267
268 int ltop = previousLineBottom;
269 int lbottom = getLineTop(i+1);
270 previousLineBottom = lbottom;
271 int lbaseline = lbottom - getLineDescent(i);
272
Doug Felt71b8dd72010-02-16 17:27:09 -0800273 boolean isFirstParaLine = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800274 if (spannedText) {
275 if (start == 0 || buf.charAt(start - 1) == '\n') {
Doug Felt71b8dd72010-02-16 17:27:09 -0800276 isFirstParaLine = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800277 }
Doug Felt71b8dd72010-02-16 17:27:09 -0800278 // New batch of paragraph styles, compute the alignment.
279 // Last alignment style wins.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800280 if (start >= spanend) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800281 Spanned sp = (Spanned) buf;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800282 spanend = sp.nextSpanTransition(start, textLength,
283 ParagraphStyle.class);
284 spans = sp.getSpans(start, spanend, ParagraphStyle.class);
285
286 align = mAlignment;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800287 for (int n = spans.length-1; n >= 0; n--) {
288 if (spans[n] instanceof AlignmentSpan) {
289 align = ((AlignmentSpan) spans[n]).getAlignment();
290 break;
291 }
292 }
293 }
294 }
295
296 int dir = getParagraphDirection(i);
297 int left = 0;
298 int right = mWidth;
299
Doug Felt71b8dd72010-02-16 17:27:09 -0800300 // Draw all leading margin spans. Adjust left or right according
301 // to the paragraph direction of the line.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800302 if (spannedText) {
303 final int length = spans.length;
304 for (int n = 0; n < length; n++) {
305 if (spans[n] instanceof LeadingMarginSpan) {
306 LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
307
308 if (dir == DIR_RIGHT_TO_LEFT) {
309 margin.drawLeadingMargin(c, paint, right, dir, ltop,
310 lbaseline, lbottom, buf,
Doug Felt71b8dd72010-02-16 17:27:09 -0800311 start, end, isFirstParaLine, this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800312
Doug Felt71b8dd72010-02-16 17:27:09 -0800313 right -= margin.getLeadingMargin(isFirstParaLine);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800314 } else {
315 margin.drawLeadingMargin(c, paint, left, dir, ltop,
316 lbaseline, lbottom, buf,
Doug Felt71b8dd72010-02-16 17:27:09 -0800317 start, end, isFirstParaLine, this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800318
Doug Felt71b8dd72010-02-16 17:27:09 -0800319 boolean useMargin = isFirstParaLine;
Mark Wagner7b5676e2009-10-16 11:44:23 -0700320 if (margin instanceof LeadingMarginSpan.LeadingMarginSpan2) {
321 int count = ((LeadingMarginSpan.LeadingMarginSpan2)margin).getLeadingMarginLineCount();
322 useMargin = count > i;
323 }
324 left += margin.getLeadingMargin(useMargin);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800325 }
326 }
327 }
328 }
329
Doug Felt71b8dd72010-02-16 17:27:09 -0800330 // Adjust the point at which to start rendering depending on the
331 // alignment of the paragraph.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800332 int x;
333 if (align == Alignment.ALIGN_NORMAL) {
334 if (dir == DIR_LEFT_TO_RIGHT) {
335 x = left;
336 } else {
337 x = right;
338 }
339 } else {
340 int max = (int)getLineMax(i, spans, false);
341 if (align == Alignment.ALIGN_OPPOSITE) {
342 if (dir == DIR_RIGHT_TO_LEFT) {
343 x = left + max;
344 } else {
345 x = right - max;
346 }
347 } else {
348 // Alignment.ALIGN_CENTER
349 max = max & ~1;
350 int half = (right - left - max) >> 1;
351 if (dir == DIR_RIGHT_TO_LEFT) {
352 x = right - half;
353 } else {
354 x = left + half;
355 }
356 }
357 }
358
359 Directions directions = getLineDirections(i);
360 boolean hasTab = getLineContainsTab(i);
361 if (directions == DIRS_ALL_LEFT_TO_RIGHT &&
362 !spannedText && !hasTab) {
Dave Bort76c02262009-04-13 17:17:21 -0700363 if (DEBUG) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800364 Assert.assertTrue(dir == DIR_LEFT_TO_RIGHT);
365 Assert.assertNotNull(c);
366 }
Doug Felt71b8dd72010-02-16 17:27:09 -0800367 // XXX: assumes there's nothing additional to be done
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800368 c.drawText(buf, start, end, x, lbaseline, paint);
369 } else {
370 drawText(c, buf, start, end, dir, directions,
371 x, ltop, lbaseline, lbottom, paint, mWorkPaint,
372 hasTab, spans);
373 }
374 }
375 }
376
377 /**
378 * Return the text that is displayed by this Layout.
379 */
380 public final CharSequence getText() {
381 return mText;
382 }
383
384 /**
385 * Return the base Paint properties for this layout.
386 * Do NOT change the paint, which may result in funny
387 * drawing for this layout.
388 */
389 public final TextPaint getPaint() {
390 return mPaint;
391 }
392
393 /**
394 * Return the width of this layout.
395 */
396 public final int getWidth() {
397 return mWidth;
398 }
399
400 /**
401 * Return the width to which this Layout is ellipsizing, or
402 * {@link #getWidth} if it is not doing anything special.
403 */
404 public int getEllipsizedWidth() {
405 return mWidth;
406 }
407
408 /**
409 * Increase the width of this layout to the specified width.
Doug Felt71b8dd72010-02-16 17:27:09 -0800410 * Be careful to use this only when you know it is appropriate&mdash;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800411 * it does not cause the text to reflow to use the full new width.
412 */
413 public final void increaseWidthTo(int wid) {
414 if (wid < mWidth) {
415 throw new RuntimeException("attempted to reduce Layout width");
416 }
417
418 mWidth = wid;
419 }
420
421 /**
422 * Return the total height of this layout.
423 */
424 public int getHeight() {
Doug Felt71b8dd72010-02-16 17:27:09 -0800425 return getLineTop(getLineCount());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800426 }
427
428 /**
429 * Return the base alignment of this layout.
430 */
431 public final Alignment getAlignment() {
432 return mAlignment;
433 }
434
435 /**
436 * Return what the text height is multiplied by to get the line height.
437 */
438 public final float getSpacingMultiplier() {
439 return mSpacingMult;
440 }
441
442 /**
443 * Return the number of units of leading that are added to each line.
444 */
445 public final float getSpacingAdd() {
446 return mSpacingAdd;
447 }
448
449 /**
450 * Return the number of lines of text in this layout.
451 */
452 public abstract int getLineCount();
453
454 /**
455 * Return the baseline for the specified line (0&hellip;getLineCount() - 1)
456 * If bounds is not null, return the top, left, right, bottom extents
457 * of the specified line in it.
458 * @param line which line to examine (0..getLineCount() - 1)
459 * @param bounds Optional. If not null, it returns the extent of the line
460 * @return the Y-coordinate of the baseline
461 */
462 public int getLineBounds(int line, Rect bounds) {
463 if (bounds != null) {
464 bounds.left = 0; // ???
465 bounds.top = getLineTop(line);
466 bounds.right = mWidth; // ???
Doug Felt71b8dd72010-02-16 17:27:09 -0800467 bounds.bottom = getLineTop(line + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800468 }
469 return getLineBaseline(line);
470 }
471
472 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800473 * Return the vertical position of the top of the specified line
474 * (0&hellip;getLineCount()).
475 * If the specified line is equal to the line count, returns the
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800476 * bottom of the last line.
477 */
478 public abstract int getLineTop(int line);
479
480 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800481 * Return the descent of the specified line(0&hellip;getLineCount() - 1).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800482 */
483 public abstract int getLineDescent(int line);
484
485 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800486 * Return the text offset of the beginning of the specified line (
487 * 0&hellip;getLineCount()). If the specified line is equal to the line
488 * count, returns the length of the text.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800489 */
490 public abstract int getLineStart(int line);
491
492 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800493 * Returns the primary directionality of the paragraph containing the
494 * specified line, either 1 for left-to-right lines, or -1 for right-to-left
495 * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800496 */
497 public abstract int getParagraphDirection(int line);
498
499 /**
The Android Open Source Project10592532009-03-18 17:39:46 -0700500 * Returns whether the specified line contains one or more
501 * characters that need to be handled specially, like tabs
502 * or emoji.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800503 */
504 public abstract boolean getLineContainsTab(int line);
505
506 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800507 * Returns the directional run information for the specified line.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800508 * The array alternates counts of characters in left-to-right
509 * and right-to-left segments of the line.
Doug Felt71b8dd72010-02-16 17:27:09 -0800510 *
511 * <p>NOTE: this is inadequate to support bidirectional text, and will change.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800512 */
513 public abstract Directions getLineDirections(int line);
514
515 /**
516 * Returns the (negative) number of extra pixels of ascent padding in the
517 * top line of the Layout.
518 */
519 public abstract int getTopPadding();
520
521 /**
522 * Returns the number of extra pixels of descent padding in the
523 * bottom line of the Layout.
524 */
525 public abstract int getBottomPadding();
526
527 /**
528 * Get the primary horizontal position for the specified text offset.
529 * This is the location where a new character would be inserted in
530 * the paragraph's primary direction.
531 */
532 public float getPrimaryHorizontal(int offset) {
533 return getHorizontal(offset, false, true);
534 }
535
536 /**
537 * Get the secondary horizontal position for the specified text offset.
538 * This is the location where a new character would be inserted in
539 * the direction other than the paragraph's primary direction.
540 */
541 public float getSecondaryHorizontal(int offset) {
542 return getHorizontal(offset, true, true);
543 }
544
545 private float getHorizontal(int offset, boolean trailing, boolean alt) {
546 int line = getLineForOffset(offset);
547
548 return getHorizontal(offset, trailing, alt, line);
549 }
550
551 private float getHorizontal(int offset, boolean trailing, boolean alt,
552 int line) {
553 int start = getLineStart(line);
554 int end = getLineVisibleEnd(line);
555 int dir = getParagraphDirection(line);
556 boolean tab = getLineContainsTab(line);
557 Directions directions = getLineDirections(line);
558
559 TabStopSpan[] tabs = null;
560 if (tab && mText instanceof Spanned) {
561 tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
562 }
563
564 float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end,
565 dir, directions, trailing, alt, tab, tabs);
566
567 if (offset > end) {
568 if (dir == DIR_RIGHT_TO_LEFT)
569 wid -= measureText(mPaint, mWorkPaint,
570 mText, end, offset, null, tab, tabs);
571 else
572 wid += measureText(mPaint, mWorkPaint,
573 mText, end, offset, null, tab, tabs);
574 }
575
576 Alignment align = getParagraphAlignment(line);
577 int left = getParagraphLeft(line);
578 int right = getParagraphRight(line);
579
580 if (align == Alignment.ALIGN_NORMAL) {
581 if (dir == DIR_RIGHT_TO_LEFT)
582 return right + wid;
583 else
584 return left + wid;
585 }
586
587 float max = getLineMax(line);
588
589 if (align == Alignment.ALIGN_OPPOSITE) {
590 if (dir == DIR_RIGHT_TO_LEFT)
591 return left + max + wid;
592 else
593 return right - max + wid;
594 } else { /* align == Alignment.ALIGN_CENTER */
595 int imax = ((int) max) & ~1;
596
597 if (dir == DIR_RIGHT_TO_LEFT)
598 return right - (((right - left) - imax) / 2) + wid;
599 else
600 return left + ((right - left) - imax) / 2 + wid;
601 }
602 }
603
604 /**
605 * Get the leftmost position that should be exposed for horizontal
606 * scrolling on the specified line.
607 */
608 public float getLineLeft(int line) {
609 int dir = getParagraphDirection(line);
610 Alignment align = getParagraphAlignment(line);
611
612 if (align == Alignment.ALIGN_NORMAL) {
613 if (dir == DIR_RIGHT_TO_LEFT)
614 return getParagraphRight(line) - getLineMax(line);
615 else
616 return 0;
617 } else if (align == Alignment.ALIGN_OPPOSITE) {
618 if (dir == DIR_RIGHT_TO_LEFT)
619 return 0;
620 else
621 return mWidth - getLineMax(line);
622 } else { /* align == Alignment.ALIGN_CENTER */
623 int left = getParagraphLeft(line);
624 int right = getParagraphRight(line);
625 int max = ((int) getLineMax(line)) & ~1;
626
627 return left + ((right - left) - max) / 2;
628 }
629 }
630
631 /**
632 * Get the rightmost position that should be exposed for horizontal
633 * scrolling on the specified line.
634 */
635 public float getLineRight(int line) {
636 int dir = getParagraphDirection(line);
637 Alignment align = getParagraphAlignment(line);
638
639 if (align == Alignment.ALIGN_NORMAL) {
640 if (dir == DIR_RIGHT_TO_LEFT)
641 return mWidth;
642 else
643 return getParagraphLeft(line) + getLineMax(line);
644 } else if (align == Alignment.ALIGN_OPPOSITE) {
645 if (dir == DIR_RIGHT_TO_LEFT)
646 return getLineMax(line);
647 else
648 return mWidth;
649 } else { /* align == Alignment.ALIGN_CENTER */
650 int left = getParagraphLeft(line);
651 int right = getParagraphRight(line);
652 int max = ((int) getLineMax(line)) & ~1;
653
654 return right - ((right - left) - max) / 2;
655 }
656 }
657
658 /**
659 * Gets the horizontal extent of the specified line, excluding
660 * trailing whitespace.
661 */
662 public float getLineMax(int line) {
663 return getLineMax(line, null, false);
664 }
665
666 /**
667 * Gets the horizontal extent of the specified line, including
668 * trailing whitespace.
669 */
670 public float getLineWidth(int line) {
671 return getLineMax(line, null, true);
672 }
673
674 private float getLineMax(int line, Object[] tabs, boolean full) {
675 int start = getLineStart(line);
676 int end;
677
678 if (full) {
679 end = getLineEnd(line);
680 } else {
681 end = getLineVisibleEnd(line);
682 }
683 boolean tab = getLineContainsTab(line);
684
685 if (tabs == null && tab && mText instanceof Spanned) {
686 tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
687 }
688
689 return measureText(mPaint, mWorkPaint,
690 mText, start, end, null, tab, tabs);
691 }
692
693 /**
694 * Get the line number corresponding to the specified vertical position.
695 * If you ask for a position above 0, you get 0; if you ask for a position
696 * below the bottom of the text, you get the last line.
697 */
698 // FIXME: It may be faster to do a linear search for layouts without many lines.
699 public int getLineForVertical(int vertical) {
700 int high = getLineCount(), low = -1, guess;
701
702 while (high - low > 1) {
703 guess = (high + low) / 2;
704
705 if (getLineTop(guess) > vertical)
706 high = guess;
707 else
708 low = guess;
709 }
710
711 if (low < 0)
712 return 0;
713 else
714 return low;
715 }
716
717 /**
718 * Get the line number on which the specified text offset appears.
719 * If you ask for a position before 0, you get 0; if you ask for a position
720 * beyond the end of the text, you get the last line.
721 */
722 public int getLineForOffset(int offset) {
723 int high = getLineCount(), low = -1, guess;
724
725 while (high - low > 1) {
726 guess = (high + low) / 2;
727
728 if (getLineStart(guess) > offset)
729 high = guess;
730 else
731 low = guess;
732 }
733
734 if (low < 0)
735 return 0;
736 else
737 return low;
738 }
739
740 /**
741 * Get the character offset on the specfied line whose position is
742 * closest to the specified horizontal position.
743 */
744 public int getOffsetForHorizontal(int line, float horiz) {
745 int max = getLineEnd(line) - 1;
746 int min = getLineStart(line);
747 Directions dirs = getLineDirections(line);
748
749 if (line == getLineCount() - 1)
750 max++;
751
752 int best = min;
753 float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
754
755 int here = min;
756 for (int i = 0; i < dirs.mDirections.length; i++) {
757 int there = here + dirs.mDirections[i];
758 int swap = ((i & 1) == 0) ? 1 : -1;
759
760 if (there > max)
761 there = max;
762
763 int high = there - 1 + 1, low = here + 1 - 1, guess;
764
765 while (high - low > 1) {
766 guess = (high + low) / 2;
767 int adguess = getOffsetAtStartOf(guess);
768
769 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
770 high = guess;
771 else
772 low = guess;
773 }
774
775 if (low < here + 1)
776 low = here + 1;
777
778 if (low < there) {
779 low = getOffsetAtStartOf(low);
780
781 float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
782
783 int aft = TextUtils.getOffsetAfter(mText, low);
784 if (aft < there) {
785 float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
786
787 if (other < dist) {
788 dist = other;
789 low = aft;
790 }
791 }
792
793 if (dist < bestdist) {
794 bestdist = dist;
795 best = low;
796 }
797 }
798
799 float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
800
801 if (dist < bestdist) {
802 bestdist = dist;
803 best = here;
804 }
805
806 here = there;
807 }
808
809 float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
810
811 if (dist < bestdist) {
812 bestdist = dist;
813 best = max;
814 }
815
816 return best;
817 }
818
819 /**
820 * Return the text offset after the last character on the specified line.
821 */
822 public final int getLineEnd(int line) {
823 return getLineStart(line + 1);
824 }
825
826 /**
827 * Return the text offset after the last visible character (so whitespace
828 * is not counted) on the specified line.
829 */
830 public int getLineVisibleEnd(int line) {
831 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
832 }
833
834 private int getLineVisibleEnd(int line, int start, int end) {
Dave Bort76c02262009-04-13 17:17:21 -0700835 if (DEBUG) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800836 Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end);
837 }
838
839 CharSequence text = mText;
840 char ch;
841 if (line == getLineCount() - 1) {
842 return end;
843 }
844
845 for (; end > start; end--) {
846 ch = text.charAt(end - 1);
847
848 if (ch == '\n') {
849 return end - 1;
850 }
851
852 if (ch != ' ' && ch != '\t') {
853 break;
854 }
855
856 }
857
858 return end;
859 }
860
861 /**
862 * Return the vertical position of the bottom of the specified line.
863 */
864 public final int getLineBottom(int line) {
865 return getLineTop(line + 1);
866 }
867
868 /**
869 * Return the vertical position of the baseline of the specified line.
870 */
871 public final int getLineBaseline(int line) {
872 // getLineTop(line+1) == getLineTop(line)
873 return getLineTop(line+1) - getLineDescent(line);
874 }
875
876 /**
877 * Get the ascent of the text on the specified line.
878 * The return value is negative to match the Paint.ascent() convention.
879 */
880 public final int getLineAscent(int line) {
881 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
882 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
883 }
884
885 /**
886 * Return the text offset that would be reached by moving left
887 * (possibly onto another line) from the specified offset.
888 */
889 public int getOffsetToLeftOf(int offset) {
890 int line = getLineForOffset(offset);
891 int start = getLineStart(line);
892 int end = getLineEnd(line);
893 Directions dirs = getLineDirections(line);
894
895 if (line != getLineCount() - 1)
896 end--;
897
898 float horiz = getPrimaryHorizontal(offset);
899
900 int best = offset;
901 float besth = Integer.MIN_VALUE;
902 int candidate;
903
904 candidate = TextUtils.getOffsetBefore(mText, offset);
905 if (candidate >= start && candidate <= end) {
906 float h = getPrimaryHorizontal(candidate);
907
908 if (h < horiz && h > besth) {
909 best = candidate;
910 besth = h;
911 }
912 }
913
914 candidate = TextUtils.getOffsetAfter(mText, offset);
915 if (candidate >= start && candidate <= end) {
916 float h = getPrimaryHorizontal(candidate);
917
918 if (h < horiz && h > besth) {
919 best = candidate;
920 besth = h;
921 }
922 }
923
924 int here = start;
925 for (int i = 0; i < dirs.mDirections.length; i++) {
926 int there = here + dirs.mDirections[i];
927 if (there > end)
928 there = end;
929
930 float h = getPrimaryHorizontal(here);
931
932 if (h < horiz && h > besth) {
933 best = here;
934 besth = h;
935 }
936
937 candidate = TextUtils.getOffsetAfter(mText, here);
938 if (candidate >= start && candidate <= end) {
939 h = getPrimaryHorizontal(candidate);
940
941 if (h < horiz && h > besth) {
942 best = candidate;
943 besth = h;
944 }
945 }
946
947 candidate = TextUtils.getOffsetBefore(mText, there);
948 if (candidate >= start && candidate <= end) {
949 h = getPrimaryHorizontal(candidate);
950
951 if (h < horiz && h > besth) {
952 best = candidate;
953 besth = h;
954 }
955 }
956
957 here = there;
958 }
959
960 float h = getPrimaryHorizontal(end);
961
962 if (h < horiz && h > besth) {
963 best = end;
964 besth = h;
965 }
966
967 if (best != offset)
968 return best;
969
970 int dir = getParagraphDirection(line);
971
972 if (dir > 0) {
973 if (line == 0)
974 return best;
975 else
976 return getOffsetForHorizontal(line - 1, 10000);
977 } else {
978 if (line == getLineCount() - 1)
979 return best;
980 else
981 return getOffsetForHorizontal(line + 1, 10000);
982 }
983 }
984
985 /**
986 * Return the text offset that would be reached by moving right
987 * (possibly onto another line) from the specified offset.
988 */
989 public int getOffsetToRightOf(int offset) {
990 int line = getLineForOffset(offset);
991 int start = getLineStart(line);
992 int end = getLineEnd(line);
993 Directions dirs = getLineDirections(line);
994
995 if (line != getLineCount() - 1)
996 end--;
997
998 float horiz = getPrimaryHorizontal(offset);
999
1000 int best = offset;
1001 float besth = Integer.MAX_VALUE;
1002 int candidate;
1003
1004 candidate = TextUtils.getOffsetBefore(mText, offset);
1005 if (candidate >= start && candidate <= end) {
1006 float h = getPrimaryHorizontal(candidate);
1007
1008 if (h > horiz && h < besth) {
1009 best = candidate;
1010 besth = h;
1011 }
1012 }
1013
1014 candidate = TextUtils.getOffsetAfter(mText, offset);
1015 if (candidate >= start && candidate <= end) {
1016 float h = getPrimaryHorizontal(candidate);
1017
1018 if (h > horiz && h < besth) {
1019 best = candidate;
1020 besth = h;
1021 }
1022 }
1023
1024 int here = start;
1025 for (int i = 0; i < dirs.mDirections.length; i++) {
1026 int there = here + dirs.mDirections[i];
1027 if (there > end)
1028 there = end;
1029
1030 float h = getPrimaryHorizontal(here);
1031
1032 if (h > horiz && h < besth) {
1033 best = here;
1034 besth = h;
1035 }
1036
1037 candidate = TextUtils.getOffsetAfter(mText, here);
1038 if (candidate >= start && candidate <= end) {
1039 h = getPrimaryHorizontal(candidate);
1040
1041 if (h > horiz && h < besth) {
1042 best = candidate;
1043 besth = h;
1044 }
1045 }
1046
1047 candidate = TextUtils.getOffsetBefore(mText, there);
1048 if (candidate >= start && candidate <= end) {
1049 h = getPrimaryHorizontal(candidate);
1050
1051 if (h > horiz && h < besth) {
1052 best = candidate;
1053 besth = h;
1054 }
1055 }
1056
1057 here = there;
1058 }
1059
1060 float h = getPrimaryHorizontal(end);
1061
1062 if (h > horiz && h < besth) {
1063 best = end;
1064 besth = h;
1065 }
1066
1067 if (best != offset)
1068 return best;
1069
1070 int dir = getParagraphDirection(line);
1071
1072 if (dir > 0) {
1073 if (line == getLineCount() - 1)
1074 return best;
1075 else
1076 return getOffsetForHorizontal(line + 1, -10000);
1077 } else {
1078 if (line == 0)
1079 return best;
1080 else
1081 return getOffsetForHorizontal(line - 1, -10000);
1082 }
1083 }
1084
1085 private int getOffsetAtStartOf(int offset) {
1086 if (offset == 0)
1087 return 0;
1088
1089 CharSequence text = mText;
1090 char c = text.charAt(offset);
1091
1092 if (c >= '\uDC00' && c <= '\uDFFF') {
1093 char c1 = text.charAt(offset - 1);
1094
1095 if (c1 >= '\uD800' && c1 <= '\uDBFF')
1096 offset -= 1;
1097 }
1098
1099 if (mSpannedText) {
1100 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1101 ReplacementSpan.class);
1102
1103 for (int i = 0; i < spans.length; i++) {
1104 int start = ((Spanned) text).getSpanStart(spans[i]);
1105 int end = ((Spanned) text).getSpanEnd(spans[i]);
1106
1107 if (start < offset && end > offset)
1108 offset = start;
1109 }
1110 }
1111
1112 return offset;
1113 }
1114
1115 /**
1116 * Fills in the specified Path with a representation of a cursor
1117 * at the specified offset. This will often be a vertical line
1118 * but can be multiple discontinous lines in text with multiple
1119 * directionalities.
1120 */
1121 public void getCursorPath(int point, Path dest,
1122 CharSequence editingBuffer) {
1123 dest.reset();
1124
1125 int line = getLineForOffset(point);
1126 int top = getLineTop(line);
1127 int bottom = getLineTop(line+1);
1128
1129 float h1 = getPrimaryHorizontal(point) - 0.5f;
1130 float h2 = getSecondaryHorizontal(point) - 0.5f;
1131
1132 int caps = TextKeyListener.getMetaState(editingBuffer,
1133 KeyEvent.META_SHIFT_ON) |
1134 TextKeyListener.getMetaState(editingBuffer,
1135 TextKeyListener.META_SELECTING);
1136 int fn = TextKeyListener.getMetaState(editingBuffer,
1137 KeyEvent.META_ALT_ON);
1138 int dist = 0;
1139
1140 if (caps != 0 || fn != 0) {
1141 dist = (bottom - top) >> 2;
1142
1143 if (fn != 0)
1144 top += dist;
1145 if (caps != 0)
1146 bottom -= dist;
1147 }
1148
1149 if (h1 < 0.5f)
1150 h1 = 0.5f;
1151 if (h2 < 0.5f)
1152 h2 = 0.5f;
1153
1154 if (h1 == h2) {
1155 dest.moveTo(h1, top);
1156 dest.lineTo(h1, bottom);
1157 } else {
1158 dest.moveTo(h1, top);
1159 dest.lineTo(h1, (top + bottom) >> 1);
1160
1161 dest.moveTo(h2, (top + bottom) >> 1);
1162 dest.lineTo(h2, bottom);
1163 }
1164
1165 if (caps == 2) {
1166 dest.moveTo(h2, bottom);
1167 dest.lineTo(h2 - dist, bottom + dist);
1168 dest.lineTo(h2, bottom);
1169 dest.lineTo(h2 + dist, bottom + dist);
1170 } else if (caps == 1) {
1171 dest.moveTo(h2, bottom);
1172 dest.lineTo(h2 - dist, bottom + dist);
1173
1174 dest.moveTo(h2 - dist, bottom + dist - 0.5f);
1175 dest.lineTo(h2 + dist, bottom + dist - 0.5f);
1176
1177 dest.moveTo(h2 + dist, bottom + dist);
1178 dest.lineTo(h2, bottom);
1179 }
1180
1181 if (fn == 2) {
1182 dest.moveTo(h1, top);
1183 dest.lineTo(h1 - dist, top - dist);
1184 dest.lineTo(h1, top);
1185 dest.lineTo(h1 + dist, top - dist);
1186 } else if (fn == 1) {
1187 dest.moveTo(h1, top);
1188 dest.lineTo(h1 - dist, top - dist);
1189
1190 dest.moveTo(h1 - dist, top - dist + 0.5f);
1191 dest.lineTo(h1 + dist, top - dist + 0.5f);
1192
1193 dest.moveTo(h1 + dist, top - dist);
1194 dest.lineTo(h1, top);
1195 }
1196 }
1197
1198 private void addSelection(int line, int start, int end,
1199 int top, int bottom, Path dest) {
1200 int linestart = getLineStart(line);
1201 int lineend = getLineEnd(line);
1202 Directions dirs = getLineDirections(line);
1203
1204 if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
1205 lineend--;
1206
1207 int here = linestart;
1208 for (int i = 0; i < dirs.mDirections.length; i++) {
1209 int there = here + dirs.mDirections[i];
1210 if (there > lineend)
1211 there = lineend;
1212
1213 if (start <= there && end >= here) {
1214 int st = Math.max(start, here);
1215 int en = Math.min(end, there);
1216
1217 if (st != en) {
1218 float h1 = getHorizontal(st, false, false, line);
1219 float h2 = getHorizontal(en, true, false, line);
1220
1221 dest.addRect(h1, top, h2, bottom, Path.Direction.CW);
1222 }
1223 }
1224
1225 here = there;
1226 }
1227 }
1228
1229 /**
1230 * Fills in the specified Path with a representation of a highlight
1231 * between the specified offsets. This will often be a rectangle
1232 * or a potentially discontinuous set of rectangles. If the start
1233 * and end are the same, the returned path is empty.
1234 */
1235 public void getSelectionPath(int start, int end, Path dest) {
1236 dest.reset();
1237
1238 if (start == end)
1239 return;
1240
1241 if (end < start) {
1242 int temp = end;
1243 end = start;
1244 start = temp;
1245 }
1246
1247 int startline = getLineForOffset(start);
1248 int endline = getLineForOffset(end);
1249
1250 int top = getLineTop(startline);
1251 int bottom = getLineBottom(endline);
1252
1253 if (startline == endline) {
1254 addSelection(startline, start, end, top, bottom, dest);
1255 } else {
1256 final float width = mWidth;
1257
1258 addSelection(startline, start, getLineEnd(startline),
1259 top, getLineBottom(startline), dest);
1260
1261 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT)
1262 dest.addRect(getLineLeft(startline), top,
1263 0, getLineBottom(startline), Path.Direction.CW);
1264 else
1265 dest.addRect(getLineRight(startline), top,
1266 width, getLineBottom(startline), Path.Direction.CW);
1267
1268 for (int i = startline + 1; i < endline; i++) {
1269 top = getLineTop(i);
1270 bottom = getLineBottom(i);
1271 dest.addRect(0, top, width, bottom, Path.Direction.CW);
1272 }
1273
1274 top = getLineTop(endline);
1275 bottom = getLineBottom(endline);
1276
1277 addSelection(endline, getLineStart(endline), end,
1278 top, bottom, dest);
1279
1280 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT)
1281 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW);
1282 else
1283 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW);
1284 }
1285 }
1286
1287 /**
1288 * Get the alignment of the specified paragraph, taking into account
1289 * markup attached to it.
1290 */
1291 public final Alignment getParagraphAlignment(int line) {
1292 Alignment align = mAlignment;
1293
1294 if (mSpannedText) {
1295 Spanned sp = (Spanned) mText;
1296 AlignmentSpan[] spans = sp.getSpans(getLineStart(line),
1297 getLineEnd(line),
1298 AlignmentSpan.class);
1299
1300 int spanLength = spans.length;
1301 if (spanLength > 0) {
1302 align = spans[spanLength-1].getAlignment();
1303 }
1304 }
1305
1306 return align;
1307 }
1308
1309 /**
1310 * Get the left edge of the specified paragraph, inset by left margins.
1311 */
1312 public final int getParagraphLeft(int line) {
1313 int dir = getParagraphDirection(line);
1314
1315 int left = 0;
1316
1317 boolean par = false;
1318 int off = getLineStart(line);
1319 if (off == 0 || mText.charAt(off - 1) == '\n')
1320 par = true;
1321
1322 if (dir == DIR_LEFT_TO_RIGHT) {
1323 if (mSpannedText) {
1324 Spanned sp = (Spanned) mText;
1325 LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
1326 getLineEnd(line),
1327 LeadingMarginSpan.class);
1328
1329 for (int i = 0; i < spans.length; i++) {
Mark Wagner7b5676e2009-10-16 11:44:23 -07001330 boolean margin = par;
1331 LeadingMarginSpan span = spans[i];
1332 if (span instanceof LeadingMarginSpan.LeadingMarginSpan2) {
1333 int count = ((LeadingMarginSpan.LeadingMarginSpan2)span).getLeadingMarginLineCount();
1334 margin = count >= line;
1335 }
1336 left += span.getLeadingMargin(margin);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001337 }
1338 }
1339 }
1340
1341 return left;
1342 }
1343
1344 /**
1345 * Get the right edge of the specified paragraph, inset by right margins.
1346 */
1347 public final int getParagraphRight(int line) {
1348 int dir = getParagraphDirection(line);
1349
1350 int right = mWidth;
1351
1352 boolean par = false;
1353 int off = getLineStart(line);
1354 if (off == 0 || mText.charAt(off - 1) == '\n')
1355 par = true;
1356
1357
1358 if (dir == DIR_RIGHT_TO_LEFT) {
1359 if (mSpannedText) {
1360 Spanned sp = (Spanned) mText;
1361 LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
1362 getLineEnd(line),
1363 LeadingMarginSpan.class);
1364
1365 for (int i = 0; i < spans.length; i++) {
1366 right -= spans[i].getLeadingMargin(par);
1367 }
1368 }
1369 }
1370
1371 return right;
1372 }
1373
Eric Fischerc2d54f42009-03-27 15:52:38 -07001374 private void drawText(Canvas canvas,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001375 CharSequence text, int start, int end,
1376 int dir, Directions directions,
1377 float x, int top, int y, int bottom,
1378 TextPaint paint,
1379 TextPaint workPaint,
1380 boolean hasTabs, Object[] parspans) {
1381 char[] buf;
1382 if (!hasTabs) {
1383 if (directions == DIRS_ALL_LEFT_TO_RIGHT) {
Dave Bort76c02262009-04-13 17:17:21 -07001384 if (DEBUG) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001385 Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir);
1386 }
1387 Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false);
1388 return;
1389 }
1390 buf = null;
1391 } else {
1392 buf = TextUtils.obtain(end - start);
1393 TextUtils.getChars(text, start, end, buf, 0);
1394 }
1395
1396 float h = 0;
1397
1398 int here = 0;
1399 for (int i = 0; i < directions.mDirections.length; i++) {
1400 int there = here + directions.mDirections[i];
1401 if (there > end - start)
1402 there = end - start;
1403
1404 int segstart = here;
1405 for (int j = hasTabs ? here : there; j <= there; j++) {
1406 if (j == there || buf[j] == '\t') {
1407 h += Styled.drawText(canvas, text,
1408 start + segstart, start + j,
1409 dir, (i & 1) != 0, x + h,
1410 top, y, bottom, paint, workPaint,
1411 start + j != end);
1412
1413 if (j != there && buf[j] == '\t')
1414 h = dir * nextTab(text, start, end, h * dir, parspans);
1415
1416 segstart = j + 1;
The Android Open Source Project10592532009-03-18 17:39:46 -07001417 } else if (hasTabs && buf[j] >= 0xD800 && buf[j] <= 0xDFFF && j + 1 < there) {
1418 int emoji = Character.codePointAt(buf, j);
1419
1420 if (emoji >= MIN_EMOJI && emoji <= MAX_EMOJI) {
1421 Bitmap bm = EMOJI_FACTORY.
1422 getBitmapFromAndroidPua(emoji);
1423
1424 if (bm != null) {
1425 h += Styled.drawText(canvas, text,
1426 start + segstart, start + j,
1427 dir, (i & 1) != 0, x + h,
1428 top, y, bottom, paint, workPaint,
1429 start + j != end);
1430
Eric Fischerc2d54f42009-03-27 15:52:38 -07001431 if (mEmojiRect == null) {
1432 mEmojiRect = new RectF();
1433 }
1434
1435 workPaint.set(paint);
1436 Styled.measureText(paint, workPaint, text,
1437 start + j, start + j + 1,
1438 null);
1439
1440 float bitmapHeight = bm.getHeight();
1441 float textHeight = -workPaint.ascent();
1442 float scale = textHeight / bitmapHeight;
1443 float width = bm.getWidth() * scale;
1444
1445 mEmojiRect.set(x + h, y - textHeight,
1446 x + h + width, y);
1447
1448 canvas.drawBitmap(bm, null, mEmojiRect, paint);
1449 h += width;
1450
The Android Open Source Project10592532009-03-18 17:39:46 -07001451 j++;
1452 segstart = j + 1;
1453 }
1454 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001455 }
1456 }
1457
1458 here = there;
1459 }
1460
1461 if (hasTabs)
1462 TextUtils.recycle(buf);
1463 }
1464
1465 private static float measureText(TextPaint paint,
1466 TextPaint workPaint,
1467 CharSequence text,
1468 int start, int offset, int end,
1469 int dir, Directions directions,
1470 boolean trailing, boolean alt,
1471 boolean hasTabs, Object[] tabs) {
1472 char[] buf = null;
1473
1474 if (hasTabs) {
1475 buf = TextUtils.obtain(end - start);
1476 TextUtils.getChars(text, start, end, buf, 0);
1477 }
1478
1479 float h = 0;
1480
1481 if (alt) {
1482 if (dir == DIR_RIGHT_TO_LEFT)
1483 trailing = !trailing;
1484 }
1485
1486 int here = 0;
1487 for (int i = 0; i < directions.mDirections.length; i++) {
1488 if (alt)
1489 trailing = !trailing;
1490
1491 int there = here + directions.mDirections[i];
1492 if (there > end - start)
1493 there = end - start;
1494
1495 int segstart = here;
1496 for (int j = hasTabs ? here : there; j <= there; j++) {
The Android Open Source Project10592532009-03-18 17:39:46 -07001497 int codept = 0;
1498 Bitmap bm = null;
1499
1500 if (hasTabs && j < there) {
1501 codept = buf[j];
1502 }
1503
1504 if (codept >= 0xD800 && codept <= 0xDFFF && j + 1 < there) {
1505 codept = Character.codePointAt(buf, j);
1506
1507 if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) {
1508 bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
1509 }
1510 }
1511
1512 if (j == there || codept == '\t' || bm != null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001513 float segw;
1514
1515 if (offset < start + j ||
1516 (trailing && offset <= start + j)) {
1517 if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) {
1518 h += Styled.measureText(paint, workPaint, text,
1519 start + segstart, offset,
1520 null);
1521 return h;
1522 }
1523
1524 if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) {
1525 h -= Styled.measureText(paint, workPaint, text,
1526 start + segstart, offset,
1527 null);
1528 return h;
1529 }
1530 }
1531
1532 segw = Styled.measureText(paint, workPaint, text,
1533 start + segstart, start + j,
1534 null);
1535
1536 if (offset < start + j ||
1537 (trailing && offset <= start + j)) {
1538 if (dir == DIR_LEFT_TO_RIGHT) {
1539 h += segw - Styled.measureText(paint, workPaint,
1540 text,
1541 start + segstart,
1542 offset, null);
1543 return h;
1544 }
1545
1546 if (dir == DIR_RIGHT_TO_LEFT) {
1547 h -= segw - Styled.measureText(paint, workPaint,
1548 text,
1549 start + segstart,
1550 offset, null);
1551 return h;
1552 }
1553 }
1554
1555 if (dir == DIR_RIGHT_TO_LEFT)
1556 h -= segw;
1557 else
1558 h += segw;
1559
1560 if (j != there && buf[j] == '\t') {
1561 if (offset == start + j)
1562 return h;
1563
1564 h = dir * nextTab(text, start, end, h * dir, tabs);
1565 }
1566
The Android Open Source Project10592532009-03-18 17:39:46 -07001567 if (bm != null) {
Eric Fischerc2d54f42009-03-27 15:52:38 -07001568 workPaint.set(paint);
1569 Styled.measureText(paint, workPaint, text,
Eric Fischera82e99a2009-08-31 15:14:35 -07001570 j, j + 2, null);
Eric Fischerc2d54f42009-03-27 15:52:38 -07001571
1572 float wid = (float) bm.getWidth() *
1573 -workPaint.ascent() / bm.getHeight();
1574
The Android Open Source Project10592532009-03-18 17:39:46 -07001575 if (dir == DIR_RIGHT_TO_LEFT) {
Eric Fischerc2d54f42009-03-27 15:52:38 -07001576 h -= wid;
The Android Open Source Project10592532009-03-18 17:39:46 -07001577 } else {
Eric Fischerc2d54f42009-03-27 15:52:38 -07001578 h += wid;
The Android Open Source Project10592532009-03-18 17:39:46 -07001579 }
1580
1581 j++;
1582 }
1583
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001584 segstart = j + 1;
1585 }
1586 }
1587
1588 here = there;
1589 }
1590
1591 if (hasTabs)
1592 TextUtils.recycle(buf);
1593
1594 return h;
1595 }
1596
Doug Felt71b8dd72010-02-16 17:27:09 -08001597 /**
1598 * Measure width of a run of text on a single line that is known to all be
1599 * in the same direction as the paragraph base direction. Returns the width,
1600 * and the line metrics in fm if fm is not null.
1601 *
1602 * @param paint the paint for the text; will not be modified
1603 * @param workPaint paint available for modification
1604 * @param text text
1605 * @param start start of the line
1606 * @param end limit of the line
1607 * @param fm object to return integer metrics in, can be null
1608 * @param hasTabs true if it is known that the line has tabs
1609 * @param tabs tab position information
1610 * @return the width of the text from start to end
1611 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001612 /* package */ static float measureText(TextPaint paint,
1613 TextPaint workPaint,
1614 CharSequence text,
1615 int start, int end,
1616 Paint.FontMetricsInt fm,
1617 boolean hasTabs, Object[] tabs) {
1618 char[] buf = null;
1619
1620 if (hasTabs) {
1621 buf = TextUtils.obtain(end - start);
1622 TextUtils.getChars(text, start, end, buf, 0);
1623 }
1624
1625 int len = end - start;
1626
Doug Felt71b8dd72010-02-16 17:27:09 -08001627 int lastPos = 0;
1628 float width = 0;
1629 int ascent = 0, descent = 0, top = 0, bottom = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001630
1631 if (fm != null) {
1632 fm.ascent = 0;
1633 fm.descent = 0;
1634 }
1635
Doug Felt71b8dd72010-02-16 17:27:09 -08001636 for (int pos = hasTabs ? 0 : len; pos <= len; pos++) {
The Android Open Source Project10592532009-03-18 17:39:46 -07001637 int codept = 0;
1638 Bitmap bm = null;
1639
Doug Felt71b8dd72010-02-16 17:27:09 -08001640 if (hasTabs && pos < len) {
1641 codept = buf[pos];
The Android Open Source Project10592532009-03-18 17:39:46 -07001642 }
1643
Doug Felt71b8dd72010-02-16 17:27:09 -08001644 if (codept >= 0xD800 && codept <= 0xDFFF && pos < len) {
1645 codept = Character.codePointAt(buf, pos);
The Android Open Source Project10592532009-03-18 17:39:46 -07001646
1647 if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) {
1648 bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
1649 }
1650 }
1651
Doug Felt71b8dd72010-02-16 17:27:09 -08001652 if (pos == len || codept == '\t' || bm != null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001653 workPaint.baselineShift = 0;
1654
Doug Felt71b8dd72010-02-16 17:27:09 -08001655 width += Styled.measureText(paint, workPaint, text,
1656 start + lastPos, start + pos,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001657 fm);
1658
1659 if (fm != null) {
1660 if (workPaint.baselineShift < 0) {
1661 fm.ascent += workPaint.baselineShift;
1662 fm.top += workPaint.baselineShift;
1663 } else {
1664 fm.descent += workPaint.baselineShift;
1665 fm.bottom += workPaint.baselineShift;
1666 }
1667 }
1668
Doug Felt71b8dd72010-02-16 17:27:09 -08001669 if (pos != len) {
The Android Open Source Project10592532009-03-18 17:39:46 -07001670 if (bm == null) {
Doug Felt71b8dd72010-02-16 17:27:09 -08001671 // no emoji, must have hit a tab
1672 width = nextTab(text, start, end, width, tabs);
The Android Open Source Project10592532009-03-18 17:39:46 -07001673 } else {
Doug Felt71b8dd72010-02-16 17:27:09 -08001674 // This sets up workPaint with the font on the emoji
1675 // text, so that we can extract the ascent and scale.
1676
1677 // We can't use the result of the previous call to
1678 // measureText because the emoji might have its own style.
1679 // We have to initialize workPaint here because if the
1680 // text is unstyled measureText might not use workPaint
1681 // at all.
Eric Fischerc2d54f42009-03-27 15:52:38 -07001682 workPaint.set(paint);
1683 Styled.measureText(paint, workPaint, text,
Doug Felt71b8dd72010-02-16 17:27:09 -08001684 start + pos, start + pos + 1, null);
Eric Fischerc2d54f42009-03-27 15:52:38 -07001685
Doug Felt71b8dd72010-02-16 17:27:09 -08001686 width += (float) bm.getWidth() *
Eric Fischerc2d54f42009-03-27 15:52:38 -07001687 -workPaint.ascent() / bm.getHeight();
1688
Doug Felt71b8dd72010-02-16 17:27:09 -08001689 // Since we had an emoji, we bump past the second half
1690 // of the surrogate pair.
1691 pos++;
The Android Open Source Project10592532009-03-18 17:39:46 -07001692 }
1693 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001694
1695 if (fm != null) {
Doug Felt71b8dd72010-02-16 17:27:09 -08001696 if (fm.ascent < ascent) {
1697 ascent = fm.ascent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001698 }
Doug Felt71b8dd72010-02-16 17:27:09 -08001699 if (fm.descent > descent) {
1700 descent = fm.descent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001701 }
1702
1703 if (fm.top < top) {
1704 top = fm.top;
1705 }
Doug Felt71b8dd72010-02-16 17:27:09 -08001706 if (fm.bottom > bottom) {
1707 bottom = fm.bottom;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001708 }
The Android Open Source Project10592532009-03-18 17:39:46 -07001709
Doug Felt71b8dd72010-02-16 17:27:09 -08001710 // No need to take bitmap height into account here,
1711 // since it is scaled to match the text height.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001712 }
1713
Doug Felt71b8dd72010-02-16 17:27:09 -08001714 lastPos = pos + 1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001715 }
1716 }
1717
1718 if (fm != null) {
Doug Felt71b8dd72010-02-16 17:27:09 -08001719 fm.ascent = ascent;
1720 fm.descent = descent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001721 fm.top = top;
Doug Felt71b8dd72010-02-16 17:27:09 -08001722 fm.bottom = bottom;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001723 }
1724
1725 if (hasTabs)
1726 TextUtils.recycle(buf);
1727
Doug Felt71b8dd72010-02-16 17:27:09 -08001728 return width;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001729 }
1730
Doug Felt71b8dd72010-02-16 17:27:09 -08001731 /**
1732 * Returns the position of the next tab stop after h on the line.
1733 *
1734 * @param text the text
1735 * @param start start of the line
1736 * @param end limit of the line
1737 * @param h the current horizontal offset
1738 * @param tabs the tabs, can be null. If it is null, any tabs in effect
1739 * on the line will be used. If there are no tabs, a default offset
1740 * will be used to compute the tab stop.
1741 * @return the offset of the next tab stop.
1742 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001743 /* package */ static float nextTab(CharSequence text, int start, int end,
1744 float h, Object[] tabs) {
1745 float nh = Float.MAX_VALUE;
1746 boolean alltabs = false;
1747
1748 if (text instanceof Spanned) {
1749 if (tabs == null) {
1750 tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class);
1751 alltabs = true;
1752 }
1753
1754 for (int i = 0; i < tabs.length; i++) {
1755 if (!alltabs) {
1756 if (!(tabs[i] instanceof TabStopSpan))
1757 continue;
1758 }
1759
1760 int where = ((TabStopSpan) tabs[i]).getTabStop();
1761
1762 if (where < nh && where > h)
1763 nh = where;
1764 }
1765
1766 if (nh != Float.MAX_VALUE)
1767 return nh;
1768 }
1769
1770 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
1771 }
1772
1773 protected final boolean isSpanned() {
1774 return mSpannedText;
1775 }
1776
1777 private void ellipsize(int start, int end, int line,
1778 char[] dest, int destoff) {
1779 int ellipsisCount = getEllipsisCount(line);
1780
1781 if (ellipsisCount == 0) {
1782 return;
1783 }
1784
1785 int ellipsisStart = getEllipsisStart(line);
1786 int linestart = getLineStart(line);
1787
1788 for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
1789 char c;
1790
1791 if (i == ellipsisStart) {
1792 c = '\u2026'; // ellipsis
1793 } else {
1794 c = '\uFEFF'; // 0-width space
1795 }
1796
1797 int a = i + linestart;
1798
1799 if (a >= start && a < end) {
1800 dest[destoff + a - start] = c;
1801 }
1802 }
1803 }
1804
1805 /**
1806 * Stores information about bidirectional (left-to-right or right-to-left)
1807 * text within the layout of a line. TODO: This work is not complete
1808 * or correct and will be fleshed out in a later revision.
1809 */
1810 public static class Directions {
1811 private short[] mDirections;
1812
Doug Felt71b8dd72010-02-16 17:27:09 -08001813 // The values in mDirections are the offsets from the first character
1814 // in the line to the next flip in direction. Runs at even indices
1815 // are left-to-right, the others are right-to-left. So, for example,
1816 // a line that starts with a right-to-left run has 0 at mDirections[0],
1817 // since the 'first' (ltr) run is zero length.
1818 //
1819 // The code currently assumes that each run is adjacent to the previous
1820 // one, progressing in the base line direction. This isn't sufficient
1821 // to handle nested runs, for example numeric text in an rtl context
1822 // in an ltr paragraph.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001823 /* package */ Directions(short[] dirs) {
1824 mDirections = dirs;
1825 }
1826 }
1827
1828 /**
1829 * Return the offset of the first character to be ellipsized away,
1830 * relative to the start of the line. (So 0 if the beginning of the
1831 * line is ellipsized, not getLineStart().)
1832 */
1833 public abstract int getEllipsisStart(int line);
1834 /**
1835 * Returns the number of characters to be ellipsized away, or 0 if
1836 * no ellipsis is to take place.
1837 */
1838 public abstract int getEllipsisCount(int line);
1839
1840 /* package */ static class Ellipsizer implements CharSequence, GetChars {
1841 /* package */ CharSequence mText;
1842 /* package */ Layout mLayout;
1843 /* package */ int mWidth;
1844 /* package */ TextUtils.TruncateAt mMethod;
1845
1846 public Ellipsizer(CharSequence s) {
1847 mText = s;
1848 }
1849
1850 public char charAt(int off) {
1851 char[] buf = TextUtils.obtain(1);
1852 getChars(off, off + 1, buf, 0);
1853 char ret = buf[0];
1854
1855 TextUtils.recycle(buf);
1856 return ret;
1857 }
1858
1859 public void getChars(int start, int end, char[] dest, int destoff) {
1860 int line1 = mLayout.getLineForOffset(start);
1861 int line2 = mLayout.getLineForOffset(end);
1862
1863 TextUtils.getChars(mText, start, end, dest, destoff);
1864
1865 for (int i = line1; i <= line2; i++) {
1866 mLayout.ellipsize(start, end, i, dest, destoff);
1867 }
1868 }
1869
1870 public int length() {
1871 return mText.length();
1872 }
1873
1874 public CharSequence subSequence(int start, int end) {
1875 char[] s = new char[end - start];
1876 getChars(start, end, s, 0);
1877 return new String(s);
1878 }
1879
1880 public String toString() {
1881 char[] s = new char[length()];
1882 getChars(0, length(), s, 0);
1883 return new String(s);
1884 }
1885
1886 }
1887
1888 /* package */ static class SpannedEllipsizer
1889 extends Ellipsizer implements Spanned {
1890 private Spanned mSpanned;
1891
1892 public SpannedEllipsizer(CharSequence display) {
1893 super(display);
1894 mSpanned = (Spanned) display;
1895 }
1896
1897 public <T> T[] getSpans(int start, int end, Class<T> type) {
1898 return mSpanned.getSpans(start, end, type);
1899 }
1900
1901 public int getSpanStart(Object tag) {
1902 return mSpanned.getSpanStart(tag);
1903 }
1904
1905 public int getSpanEnd(Object tag) {
1906 return mSpanned.getSpanEnd(tag);
1907 }
1908
1909 public int getSpanFlags(Object tag) {
1910 return mSpanned.getSpanFlags(tag);
1911 }
1912
1913 public int nextSpanTransition(int start, int limit, Class type) {
1914 return mSpanned.nextSpanTransition(start, limit, type);
1915 }
1916
1917 public CharSequence subSequence(int start, int end) {
1918 char[] s = new char[end - start];
1919 getChars(start, end, s, 0);
1920
1921 SpannableString ss = new SpannableString(new String(s));
1922 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
1923 return ss;
1924 }
1925 }
1926
1927 private CharSequence mText;
1928 private TextPaint mPaint;
1929 /* package */ TextPaint mWorkPaint;
1930 private int mWidth;
1931 private Alignment mAlignment = Alignment.ALIGN_NORMAL;
1932 private float mSpacingMult;
1933 private float mSpacingAdd;
1934 private static Rect sTempRect = new Rect();
1935 private boolean mSpannedText;
1936
1937 public static final int DIR_LEFT_TO_RIGHT = 1;
1938 public static final int DIR_RIGHT_TO_LEFT = -1;
Doug Felt20178d62010-02-22 13:39:01 -08001939
1940 /* package */ static final int DIR_REQUEST_LTR = 1;
1941 /* package */ static final int DIR_REQUEST_RTL = -1;
1942 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
1943 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001944
1945 public enum Alignment {
1946 ALIGN_NORMAL,
1947 ALIGN_OPPOSITE,
1948 ALIGN_CENTER,
1949 // XXX ALIGN_LEFT,
1950 // XXX ALIGN_RIGHT,
1951 }
1952
1953 private static final int TAB_INCREMENT = 20;
1954
1955 /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
1956 new Directions(new short[] { 32767 });
1957 /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
1958 new Directions(new short[] { 0, 32767 });
1959
1960}
1961