blob: 3b8f2950f326ddaf08b43cd8afa2e2cd688d926d [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 Felt9f7a4442010-03-01 12:45:56 -080019import com.android.internal.util.ArrayUtils;
20
The Android Open Source Project10592532009-03-18 17:39:46 -070021import android.emoji.EmojiFactory;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.graphics.Canvas;
23import android.graphics.Paint;
Doug Felt9f7a4442010-03-01 12:45:56 -080024import android.graphics.Path;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025import android.graphics.Rect;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026import android.text.method.TextKeyListener;
Doug Felt9f7a4442010-03-01 12:45:56 -080027import android.text.style.AlignmentSpan;
28import android.text.style.LeadingMarginSpan;
29import android.text.style.LineBackgroundSpan;
30import android.text.style.ParagraphStyle;
31import android.text.style.ReplacementSpan;
32import android.text.style.TabStopSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033import android.view.KeyEvent;
34
Doug Felt9f7a4442010-03-01 12:45:56 -080035import junit.framework.Assert;
36
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080037/**
Doug Felt9f7a4442010-03-01 12:45:56 -080038 * A base class that manages text layout in visual elements on
39 * the screen.
40 * <p>For text that will be edited, use a {@link DynamicLayout},
41 * which will be updated as the text changes.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080042 * For text that will not change, use a {@link StaticLayout}.
43 */
44public abstract class Layout {
Dave Bort76c02262009-04-13 17:17:21 -070045 private static final boolean DEBUG = false;
Doug Felt71b8dd72010-02-16 17:27:09 -080046 private static final ParagraphStyle[] NO_PARA_SPANS =
47 ArrayUtils.emptyArray(ParagraphStyle.class);
Dave Bort76c02262009-04-13 17:17:21 -070048
The Android Open Source Project10592532009-03-18 17:39:46 -070049 /* package */ static final EmojiFactory EMOJI_FACTORY =
50 EmojiFactory.newAvailableInstance();
51 /* package */ static final int MIN_EMOJI, MAX_EMOJI;
52
53 static {
54 if (EMOJI_FACTORY != null) {
55 MIN_EMOJI = EMOJI_FACTORY.getMinimumAndroidPua();
56 MAX_EMOJI = EMOJI_FACTORY.getMaximumAndroidPua();
57 } else {
58 MIN_EMOJI = -1;
59 MAX_EMOJI = -1;
60 }
Doug Felte8e45f22010-03-29 14:58:40 -070061 }
Eric Fischerc2d54f42009-03-27 15:52:38 -070062
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063 /**
Doug Felt71b8dd72010-02-16 17:27:09 -080064 * Return how wide a layout must be in order to display the
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080065 * specified text with one line per paragraph.
66 */
67 public static float getDesiredWidth(CharSequence source,
68 TextPaint paint) {
69 return getDesiredWidth(source, 0, source.length(), paint);
70 }
Doug Felt9f7a4442010-03-01 12:45:56 -080071
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080072 /**
Doug Felt71b8dd72010-02-16 17:27:09 -080073 * Return how wide a layout must be in order to display the
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080074 * specified text slice with one line per paragraph.
75 */
76 public static float getDesiredWidth(CharSequence source,
77 int start, int end,
78 TextPaint paint) {
79 float need = 0;
80 TextPaint workPaint = new TextPaint();
81
82 int next;
83 for (int i = start; i <= end; i = next) {
84 next = TextUtils.indexOf(source, '\n', i, end);
85
86 if (next < 0)
87 next = end;
88
Doug Felt71b8dd72010-02-16 17:27:09 -080089 // note, omits trailing paragraph char
Doug Felte8e45f22010-03-29 14:58:40 -070090 float w = measurePara(paint, workPaint,
91 source, i, next, true, null);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080092
93 if (w > need)
94 need = w;
95
96 next++;
97 }
98
99 return need;
100 }
101
102 /**
103 * Subclasses of Layout use this constructor to set the display text,
104 * width, and other standard properties.
Doug Felt71b8dd72010-02-16 17:27:09 -0800105 * @param text the text to render
106 * @param paint the default paint for the layout. Styles can override
107 * various attributes of the paint.
108 * @param width the wrapping width for the text.
109 * @param align whether to left, right, or center the text. Styles can
110 * override the alignment.
111 * @param spacingMult factor by which to scale the font size to get the
112 * default line spacing
113 * @param spacingAdd amount to add to the default line spacing
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800114 */
115 protected Layout(CharSequence text, TextPaint paint,
116 int width, Alignment align,
Doug Felt71b8dd72010-02-16 17:27:09 -0800117 float spacingMult, float spacingAdd) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800118 if (width < 0)
119 throw new IllegalArgumentException("Layout: " + width + " < 0");
120
Doug Felte8e45f22010-03-29 14:58:40 -0700121 // Ensure paint doesn't have baselineShift set.
122 // While normally we don't modify the paint the user passed in,
123 // we were already doing this in Styled.drawUniformRun with both
124 // baselineShift and bgColor. We probably should reevaluate bgColor.
125 if (paint != null) {
126 paint.bgColor = 0;
127 paint.baselineShift = 0;
128 }
129
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800130 mText = text;
131 mPaint = paint;
132 mWorkPaint = new TextPaint();
133 mWidth = width;
134 mAlignment = align;
Doug Felt71b8dd72010-02-16 17:27:09 -0800135 mSpacingMult = spacingMult;
136 mSpacingAdd = spacingAdd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800137 mSpannedText = text instanceof Spanned;
138 }
139
140 /**
141 * Replace constructor properties of this Layout with new ones. Be careful.
142 */
143 /* package */ void replaceWith(CharSequence text, TextPaint paint,
144 int width, Alignment align,
145 float spacingmult, float spacingadd) {
146 if (width < 0) {
147 throw new IllegalArgumentException("Layout: " + width + " < 0");
148 }
149
150 mText = text;
151 mPaint = paint;
152 mWidth = width;
153 mAlignment = align;
154 mSpacingMult = spacingmult;
155 mSpacingAdd = spacingadd;
156 mSpannedText = text instanceof Spanned;
157 }
158
159 /**
160 * Draw this Layout on the specified Canvas.
161 */
162 public void draw(Canvas c) {
163 draw(c, null, null, 0);
164 }
165
166 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800167 * Draw this Layout on the specified canvas, with the highlight path drawn
168 * between the background and the text.
169 *
170 * @param c the canvas
171 * @param highlight the path of the highlight or cursor; can be null
172 * @param highlightPaint the paint for the highlight
173 * @param cursorOffsetVertical the amount to temporarily translate the
174 * canvas while rendering the highlight
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800175 */
Doug Felt71b8dd72010-02-16 17:27:09 -0800176 public void draw(Canvas c, Path highlight, Paint highlightPaint,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800177 int cursorOffsetVertical) {
178 int dtop, dbottom;
179
180 synchronized (sTempRect) {
181 if (!c.getClipBounds(sTempRect)) {
182 return;
183 }
184
185 dtop = sTempRect.top;
186 dbottom = sTempRect.bottom;
187 }
188
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800189
190 int top = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800191 int bottom = getLineTop(getLineCount());
192
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800193 if (dtop > top) {
194 top = dtop;
195 }
196 if (dbottom < bottom) {
197 bottom = dbottom;
198 }
Doug Felt9f7a4442010-03-01 12:45:56 -0800199
200 int first = getLineForVertical(top);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800201 int last = getLineForVertical(bottom);
Doug Felt9f7a4442010-03-01 12:45:56 -0800202
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800203 int previousLineBottom = getLineTop(first);
204 int previousLineEnd = getLineStart(first);
Doug Felt9f7a4442010-03-01 12:45:56 -0800205
Doug Felt71b8dd72010-02-16 17:27:09 -0800206 TextPaint paint = mPaint;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800207 CharSequence buf = mText;
Doug Felt71b8dd72010-02-16 17:27:09 -0800208 int width = mWidth;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800209 boolean spannedText = mSpannedText;
210
Doug Felt71b8dd72010-02-16 17:27:09 -0800211 ParagraphStyle[] spans = NO_PARA_SPANS;
212 int spanend = 0;
213 int textLength = 0;
214
215 // First, draw LineBackgroundSpans.
216 // LineBackgroundSpans know nothing about the alignment or direction of
217 // the layout or line. XXX: Should they?
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800218 if (spannedText) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800219 textLength = buf.length();
220 for (int i = first; i <= last; i++) {
221 int start = previousLineEnd;
222 int end = getLineStart(i+1);
223 previousLineEnd = end;
224
225 int ltop = previousLineBottom;
226 int lbottom = getLineTop(i+1);
227 previousLineBottom = lbottom;
228 int lbaseline = lbottom - getLineDescent(i);
229
230 if (start >= spanend) {
231 Spanned sp = (Spanned) buf;
232 spanend = sp.nextSpanTransition(start, textLength,
233 LineBackgroundSpan.class);
234 spans = sp.getSpans(start, spanend,
235 LineBackgroundSpan.class);
236 }
237
238 for (int n = 0; n < spans.length; n++) {
239 LineBackgroundSpan back = (LineBackgroundSpan) spans[n];
240
Doug Felt71b8dd72010-02-16 17:27:09 -0800241 back.drawBackground(c, paint, 0, width,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800242 ltop, lbaseline, lbottom,
243 buf, start, end,
244 i);
245 }
246 }
247 // reset to their original values
248 spanend = 0;
249 previousLineBottom = getLineTop(first);
250 previousLineEnd = getLineStart(first);
Doug Felt71b8dd72010-02-16 17:27:09 -0800251 spans = NO_PARA_SPANS;
Doug Felt9f7a4442010-03-01 12:45:56 -0800252 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800253
254 // There can be a highlight even without spans if we are drawing
255 // a non-spanned transformation of a spanned editing buffer.
256 if (highlight != null) {
257 if (cursorOffsetVertical != 0) {
258 c.translate(0, cursorOffsetVertical);
259 }
260
Doug Felt71b8dd72010-02-16 17:27:09 -0800261 c.drawPath(highlight, highlightPaint);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800262
263 if (cursorOffsetVertical != 0) {
264 c.translate(0, -cursorOffsetVertical);
265 }
266 }
267
268 Alignment align = mAlignment;
Doug Felt9f7a4442010-03-01 12:45:56 -0800269
Doug Felte8e45f22010-03-29 14:58:40 -0700270 TextLine tl = TextLine.obtain();
Doug Felt71b8dd72010-02-16 17:27:09 -0800271 // Next draw the lines, one at a time.
272 // the baseline is the top of the following line minus the current
273 // line's descent.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800274 for (int i = first; i <= last; i++) {
275 int start = previousLineEnd;
276
277 previousLineEnd = getLineStart(i+1);
278 int end = getLineVisibleEnd(i, start, previousLineEnd);
279
280 int ltop = previousLineBottom;
281 int lbottom = getLineTop(i+1);
282 previousLineBottom = lbottom;
283 int lbaseline = lbottom - getLineDescent(i);
284
Doug Felt71b8dd72010-02-16 17:27:09 -0800285 boolean isFirstParaLine = false;
Doug Felt9f7a4442010-03-01 12:45:56 -0800286 if (spannedText) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800287 if (start == 0 || buf.charAt(start - 1) == '\n') {
Doug Felt71b8dd72010-02-16 17:27:09 -0800288 isFirstParaLine = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800289 }
Doug Felt71b8dd72010-02-16 17:27:09 -0800290 // New batch of paragraph styles, compute the alignment.
291 // Last alignment style wins.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800292 if (start >= spanend) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800293 Spanned sp = (Spanned) buf;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800294 spanend = sp.nextSpanTransition(start, textLength,
295 ParagraphStyle.class);
296 spans = sp.getSpans(start, spanend, ParagraphStyle.class);
Doug Felt9f7a4442010-03-01 12:45:56 -0800297
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800298 align = mAlignment;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800299 for (int n = spans.length-1; n >= 0; n--) {
300 if (spans[n] instanceof AlignmentSpan) {
301 align = ((AlignmentSpan) spans[n]).getAlignment();
302 break;
303 }
304 }
305 }
306 }
Doug Felt9f7a4442010-03-01 12:45:56 -0800307
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800308 int dir = getParagraphDirection(i);
309 int left = 0;
310 int right = mWidth;
311
Doug Felt71b8dd72010-02-16 17:27:09 -0800312 // Draw all leading margin spans. Adjust left or right according
313 // to the paragraph direction of the line.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800314 if (spannedText) {
315 final int length = spans.length;
316 for (int n = 0; n < length; n++) {
317 if (spans[n] instanceof LeadingMarginSpan) {
318 LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
319
320 if (dir == DIR_RIGHT_TO_LEFT) {
321 margin.drawLeadingMargin(c, paint, right, dir, ltop,
322 lbaseline, lbottom, buf,
Doug Felt71b8dd72010-02-16 17:27:09 -0800323 start, end, isFirstParaLine, this);
Doug Felt9f7a4442010-03-01 12:45:56 -0800324
Doug Felt71b8dd72010-02-16 17:27:09 -0800325 right -= margin.getLeadingMargin(isFirstParaLine);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800326 } else {
327 margin.drawLeadingMargin(c, paint, left, dir, ltop,
328 lbaseline, lbottom, buf,
Doug Felt71b8dd72010-02-16 17:27:09 -0800329 start, end, isFirstParaLine, this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800330
Doug Felt71b8dd72010-02-16 17:27:09 -0800331 boolean useMargin = isFirstParaLine;
Mark Wagner7b5676e2009-10-16 11:44:23 -0700332 if (margin instanceof LeadingMarginSpan.LeadingMarginSpan2) {
333 int count = ((LeadingMarginSpan.LeadingMarginSpan2)margin).getLeadingMarginLineCount();
334 useMargin = count > i;
335 }
336 left += margin.getLeadingMargin(useMargin);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800337 }
338 }
339 }
340 }
341
Doug Felt71b8dd72010-02-16 17:27:09 -0800342 // Adjust the point at which to start rendering depending on the
343 // alignment of the paragraph.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800344 int x;
345 if (align == Alignment.ALIGN_NORMAL) {
346 if (dir == DIR_LEFT_TO_RIGHT) {
347 x = left;
348 } else {
349 x = right;
350 }
351 } else {
352 int max = (int)getLineMax(i, spans, false);
353 if (align == Alignment.ALIGN_OPPOSITE) {
354 if (dir == DIR_RIGHT_TO_LEFT) {
355 x = left + max;
356 } else {
357 x = right - max;
358 }
359 } else {
360 // Alignment.ALIGN_CENTER
361 max = max & ~1;
362 int half = (right - left - max) >> 1;
363 if (dir == DIR_RIGHT_TO_LEFT) {
364 x = right - half;
365 } else {
366 x = left + half;
367 }
368 }
369 }
370
371 Directions directions = getLineDirections(i);
372 boolean hasTab = getLineContainsTab(i);
373 if (directions == DIRS_ALL_LEFT_TO_RIGHT &&
374 !spannedText && !hasTab) {
Dave Bort76c02262009-04-13 17:17:21 -0700375 if (DEBUG) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800376 Assert.assertTrue(dir == DIR_LEFT_TO_RIGHT);
377 Assert.assertNotNull(c);
378 }
Doug Felt71b8dd72010-02-16 17:27:09 -0800379 // XXX: assumes there's nothing additional to be done
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800380 c.drawText(buf, start, end, x, lbaseline, paint);
381 } else {
Doug Felte8e45f22010-03-29 14:58:40 -0700382 tl.set(paint, buf, start, end, dir, directions, hasTab, spans);
383 tl.draw(c, x, ltop, lbaseline, lbottom);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800384 }
385 }
Doug Felte8e45f22010-03-29 14:58:40 -0700386 TextLine.recycle(tl);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800387 }
388
389 /**
390 * Return the text that is displayed by this Layout.
391 */
392 public final CharSequence getText() {
393 return mText;
394 }
395
396 /**
397 * Return the base Paint properties for this layout.
398 * Do NOT change the paint, which may result in funny
399 * drawing for this layout.
400 */
401 public final TextPaint getPaint() {
402 return mPaint;
403 }
404
405 /**
406 * Return the width of this layout.
407 */
408 public final int getWidth() {
409 return mWidth;
410 }
411
412 /**
413 * Return the width to which this Layout is ellipsizing, or
414 * {@link #getWidth} if it is not doing anything special.
415 */
416 public int getEllipsizedWidth() {
417 return mWidth;
418 }
419
420 /**
421 * Increase the width of this layout to the specified width.
Doug Felt71b8dd72010-02-16 17:27:09 -0800422 * Be careful to use this only when you know it is appropriate&mdash;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800423 * it does not cause the text to reflow to use the full new width.
424 */
425 public final void increaseWidthTo(int wid) {
426 if (wid < mWidth) {
427 throw new RuntimeException("attempted to reduce Layout width");
428 }
429
430 mWidth = wid;
431 }
Doug Felt9f7a4442010-03-01 12:45:56 -0800432
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800433 /**
434 * Return the total height of this layout.
435 */
436 public int getHeight() {
Doug Felt71b8dd72010-02-16 17:27:09 -0800437 return getLineTop(getLineCount());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800438 }
439
440 /**
441 * Return the base alignment of this layout.
442 */
443 public final Alignment getAlignment() {
444 return mAlignment;
445 }
446
447 /**
448 * Return what the text height is multiplied by to get the line height.
449 */
450 public final float getSpacingMultiplier() {
451 return mSpacingMult;
452 }
453
454 /**
455 * Return the number of units of leading that are added to each line.
456 */
457 public final float getSpacingAdd() {
458 return mSpacingAdd;
459 }
460
461 /**
462 * Return the number of lines of text in this layout.
463 */
464 public abstract int getLineCount();
Doug Felt9f7a4442010-03-01 12:45:56 -0800465
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800466 /**
467 * Return the baseline for the specified line (0&hellip;getLineCount() - 1)
468 * If bounds is not null, return the top, left, right, bottom extents
469 * of the specified line in it.
470 * @param line which line to examine (0..getLineCount() - 1)
471 * @param bounds Optional. If not null, it returns the extent of the line
472 * @return the Y-coordinate of the baseline
473 */
474 public int getLineBounds(int line, Rect bounds) {
475 if (bounds != null) {
476 bounds.left = 0; // ???
477 bounds.top = getLineTop(line);
478 bounds.right = mWidth; // ???
Doug Felt71b8dd72010-02-16 17:27:09 -0800479 bounds.bottom = getLineTop(line + 1);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800480 }
481 return getLineBaseline(line);
482 }
483
484 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800485 * Return the vertical position of the top of the specified line
486 * (0&hellip;getLineCount()).
487 * If the specified line is equal to the line count, returns the
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800488 * bottom of the last line.
489 */
490 public abstract int getLineTop(int line);
491
492 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800493 * Return the descent of the specified line(0&hellip;getLineCount() - 1).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800494 */
495 public abstract int getLineDescent(int line);
496
497 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800498 * Return the text offset of the beginning of the specified line (
499 * 0&hellip;getLineCount()). If the specified line is equal to the line
500 * count, returns the length of the text.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800501 */
502 public abstract int getLineStart(int line);
503
504 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800505 * Returns the primary directionality of the paragraph containing the
506 * specified line, either 1 for left-to-right lines, or -1 for right-to-left
507 * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800508 */
509 public abstract int getParagraphDirection(int line);
510
511 /**
The Android Open Source Project10592532009-03-18 17:39:46 -0700512 * Returns whether the specified line contains one or more
513 * characters that need to be handled specially, like tabs
514 * or emoji.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800515 */
516 public abstract boolean getLineContainsTab(int line);
517
518 /**
Doug Felt71b8dd72010-02-16 17:27:09 -0800519 * Returns the directional run information for the specified line.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800520 * The array alternates counts of characters in left-to-right
521 * and right-to-left segments of the line.
Doug Felt71b8dd72010-02-16 17:27:09 -0800522 *
523 * <p>NOTE: this is inadequate to support bidirectional text, and will change.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800524 */
525 public abstract Directions getLineDirections(int line);
526
527 /**
528 * Returns the (negative) number of extra pixels of ascent padding in the
529 * top line of the Layout.
530 */
531 public abstract int getTopPadding();
532
533 /**
534 * Returns the number of extra pixels of descent padding in the
535 * bottom line of the Layout.
536 */
537 public abstract int getBottomPadding();
538
Doug Felt4e0c5e52010-03-15 16:56:02 -0700539
540 /**
541 * Returns true if the character at offset and the preceding character
542 * are at different run levels (and thus there's a split caret).
543 * @param offset the offset
544 * @return true if at a level boundary
545 */
546 private boolean isLevelBoundary(int offset) {
Doug Felt9f7a4442010-03-01 12:45:56 -0800547 int line = getLineForOffset(offset);
Doug Felt4e0c5e52010-03-15 16:56:02 -0700548 Directions dirs = getLineDirections(line);
549 if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) {
550 return false;
551 }
552
553 int[] runs = dirs.mDirections;
Doug Felt9f7a4442010-03-01 12:45:56 -0800554 int lineStart = getLineStart(line);
Doug Felt4e0c5e52010-03-15 16:56:02 -0700555 int lineEnd = getLineEnd(line);
556 if (offset == lineStart || offset == lineEnd) {
557 int paraLevel = getParagraphDirection(line) == 1 ? 0 : 1;
558 int runIndex = offset == lineStart ? 0 : runs.length - 2;
559 return ((runs[runIndex + 1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK) != paraLevel;
560 }
561
562 offset -= lineStart;
Doug Felt9f7a4442010-03-01 12:45:56 -0800563 for (int i = 0; i < runs.length; i += 2) {
Doug Felt4e0c5e52010-03-15 16:56:02 -0700564 if (offset == runs[i]) {
565 return true;
Doug Felt9f7a4442010-03-01 12:45:56 -0800566 }
567 }
Doug Felt4e0c5e52010-03-15 16:56:02 -0700568 return false;
Doug Felt9f7a4442010-03-01 12:45:56 -0800569 }
570
571 private boolean primaryIsTrailingPrevious(int offset) {
572 int line = getLineForOffset(offset);
573 int lineStart = getLineStart(line);
Doug Felt4e0c5e52010-03-15 16:56:02 -0700574 int lineEnd = getLineEnd(line);
Doug Felt9f7a4442010-03-01 12:45:56 -0800575 int[] runs = getLineDirections(line).mDirections;
576
577 int levelAt = -1;
578 for (int i = 0; i < runs.length; i += 2) {
579 int start = lineStart + runs[i];
580 int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
581 if (limit > lineEnd) {
582 limit = lineEnd;
583 }
584 if (offset >= start && offset < limit) {
585 if (offset > start) {
586 // Previous character is at same level, so don't use trailing.
587 return false;
588 }
589 levelAt = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
590 break;
591 }
592 }
593 if (levelAt == -1) {
594 // Offset was limit of line.
595 levelAt = getParagraphDirection(line) == 1 ? 0 : 1;
596 }
597
598 // At level boundary, check previous level.
599 int levelBefore = -1;
600 if (offset == lineStart) {
601 levelBefore = getParagraphDirection(line) == 1 ? 0 : 1;
602 } else {
603 offset -= 1;
604 for (int i = 0; i < runs.length; i += 2) {
605 int start = lineStart + runs[i];
606 int limit = start + (runs[i+1] & RUN_LENGTH_MASK);
607 if (limit > lineEnd) {
608 limit = lineEnd;
609 }
610 if (offset >= start && offset < limit) {
611 levelBefore = (runs[i+1] >>> RUN_LEVEL_SHIFT) & RUN_LEVEL_MASK;
612 break;
613 }
614 }
615 }
616
617 return levelBefore < levelAt;
618 }
619
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800620 /**
621 * Get the primary horizontal position for the specified text offset.
622 * This is the location where a new character would be inserted in
623 * the paragraph's primary direction.
624 */
625 public float getPrimaryHorizontal(int offset) {
Doug Felt9f7a4442010-03-01 12:45:56 -0800626 boolean trailing = primaryIsTrailingPrevious(offset);
627 return getHorizontal(offset, trailing);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800628 }
629
630 /**
631 * Get the secondary horizontal position for the specified text offset.
632 * This is the location where a new character would be inserted in
633 * the direction other than the paragraph's primary direction.
634 */
635 public float getSecondaryHorizontal(int offset) {
Doug Felt9f7a4442010-03-01 12:45:56 -0800636 boolean trailing = primaryIsTrailingPrevious(offset);
637 return getHorizontal(offset, !trailing);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800638 }
639
Doug Felt9f7a4442010-03-01 12:45:56 -0800640 private float getHorizontal(int offset, boolean trailing) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800641 int line = getLineForOffset(offset);
642
Doug Felt9f7a4442010-03-01 12:45:56 -0800643 return getHorizontal(offset, trailing, line);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800644 }
645
Doug Felt9f7a4442010-03-01 12:45:56 -0800646 private float getHorizontal(int offset, boolean trailing, int line) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800647 int start = getLineStart(line);
Doug Felte8e45f22010-03-29 14:58:40 -0700648 int end = getLineEnd(line);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800649 int dir = getParagraphDirection(line);
650 boolean tab = getLineContainsTab(line);
651 Directions directions = getLineDirections(line);
652
653 TabStopSpan[] tabs = null;
654 if (tab && mText instanceof Spanned) {
655 tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class);
656 }
657
Doug Felte8e45f22010-03-29 14:58:40 -0700658 TextLine tl = TextLine.obtain();
659 tl.set(mPaint, mText, start, end, dir, directions, tab, tabs);
660 float wid = tl.measure(offset - start, trailing, null);
661 TextLine.recycle(tl);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800662
663 Alignment align = getParagraphAlignment(line);
664 int left = getParagraphLeft(line);
665 int right = getParagraphRight(line);
666
667 if (align == Alignment.ALIGN_NORMAL) {
668 if (dir == DIR_RIGHT_TO_LEFT)
669 return right + wid;
670 else
671 return left + wid;
672 }
673
674 float max = getLineMax(line);
675
676 if (align == Alignment.ALIGN_OPPOSITE) {
677 if (dir == DIR_RIGHT_TO_LEFT)
678 return left + max + wid;
679 else
680 return right - max + wid;
681 } else { /* align == Alignment.ALIGN_CENTER */
682 int imax = ((int) max) & ~1;
683
684 if (dir == DIR_RIGHT_TO_LEFT)
685 return right - (((right - left) - imax) / 2) + wid;
686 else
687 return left + ((right - left) - imax) / 2 + wid;
688 }
689 }
690
691 /**
692 * Get the leftmost position that should be exposed for horizontal
693 * scrolling on the specified line.
694 */
695 public float getLineLeft(int line) {
696 int dir = getParagraphDirection(line);
697 Alignment align = getParagraphAlignment(line);
698
699 if (align == Alignment.ALIGN_NORMAL) {
700 if (dir == DIR_RIGHT_TO_LEFT)
701 return getParagraphRight(line) - getLineMax(line);
702 else
703 return 0;
704 } else if (align == Alignment.ALIGN_OPPOSITE) {
705 if (dir == DIR_RIGHT_TO_LEFT)
706 return 0;
707 else
708 return mWidth - getLineMax(line);
709 } else { /* align == Alignment.ALIGN_CENTER */
710 int left = getParagraphLeft(line);
711 int right = getParagraphRight(line);
712 int max = ((int) getLineMax(line)) & ~1;
713
714 return left + ((right - left) - max) / 2;
715 }
716 }
717
718 /**
719 * Get the rightmost position that should be exposed for horizontal
720 * scrolling on the specified line.
721 */
722 public float getLineRight(int line) {
723 int dir = getParagraphDirection(line);
724 Alignment align = getParagraphAlignment(line);
725
726 if (align == Alignment.ALIGN_NORMAL) {
727 if (dir == DIR_RIGHT_TO_LEFT)
728 return mWidth;
729 else
730 return getParagraphLeft(line) + getLineMax(line);
731 } else if (align == Alignment.ALIGN_OPPOSITE) {
732 if (dir == DIR_RIGHT_TO_LEFT)
733 return getLineMax(line);
734 else
735 return mWidth;
736 } else { /* align == Alignment.ALIGN_CENTER */
737 int left = getParagraphLeft(line);
738 int right = getParagraphRight(line);
739 int max = ((int) getLineMax(line)) & ~1;
740
741 return right - ((right - left) - max) / 2;
742 }
743 }
744
745 /**
746 * Gets the horizontal extent of the specified line, excluding
747 * trailing whitespace.
748 */
749 public float getLineMax(int line) {
750 return getLineMax(line, null, false);
751 }
752
753 /**
754 * Gets the horizontal extent of the specified line, including
755 * trailing whitespace.
756 */
757 public float getLineWidth(int line) {
758 return getLineMax(line, null, true);
759 }
760
761 private float getLineMax(int line, Object[] tabs, boolean full) {
762 int start = getLineStart(line);
Doug Felte8e45f22010-03-29 14:58:40 -0700763 int end = full ? getLineEnd(line) : getLineVisibleEnd(line);
764 boolean hasTabs = getLineContainsTab(line);
765 Directions directions = getLineDirections(line);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800766
Doug Felte8e45f22010-03-29 14:58:40 -0700767 TextLine tl = TextLine.obtain();
768 tl.set(mPaint, mText, start, end, 1, directions, hasTabs, tabs);
769 float width = tl.metrics(null);
770 TextLine.recycle(tl);
771 return width;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800772 }
773
774 /**
775 * Get the line number corresponding to the specified vertical position.
776 * If you ask for a position above 0, you get 0; if you ask for a position
777 * below the bottom of the text, you get the last line.
778 */
779 // FIXME: It may be faster to do a linear search for layouts without many lines.
780 public int getLineForVertical(int vertical) {
781 int high = getLineCount(), low = -1, guess;
782
783 while (high - low > 1) {
784 guess = (high + low) / 2;
785
786 if (getLineTop(guess) > vertical)
787 high = guess;
788 else
789 low = guess;
790 }
791
792 if (low < 0)
793 return 0;
794 else
795 return low;
796 }
797
798 /**
799 * Get the line number on which the specified text offset appears.
800 * If you ask for a position before 0, you get 0; if you ask for a position
801 * beyond the end of the text, you get the last line.
802 */
803 public int getLineForOffset(int offset) {
804 int high = getLineCount(), low = -1, guess;
805
806 while (high - low > 1) {
807 guess = (high + low) / 2;
808
809 if (getLineStart(guess) > offset)
810 high = guess;
811 else
812 low = guess;
813 }
814
815 if (low < 0)
816 return 0;
817 else
818 return low;
819 }
820
821 /**
Doug Felt9f7a4442010-03-01 12:45:56 -0800822 * Get the character offset on the specified line whose position is
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800823 * closest to the specified horizontal position.
824 */
825 public int getOffsetForHorizontal(int line, float horiz) {
826 int max = getLineEnd(line) - 1;
827 int min = getLineStart(line);
828 Directions dirs = getLineDirections(line);
829
830 if (line == getLineCount() - 1)
831 max++;
832
833 int best = min;
834 float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz);
835
Doug Felt9f7a4442010-03-01 12:45:56 -0800836 for (int i = 0; i < dirs.mDirections.length; i += 2) {
837 int here = min + dirs.mDirections[i];
838 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
839 int swap = (dirs.mDirections[i+1] & RUN_RTL_FLAG) != 0 ? -1 : 1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800840
841 if (there > max)
842 there = max;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800843 int high = there - 1 + 1, low = here + 1 - 1, guess;
844
845 while (high - low > 1) {
846 guess = (high + low) / 2;
847 int adguess = getOffsetAtStartOf(guess);
848
849 if (getPrimaryHorizontal(adguess) * swap >= horiz * swap)
850 high = guess;
851 else
852 low = guess;
853 }
854
855 if (low < here + 1)
856 low = here + 1;
857
858 if (low < there) {
859 low = getOffsetAtStartOf(low);
860
861 float dist = Math.abs(getPrimaryHorizontal(low) - horiz);
862
863 int aft = TextUtils.getOffsetAfter(mText, low);
864 if (aft < there) {
865 float other = Math.abs(getPrimaryHorizontal(aft) - horiz);
866
867 if (other < dist) {
868 dist = other;
869 low = aft;
870 }
871 }
872
873 if (dist < bestdist) {
874 bestdist = dist;
Doug Felt9f7a4442010-03-01 12:45:56 -0800875 best = low;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800876 }
877 }
878
879 float dist = Math.abs(getPrimaryHorizontal(here) - horiz);
880
881 if (dist < bestdist) {
882 bestdist = dist;
883 best = here;
884 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800885 }
886
887 float dist = Math.abs(getPrimaryHorizontal(max) - horiz);
888
889 if (dist < bestdist) {
890 bestdist = dist;
891 best = max;
892 }
893
894 return best;
895 }
896
897 /**
898 * Return the text offset after the last character on the specified line.
899 */
900 public final int getLineEnd(int line) {
901 return getLineStart(line + 1);
902 }
903
Doug Felt9f7a4442010-03-01 12:45:56 -0800904 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800905 * Return the text offset after the last visible character (so whitespace
906 * is not counted) on the specified line.
907 */
908 public int getLineVisibleEnd(int line) {
909 return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
910 }
Doug Felt9f7a4442010-03-01 12:45:56 -0800911
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800912 private int getLineVisibleEnd(int line, int start, int end) {
Dave Bort76c02262009-04-13 17:17:21 -0700913 if (DEBUG) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800914 Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end);
915 }
916
917 CharSequence text = mText;
918 char ch;
919 if (line == getLineCount() - 1) {
920 return end;
921 }
922
923 for (; end > start; end--) {
924 ch = text.charAt(end - 1);
925
926 if (ch == '\n') {
927 return end - 1;
928 }
929
930 if (ch != ' ' && ch != '\t') {
931 break;
932 }
933
934 }
935
936 return end;
937 }
938
939 /**
940 * Return the vertical position of the bottom of the specified line.
941 */
942 public final int getLineBottom(int line) {
943 return getLineTop(line + 1);
944 }
945
946 /**
947 * Return the vertical position of the baseline of the specified line.
948 */
949 public final int getLineBaseline(int line) {
950 // getLineTop(line+1) == getLineTop(line)
951 return getLineTop(line+1) - getLineDescent(line);
952 }
953
954 /**
955 * Get the ascent of the text on the specified line.
956 * The return value is negative to match the Paint.ascent() convention.
957 */
958 public final int getLineAscent(int line) {
959 // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line)
960 return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
961 }
962
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800963 public int getOffsetToLeftOf(int offset) {
Doug Felt9f7a4442010-03-01 12:45:56 -0800964 return getOffsetToLeftRightOf(offset, true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800965 }
966
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800967 public int getOffsetToRightOf(int offset) {
Doug Felt9f7a4442010-03-01 12:45:56 -0800968 return getOffsetToLeftRightOf(offset, false);
969 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800970
Doug Felt9f7a4442010-03-01 12:45:56 -0800971 private int getOffsetToLeftRightOf(int caret, boolean toLeft) {
972 int line = getLineForOffset(caret);
973 int lineStart = getLineStart(line);
974 int lineEnd = getLineEnd(line);
Doug Felte8e45f22010-03-29 14:58:40 -0700975 int lineDir = getParagraphDirection(line);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800976
Doug Felte8e45f22010-03-29 14:58:40 -0700977 boolean advance = toLeft == (lineDir == DIR_RIGHT_TO_LEFT);
978 if (caret == (advance ? lineEnd : lineStart)) {
979 // walking off line, so look at the line we're headed to
980 if (caret == lineStart) {
981 if (line > 0) {
982 --line;
983 } else {
984 return caret; // at very start, don't move
Doug Felt9f7a4442010-03-01 12:45:56 -0800985 }
Doug Felte8e45f22010-03-29 14:58:40 -0700986 } else {
987 if (line < getLineCount() - 1) {
988 ++line;
989 } else {
990 return caret; // at very end, don't move
991 }
Doug Felt9f7a4442010-03-01 12:45:56 -0800992 }
Doug Felt9f7a4442010-03-01 12:45:56 -0800993
Doug Felte8e45f22010-03-29 14:58:40 -0700994 lineStart = getLineStart(line);
995 lineEnd = getLineEnd(line);
996 int newDir = getParagraphDirection(line);
997 if (newDir != lineDir) {
998 // unusual case. we want to walk onto the line, but it runs
999 // in a different direction than this one, so we fake movement
1000 // in the opposite direction.
1001 toLeft = !toLeft;
1002 lineDir = newDir;
1003 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001004 }
Doug Felt9f7a4442010-03-01 12:45:56 -08001005
Doug Felte8e45f22010-03-29 14:58:40 -07001006 Directions directions = getLineDirections(line);
Doug Felt9f7a4442010-03-01 12:45:56 -08001007
Doug Felte8e45f22010-03-29 14:58:40 -07001008 TextLine tl = TextLine.obtain();
1009 // XXX: we don't care about tabs
1010 tl.set(mPaint, mText, lineStart, lineEnd, lineDir, directions, false, null);
1011 caret = lineStart + tl.getOffsetToLeftRightOf(caret - lineStart, toLeft);
1012 tl = TextLine.recycle(tl);
1013 return caret;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001014 }
1015
1016 private int getOffsetAtStartOf(int offset) {
Doug Felt9f7a4442010-03-01 12:45:56 -08001017 // XXX this probably should skip local reorderings and
1018 // zero-width characters, look at callers
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001019 if (offset == 0)
1020 return 0;
1021
1022 CharSequence text = mText;
1023 char c = text.charAt(offset);
1024
1025 if (c >= '\uDC00' && c <= '\uDFFF') {
1026 char c1 = text.charAt(offset - 1);
1027
1028 if (c1 >= '\uD800' && c1 <= '\uDBFF')
1029 offset -= 1;
1030 }
1031
1032 if (mSpannedText) {
1033 ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
1034 ReplacementSpan.class);
1035
1036 for (int i = 0; i < spans.length; i++) {
1037 int start = ((Spanned) text).getSpanStart(spans[i]);
1038 int end = ((Spanned) text).getSpanEnd(spans[i]);
1039
1040 if (start < offset && end > offset)
1041 offset = start;
1042 }
1043 }
1044
1045 return offset;
1046 }
1047
1048 /**
1049 * Fills in the specified Path with a representation of a cursor
1050 * at the specified offset. This will often be a vertical line
Doug Felt4e0c5e52010-03-15 16:56:02 -07001051 * but can be multiple discontinuous lines in text with multiple
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001052 * directionalities.
1053 */
1054 public void getCursorPath(int point, Path dest,
1055 CharSequence editingBuffer) {
1056 dest.reset();
1057
1058 int line = getLineForOffset(point);
1059 int top = getLineTop(line);
1060 int bottom = getLineTop(line+1);
1061
1062 float h1 = getPrimaryHorizontal(point) - 0.5f;
Doug Felt4e0c5e52010-03-15 16:56:02 -07001063 float h2 = isLevelBoundary(point) ?
1064 getSecondaryHorizontal(point) - 0.5f : h1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001065
1066 int caps = TextKeyListener.getMetaState(editingBuffer,
1067 KeyEvent.META_SHIFT_ON) |
1068 TextKeyListener.getMetaState(editingBuffer,
1069 TextKeyListener.META_SELECTING);
1070 int fn = TextKeyListener.getMetaState(editingBuffer,
1071 KeyEvent.META_ALT_ON);
1072 int dist = 0;
1073
1074 if (caps != 0 || fn != 0) {
1075 dist = (bottom - top) >> 2;
1076
1077 if (fn != 0)
1078 top += dist;
1079 if (caps != 0)
1080 bottom -= dist;
1081 }
1082
1083 if (h1 < 0.5f)
1084 h1 = 0.5f;
1085 if (h2 < 0.5f)
1086 h2 = 0.5f;
1087
1088 if (h1 == h2) {
1089 dest.moveTo(h1, top);
1090 dest.lineTo(h1, bottom);
1091 } else {
1092 dest.moveTo(h1, top);
1093 dest.lineTo(h1, (top + bottom) >> 1);
1094
1095 dest.moveTo(h2, (top + bottom) >> 1);
1096 dest.lineTo(h2, bottom);
1097 }
1098
1099 if (caps == 2) {
1100 dest.moveTo(h2, bottom);
1101 dest.lineTo(h2 - dist, bottom + dist);
1102 dest.lineTo(h2, bottom);
1103 dest.lineTo(h2 + dist, bottom + dist);
1104 } else if (caps == 1) {
1105 dest.moveTo(h2, bottom);
1106 dest.lineTo(h2 - dist, bottom + dist);
1107
1108 dest.moveTo(h2 - dist, bottom + dist - 0.5f);
1109 dest.lineTo(h2 + dist, bottom + dist - 0.5f);
1110
1111 dest.moveTo(h2 + dist, bottom + dist);
1112 dest.lineTo(h2, bottom);
1113 }
1114
1115 if (fn == 2) {
1116 dest.moveTo(h1, top);
1117 dest.lineTo(h1 - dist, top - dist);
1118 dest.lineTo(h1, top);
1119 dest.lineTo(h1 + dist, top - dist);
1120 } else if (fn == 1) {
1121 dest.moveTo(h1, top);
1122 dest.lineTo(h1 - dist, top - dist);
1123
1124 dest.moveTo(h1 - dist, top - dist + 0.5f);
1125 dest.lineTo(h1 + dist, top - dist + 0.5f);
1126
1127 dest.moveTo(h1 + dist, top - dist);
1128 dest.lineTo(h1, top);
1129 }
1130 }
1131
1132 private void addSelection(int line, int start, int end,
1133 int top, int bottom, Path dest) {
1134 int linestart = getLineStart(line);
1135 int lineend = getLineEnd(line);
1136 Directions dirs = getLineDirections(line);
1137
1138 if (lineend > linestart && mText.charAt(lineend - 1) == '\n')
1139 lineend--;
1140
Doug Felt9f7a4442010-03-01 12:45:56 -08001141 for (int i = 0; i < dirs.mDirections.length; i += 2) {
1142 int here = linestart + dirs.mDirections[i];
1143 int there = here + (dirs.mDirections[i+1] & RUN_LENGTH_MASK);
1144
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001145 if (there > lineend)
1146 there = lineend;
1147
1148 if (start <= there && end >= here) {
1149 int st = Math.max(start, here);
1150 int en = Math.min(end, there);
1151
1152 if (st != en) {
Doug Felt9f7a4442010-03-01 12:45:56 -08001153 float h1 = getHorizontal(st, false, line);
1154 float h2 = getHorizontal(en, true, line);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001155
1156 dest.addRect(h1, top, h2, bottom, Path.Direction.CW);
1157 }
1158 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001159 }
1160 }
1161
1162 /**
1163 * Fills in the specified Path with a representation of a highlight
1164 * between the specified offsets. This will often be a rectangle
1165 * or a potentially discontinuous set of rectangles. If the start
1166 * and end are the same, the returned path is empty.
1167 */
1168 public void getSelectionPath(int start, int end, Path dest) {
1169 dest.reset();
1170
1171 if (start == end)
1172 return;
1173
1174 if (end < start) {
1175 int temp = end;
1176 end = start;
1177 start = temp;
1178 }
1179
1180 int startline = getLineForOffset(start);
1181 int endline = getLineForOffset(end);
1182
1183 int top = getLineTop(startline);
1184 int bottom = getLineBottom(endline);
1185
1186 if (startline == endline) {
1187 addSelection(startline, start, end, top, bottom, dest);
1188 } else {
1189 final float width = mWidth;
1190
1191 addSelection(startline, start, getLineEnd(startline),
1192 top, getLineBottom(startline), dest);
Doug Felt9f7a4442010-03-01 12:45:56 -08001193
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001194 if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT)
1195 dest.addRect(getLineLeft(startline), top,
1196 0, getLineBottom(startline), Path.Direction.CW);
1197 else
1198 dest.addRect(getLineRight(startline), top,
1199 width, getLineBottom(startline), Path.Direction.CW);
1200
1201 for (int i = startline + 1; i < endline; i++) {
1202 top = getLineTop(i);
1203 bottom = getLineBottom(i);
1204 dest.addRect(0, top, width, bottom, Path.Direction.CW);
1205 }
1206
1207 top = getLineTop(endline);
1208 bottom = getLineBottom(endline);
1209
1210 addSelection(endline, getLineStart(endline), end,
1211 top, bottom, dest);
1212
1213 if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT)
1214 dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW);
1215 else
1216 dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW);
1217 }
1218 }
1219
1220 /**
1221 * Get the alignment of the specified paragraph, taking into account
1222 * markup attached to it.
1223 */
1224 public final Alignment getParagraphAlignment(int line) {
1225 Alignment align = mAlignment;
1226
1227 if (mSpannedText) {
1228 Spanned sp = (Spanned) mText;
1229 AlignmentSpan[] spans = sp.getSpans(getLineStart(line),
1230 getLineEnd(line),
1231 AlignmentSpan.class);
1232
1233 int spanLength = spans.length;
1234 if (spanLength > 0) {
1235 align = spans[spanLength-1].getAlignment();
1236 }
1237 }
1238
1239 return align;
1240 }
1241
1242 /**
1243 * Get the left edge of the specified paragraph, inset by left margins.
1244 */
1245 public final int getParagraphLeft(int line) {
1246 int dir = getParagraphDirection(line);
1247
1248 int left = 0;
1249
1250 boolean par = false;
1251 int off = getLineStart(line);
1252 if (off == 0 || mText.charAt(off - 1) == '\n')
1253 par = true;
1254
1255 if (dir == DIR_LEFT_TO_RIGHT) {
1256 if (mSpannedText) {
1257 Spanned sp = (Spanned) mText;
1258 LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
1259 getLineEnd(line),
1260 LeadingMarginSpan.class);
1261
1262 for (int i = 0; i < spans.length; i++) {
Mark Wagner7b5676e2009-10-16 11:44:23 -07001263 boolean margin = par;
1264 LeadingMarginSpan span = spans[i];
1265 if (span instanceof LeadingMarginSpan.LeadingMarginSpan2) {
1266 int count = ((LeadingMarginSpan.LeadingMarginSpan2)span).getLeadingMarginLineCount();
1267 margin = count >= line;
1268 }
1269 left += span.getLeadingMargin(margin);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001270 }
1271 }
1272 }
1273
1274 return left;
1275 }
1276
1277 /**
1278 * Get the right edge of the specified paragraph, inset by right margins.
1279 */
1280 public final int getParagraphRight(int line) {
1281 int dir = getParagraphDirection(line);
1282
1283 int right = mWidth;
1284
1285 boolean par = false;
1286 int off = getLineStart(line);
1287 if (off == 0 || mText.charAt(off - 1) == '\n')
1288 par = true;
1289
1290
1291 if (dir == DIR_RIGHT_TO_LEFT) {
1292 if (mSpannedText) {
1293 Spanned sp = (Spanned) mText;
1294 LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line),
1295 getLineEnd(line),
1296 LeadingMarginSpan.class);
1297
1298 for (int i = 0; i < spans.length; i++) {
1299 right -= spans[i].getLeadingMargin(par);
1300 }
1301 }
1302 }
1303
1304 return right;
1305 }
1306
Doug Felte8e45f22010-03-29 14:58:40 -07001307 /* package */
1308 static float measurePara(TextPaint paint, TextPaint workPaint,
1309 CharSequence text, int start, int end, boolean hasTabs,
1310 Object[] tabs) {
1311
1312 MeasuredText mt = MeasuredText.obtain();
1313 TextLine tl = TextLine.obtain();
1314 try {
1315 mt.setPara(text, start, end, DIR_REQUEST_LTR);
1316 Directions directions;
1317 if (mt.mEasy){
1318 directions = DIRS_ALL_LEFT_TO_RIGHT;
1319 } else {
1320 directions = AndroidBidi.directions(mt.mDir, mt.mLevels,
1321 0, mt.mChars, 0, mt.mLen);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001322 }
Doug Felte8e45f22010-03-29 14:58:40 -07001323 tl.set(paint, text, start, end, 1, directions, hasTabs, tabs);
1324 return tl.metrics(null);
1325 } finally {
1326 TextLine.recycle(tl);
1327 MeasuredText.recycle(mt);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001328 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001329 }
1330
Doug Felt71b8dd72010-02-16 17:27:09 -08001331 /**
1332 * Returns the position of the next tab stop after h on the line.
1333 *
1334 * @param text the text
1335 * @param start start of the line
1336 * @param end limit of the line
1337 * @param h the current horizontal offset
1338 * @param tabs the tabs, can be null. If it is null, any tabs in effect
1339 * on the line will be used. If there are no tabs, a default offset
1340 * will be used to compute the tab stop.
1341 * @return the offset of the next tab stop.
1342 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001343 /* package */ static float nextTab(CharSequence text, int start, int end,
1344 float h, Object[] tabs) {
1345 float nh = Float.MAX_VALUE;
1346 boolean alltabs = false;
1347
1348 if (text instanceof Spanned) {
1349 if (tabs == null) {
1350 tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class);
1351 alltabs = true;
1352 }
1353
1354 for (int i = 0; i < tabs.length; i++) {
1355 if (!alltabs) {
1356 if (!(tabs[i] instanceof TabStopSpan))
1357 continue;
1358 }
1359
1360 int where = ((TabStopSpan) tabs[i]).getTabStop();
1361
1362 if (where < nh && where > h)
1363 nh = where;
1364 }
1365
1366 if (nh != Float.MAX_VALUE)
1367 return nh;
1368 }
1369
1370 return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT;
1371 }
1372
1373 protected final boolean isSpanned() {
1374 return mSpannedText;
1375 }
1376
1377 private void ellipsize(int start, int end, int line,
1378 char[] dest, int destoff) {
1379 int ellipsisCount = getEllipsisCount(line);
1380
1381 if (ellipsisCount == 0) {
1382 return;
1383 }
1384
1385 int ellipsisStart = getEllipsisStart(line);
1386 int linestart = getLineStart(line);
1387
1388 for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) {
1389 char c;
1390
1391 if (i == ellipsisStart) {
1392 c = '\u2026'; // ellipsis
1393 } else {
1394 c = '\uFEFF'; // 0-width space
1395 }
1396
1397 int a = i + linestart;
1398
1399 if (a >= start && a < end) {
1400 dest[destoff + a - start] = c;
1401 }
1402 }
1403 }
1404
1405 /**
1406 * Stores information about bidirectional (left-to-right or right-to-left)
Doug Felt9f7a4442010-03-01 12:45:56 -08001407 * text within the layout of a line.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001408 */
1409 public static class Directions {
Doug Felt9f7a4442010-03-01 12:45:56 -08001410 // Directions represents directional runs within a line of text.
1411 // Runs are pairs of ints listed in visual order, starting from the
1412 // leading margin. The first int of each pair is the offset from
1413 // the first character of the line to the start of the run. The
1414 // second int represents both the length and level of the run.
1415 // The length is in the lower bits, accessed by masking with
1416 // DIR_LENGTH_MASK. The level is in the higher bits, accessed
1417 // by shifting by DIR_LEVEL_SHIFT and masking by DIR_LEVEL_MASK.
1418 // To simply test for an RTL direction, test the bit using
1419 // DIR_RTL_FLAG, if set then the direction is rtl.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001420
Doug Felt9f7a4442010-03-01 12:45:56 -08001421 /* package */ int[] mDirections;
1422 /* package */ Directions(int[] dirs) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001423 mDirections = dirs;
1424 }
1425 }
1426
1427 /**
1428 * Return the offset of the first character to be ellipsized away,
1429 * relative to the start of the line. (So 0 if the beginning of the
1430 * line is ellipsized, not getLineStart().)
1431 */
1432 public abstract int getEllipsisStart(int line);
Doug Felte8e45f22010-03-29 14:58:40 -07001433
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001434 /**
1435 * Returns the number of characters to be ellipsized away, or 0 if
1436 * no ellipsis is to take place.
1437 */
1438 public abstract int getEllipsisCount(int line);
1439
1440 /* package */ static class Ellipsizer implements CharSequence, GetChars {
1441 /* package */ CharSequence mText;
1442 /* package */ Layout mLayout;
1443 /* package */ int mWidth;
1444 /* package */ TextUtils.TruncateAt mMethod;
1445
1446 public Ellipsizer(CharSequence s) {
1447 mText = s;
1448 }
1449
1450 public char charAt(int off) {
1451 char[] buf = TextUtils.obtain(1);
1452 getChars(off, off + 1, buf, 0);
1453 char ret = buf[0];
1454
1455 TextUtils.recycle(buf);
1456 return ret;
1457 }
1458
1459 public void getChars(int start, int end, char[] dest, int destoff) {
1460 int line1 = mLayout.getLineForOffset(start);
1461 int line2 = mLayout.getLineForOffset(end);
1462
1463 TextUtils.getChars(mText, start, end, dest, destoff);
1464
1465 for (int i = line1; i <= line2; i++) {
1466 mLayout.ellipsize(start, end, i, dest, destoff);
1467 }
1468 }
1469
1470 public int length() {
1471 return mText.length();
1472 }
Doug Felt9f7a4442010-03-01 12:45:56 -08001473
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001474 public CharSequence subSequence(int start, int end) {
1475 char[] s = new char[end - start];
1476 getChars(start, end, s, 0);
1477 return new String(s);
1478 }
1479
1480 public String toString() {
1481 char[] s = new char[length()];
1482 getChars(0, length(), s, 0);
1483 return new String(s);
1484 }
1485
1486 }
1487
1488 /* package */ static class SpannedEllipsizer
1489 extends Ellipsizer implements Spanned {
1490 private Spanned mSpanned;
1491
1492 public SpannedEllipsizer(CharSequence display) {
1493 super(display);
1494 mSpanned = (Spanned) display;
1495 }
1496
1497 public <T> T[] getSpans(int start, int end, Class<T> type) {
1498 return mSpanned.getSpans(start, end, type);
1499 }
1500
1501 public int getSpanStart(Object tag) {
1502 return mSpanned.getSpanStart(tag);
1503 }
1504
1505 public int getSpanEnd(Object tag) {
1506 return mSpanned.getSpanEnd(tag);
1507 }
1508
1509 public int getSpanFlags(Object tag) {
1510 return mSpanned.getSpanFlags(tag);
1511 }
1512
1513 public int nextSpanTransition(int start, int limit, Class type) {
1514 return mSpanned.nextSpanTransition(start, limit, type);
1515 }
1516
1517 public CharSequence subSequence(int start, int end) {
1518 char[] s = new char[end - start];
1519 getChars(start, end, s, 0);
1520
1521 SpannableString ss = new SpannableString(new String(s));
1522 TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0);
1523 return ss;
1524 }
1525 }
1526
1527 private CharSequence mText;
1528 private TextPaint mPaint;
1529 /* package */ TextPaint mWorkPaint;
1530 private int mWidth;
1531 private Alignment mAlignment = Alignment.ALIGN_NORMAL;
1532 private float mSpacingMult;
1533 private float mSpacingAdd;
1534 private static Rect sTempRect = new Rect();
1535 private boolean mSpannedText;
1536
1537 public static final int DIR_LEFT_TO_RIGHT = 1;
1538 public static final int DIR_RIGHT_TO_LEFT = -1;
Doug Felt9f7a4442010-03-01 12:45:56 -08001539
Doug Felt20178d62010-02-22 13:39:01 -08001540 /* package */ static final int DIR_REQUEST_LTR = 1;
1541 /* package */ static final int DIR_REQUEST_RTL = -1;
1542 /* package */ static final int DIR_REQUEST_DEFAULT_LTR = 2;
1543 /* package */ static final int DIR_REQUEST_DEFAULT_RTL = -2;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001544
Doug Felt9f7a4442010-03-01 12:45:56 -08001545 /* package */ static final int RUN_LENGTH_MASK = 0x03ffffff;
1546 /* package */ static final int RUN_LEVEL_SHIFT = 26;
1547 /* package */ static final int RUN_LEVEL_MASK = 0x3f;
1548 /* package */ static final int RUN_RTL_FLAG = 1 << RUN_LEVEL_SHIFT;
1549
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001550 public enum Alignment {
1551 ALIGN_NORMAL,
1552 ALIGN_OPPOSITE,
1553 ALIGN_CENTER,
1554 // XXX ALIGN_LEFT,
1555 // XXX ALIGN_RIGHT,
1556 }
1557
1558 private static final int TAB_INCREMENT = 20;
1559
1560 /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT =
Doug Felt9f7a4442010-03-01 12:45:56 -08001561 new Directions(new int[] { 0, RUN_LENGTH_MASK });
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001562 /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT =
Doug Felt9f7a4442010-03-01 12:45:56 -08001563 new Directions(new int[] { 0, RUN_LENGTH_MASK | RUN_RTL_FLAG });
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001564}
1565