diff options
| author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
|---|---|---|
| committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
| commit | 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch) | |
| tree | d88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/text/Layout.java | |
| parent | d83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff) | |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/text/Layout.java')
| -rw-r--r-- | core/java/android/text/Layout.java | 1747 |
1 files changed, 1747 insertions, 0 deletions
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java new file mode 100644 index 000000000000..95acf9d23617 --- /dev/null +++ b/core/java/android/text/Layout.java @@ -0,0 +1,1747 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Path; +import com.android.internal.util.ArrayUtils; +import android.util.Config; + +import junit.framework.Assert; +import android.text.style.*; +import android.text.method.TextKeyListener; +import android.view.KeyEvent; + +/** + * A base class that manages text layout in visual elements on + * the screen. + * <p>For text that will be edited, use a {@link DynamicLayout}, + * which will be updated as the text changes. + * For text that will not change, use a {@link StaticLayout}. + */ +public abstract class Layout { + /** + * Return how wide a layout would be necessary to display the + * specified text with one line per paragraph. + */ + public static float getDesiredWidth(CharSequence source, + TextPaint paint) { + return getDesiredWidth(source, 0, source.length(), paint); + } + + /** + * Return how wide a layout would be necessary to display the + * specified text slice with one line per paragraph. + */ + public static float getDesiredWidth(CharSequence source, + int start, int end, + TextPaint paint) { + float need = 0; + TextPaint workPaint = new TextPaint(); + + int next; + for (int i = start; i <= end; i = next) { + next = TextUtils.indexOf(source, '\n', i, end); + + if (next < 0) + next = end; + + float w = measureText(paint, workPaint, + source, i, next, null, true, null); + + if (w > need) + need = w; + + next++; + } + + return need; + } + + /** + * Subclasses of Layout use this constructor to set the display text, + * width, and other standard properties. + */ + protected Layout(CharSequence text, TextPaint paint, + int width, Alignment align, + float spacingmult, float spacingadd) { + if (width < 0) + throw new IllegalArgumentException("Layout: " + width + " < 0"); + + mText = text; + mPaint = paint; + mWorkPaint = new TextPaint(); + mWidth = width; + mAlignment = align; + mSpacingMult = spacingmult; + mSpacingAdd = spacingadd; + mSpannedText = text instanceof Spanned; + } + + /** + * Replace constructor properties of this Layout with new ones. Be careful. + */ + /* package */ void replaceWith(CharSequence text, TextPaint paint, + int width, Alignment align, + float spacingmult, float spacingadd) { + if (width < 0) { + throw new IllegalArgumentException("Layout: " + width + " < 0"); + } + + mText = text; + mPaint = paint; + mWidth = width; + mAlignment = align; + mSpacingMult = spacingmult; + mSpacingAdd = spacingadd; + mSpannedText = text instanceof Spanned; + } + + /** + * Draw this Layout on the specified Canvas. + */ + public void draw(Canvas c) { + draw(c, null, null, 0); + } + + /** + * Draw the specified rectangle from this Layout on the specified Canvas, + * with the specified path drawn between the background and the text. + */ + public void draw(Canvas c, Path highlight, Paint highlightpaint, + int cursorOffsetVertical) { + int dtop, dbottom; + + synchronized (sTempRect) { + if (!c.getClipBounds(sTempRect)) { + return; + } + + dtop = sTempRect.top; + dbottom = sTempRect.bottom; + } + + TextPaint paint = mPaint; + + int top = 0; + // getLineBottom(getLineCount() -1) just calls getLineTop(getLineCount) + int bottom = getLineTop(getLineCount()); + + + if (dtop > top) { + top = dtop; + } + if (dbottom < bottom) { + bottom = dbottom; + } + + int first = getLineForVertical(top); + int last = getLineForVertical(bottom); + + int previousLineBottom = getLineTop(first); + int previousLineEnd = getLineStart(first); + + CharSequence buf = mText; + + ParagraphStyle[] nospans = ArrayUtils.emptyArray(ParagraphStyle.class); + ParagraphStyle[] spans = nospans; + int spanend = 0; + int textLength = 0; + boolean spannedText = mSpannedText; + + if (spannedText) { + spanend = 0; + textLength = buf.length(); + for (int i = first; i <= last; i++) { + int start = previousLineEnd; + int end = getLineStart(i+1); + previousLineEnd = end; + + int ltop = previousLineBottom; + int lbottom = getLineTop(i+1); + previousLineBottom = lbottom; + int lbaseline = lbottom - getLineDescent(i); + + if (start >= spanend) { + Spanned sp = (Spanned) buf; + spanend = sp.nextSpanTransition(start, textLength, + LineBackgroundSpan.class); + spans = sp.getSpans(start, spanend, + LineBackgroundSpan.class); + } + + for (int n = 0; n < spans.length; n++) { + LineBackgroundSpan back = (LineBackgroundSpan) spans[n]; + + back.drawBackground(c, paint, 0, mWidth, + ltop, lbaseline, lbottom, + buf, start, end, + i); + } + } + // reset to their original values + spanend = 0; + previousLineBottom = getLineTop(first); + previousLineEnd = getLineStart(first); + spans = nospans; + } + + // There can be a highlight even without spans if we are drawing + // a non-spanned transformation of a spanned editing buffer. + if (highlight != null) { + if (cursorOffsetVertical != 0) { + c.translate(0, cursorOffsetVertical); + } + + c.drawPath(highlight, highlightpaint); + + if (cursorOffsetVertical != 0) { + c.translate(0, -cursorOffsetVertical); + } + } + + Alignment align = mAlignment; + + for (int i = first; i <= last; i++) { + int start = previousLineEnd; + + previousLineEnd = getLineStart(i+1); + int end = getLineVisibleEnd(i, start, previousLineEnd); + + int ltop = previousLineBottom; + int lbottom = getLineTop(i+1); + previousLineBottom = lbottom; + int lbaseline = lbottom - getLineDescent(i); + + boolean par = false; + if (spannedText) { + if (start == 0 || buf.charAt(start - 1) == '\n') { + par = true; + } + if (start >= spanend) { + + Spanned sp = (Spanned) buf; + + spanend = sp.nextSpanTransition(start, textLength, + ParagraphStyle.class); + spans = sp.getSpans(start, spanend, ParagraphStyle.class); + + align = mAlignment; + + for (int n = spans.length-1; n >= 0; n--) { + if (spans[n] instanceof AlignmentSpan) { + align = ((AlignmentSpan) spans[n]).getAlignment(); + break; + } + } + } + } + + int dir = getParagraphDirection(i); + int left = 0; + int right = mWidth; + + if (spannedText) { + final int length = spans.length; + for (int n = 0; n < length; n++) { + if (spans[n] instanceof LeadingMarginSpan) { + LeadingMarginSpan margin = (LeadingMarginSpan) spans[n]; + + if (dir == DIR_RIGHT_TO_LEFT) { + margin.drawLeadingMargin(c, paint, right, dir, ltop, + lbaseline, lbottom, buf, + start, end, par, this); + + right -= margin.getLeadingMargin(par); + } else { + margin.drawLeadingMargin(c, paint, left, dir, ltop, + lbaseline, lbottom, buf, + start, end, par, this); + + left += margin.getLeadingMargin(par); + } + } + } + } + + int x; + if (align == Alignment.ALIGN_NORMAL) { + if (dir == DIR_LEFT_TO_RIGHT) { + x = left; + } else { + x = right; + } + } else { + int max = (int)getLineMax(i, spans, false); + if (align == Alignment.ALIGN_OPPOSITE) { + if (dir == DIR_RIGHT_TO_LEFT) { + x = left + max; + } else { + x = right - max; + } + } else { + // Alignment.ALIGN_CENTER + max = max & ~1; + int half = (right - left - max) >> 1; + if (dir == DIR_RIGHT_TO_LEFT) { + x = right - half; + } else { + x = left + half; + } + } + } + + Directions directions = getLineDirections(i); + boolean hasTab = getLineContainsTab(i); + if (directions == DIRS_ALL_LEFT_TO_RIGHT && + !spannedText && !hasTab) { + if (Config.DEBUG) { + Assert.assertTrue(dir == DIR_LEFT_TO_RIGHT); + Assert.assertNotNull(c); + } + c.drawText(buf, start, end, x, lbaseline, paint); + } else { + drawText(c, buf, start, end, dir, directions, + x, ltop, lbaseline, lbottom, paint, mWorkPaint, + hasTab, spans); + } + } + } + + /** + * Return the text that is displayed by this Layout. + */ + public final CharSequence getText() { + return mText; + } + + /** + * Return the base Paint properties for this layout. + * Do NOT change the paint, which may result in funny + * drawing for this layout. + */ + public final TextPaint getPaint() { + return mPaint; + } + + /** + * Return the width of this layout. + */ + public final int getWidth() { + return mWidth; + } + + /** + * Return the width to which this Layout is ellipsizing, or + * {@link #getWidth} if it is not doing anything special. + */ + public int getEllipsizedWidth() { + return mWidth; + } + + /** + * Increase the width of this layout to the specified width. + * Be careful to use this only when you know it is appropriate -- + * it does not cause the text to reflow to use the full new width. + */ + public final void increaseWidthTo(int wid) { + if (wid < mWidth) { + throw new RuntimeException("attempted to reduce Layout width"); + } + + mWidth = wid; + } + + /** + * Return the total height of this layout. + */ + public int getHeight() { + return getLineTop(getLineCount()); // same as getLineBottom(getLineCount() - 1); + } + + /** + * Return the base alignment of this layout. + */ + public final Alignment getAlignment() { + return mAlignment; + } + + /** + * Return what the text height is multiplied by to get the line height. + */ + public final float getSpacingMultiplier() { + return mSpacingMult; + } + + /** + * Return the number of units of leading that are added to each line. + */ + public final float getSpacingAdd() { + return mSpacingAdd; + } + + /** + * Return the number of lines of text in this layout. + */ + public abstract int getLineCount(); + + /** + * Return the baseline for the specified line (0…getLineCount() - 1) + * If bounds is not null, return the top, left, right, bottom extents + * of the specified line in it. + * @param line which line to examine (0..getLineCount() - 1) + * @param bounds Optional. If not null, it returns the extent of the line + * @return the Y-coordinate of the baseline + */ + public int getLineBounds(int line, Rect bounds) { + if (bounds != null) { + bounds.left = 0; // ??? + bounds.top = getLineTop(line); + bounds.right = mWidth; // ??? + bounds.bottom = getLineBottom(line); + } + return getLineBaseline(line); + } + + /** + * Return the vertical position of the top of the specified line. + * If the specified line is one beyond the last line, returns the + * bottom of the last line. + */ + public abstract int getLineTop(int line); + + /** + * Return the descent of the specified line. + */ + public abstract int getLineDescent(int line); + + /** + * Return the text offset of the beginning of the specified line. + * If the specified line is one beyond the last line, returns the + * end of the last line. + */ + public abstract int getLineStart(int line); + + /** + * Returns the primary directionality of the paragraph containing + * the specified line. + */ + public abstract int getParagraphDirection(int line); + + /** + * Returns whether the specified line contains one or more tabs. + */ + public abstract boolean getLineContainsTab(int line); + + /** + * Returns an array of directionalities for the specified line. + * The array alternates counts of characters in left-to-right + * and right-to-left segments of the line. + */ + public abstract Directions getLineDirections(int line); + + /** + * Returns the (negative) number of extra pixels of ascent padding in the + * top line of the Layout. + */ + public abstract int getTopPadding(); + + /** + * Returns the number of extra pixels of descent padding in the + * bottom line of the Layout. + */ + public abstract int getBottomPadding(); + + /** + * Get the primary horizontal position for the specified text offset. + * This is the location where a new character would be inserted in + * the paragraph's primary direction. + */ + public float getPrimaryHorizontal(int offset) { + return getHorizontal(offset, false, true); + } + + /** + * Get the secondary horizontal position for the specified text offset. + * This is the location where a new character would be inserted in + * the direction other than the paragraph's primary direction. + */ + public float getSecondaryHorizontal(int offset) { + return getHorizontal(offset, true, true); + } + + private float getHorizontal(int offset, boolean trailing, boolean alt) { + int line = getLineForOffset(offset); + + return getHorizontal(offset, trailing, alt, line); + } + + private float getHorizontal(int offset, boolean trailing, boolean alt, + int line) { + int start = getLineStart(line); + int end = getLineVisibleEnd(line); + int dir = getParagraphDirection(line); + boolean tab = getLineContainsTab(line); + Directions directions = getLineDirections(line); + + TabStopSpan[] tabs = null; + if (tab && mText instanceof Spanned) { + tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); + } + + float wid = measureText(mPaint, mWorkPaint, mText, start, offset, end, + dir, directions, trailing, alt, tab, tabs); + + if (offset > end) { + if (dir == DIR_RIGHT_TO_LEFT) + wid -= measureText(mPaint, mWorkPaint, + mText, end, offset, null, tab, tabs); + else + wid += measureText(mPaint, mWorkPaint, + mText, end, offset, null, tab, tabs); + } + + Alignment align = getParagraphAlignment(line); + int left = getParagraphLeft(line); + int right = getParagraphRight(line); + + if (align == Alignment.ALIGN_NORMAL) { + if (dir == DIR_RIGHT_TO_LEFT) + return right + wid; + else + return left + wid; + } + + float max = getLineMax(line); + + if (align == Alignment.ALIGN_OPPOSITE) { + if (dir == DIR_RIGHT_TO_LEFT) + return left + max + wid; + else + return right - max + wid; + } else { /* align == Alignment.ALIGN_CENTER */ + int imax = ((int) max) & ~1; + + if (dir == DIR_RIGHT_TO_LEFT) + return right - (((right - left) - imax) / 2) + wid; + else + return left + ((right - left) - imax) / 2 + wid; + } + } + + /** + * Get the leftmost position that should be exposed for horizontal + * scrolling on the specified line. + */ + public float getLineLeft(int line) { + int dir = getParagraphDirection(line); + Alignment align = getParagraphAlignment(line); + + if (align == Alignment.ALIGN_NORMAL) { + if (dir == DIR_RIGHT_TO_LEFT) + return getParagraphRight(line) - getLineMax(line); + else + return 0; + } else if (align == Alignment.ALIGN_OPPOSITE) { + if (dir == DIR_RIGHT_TO_LEFT) + return 0; + else + return mWidth - getLineMax(line); + } else { /* align == Alignment.ALIGN_CENTER */ + int left = getParagraphLeft(line); + int right = getParagraphRight(line); + int max = ((int) getLineMax(line)) & ~1; + + return left + ((right - left) - max) / 2; + } + } + + /** + * Get the rightmost position that should be exposed for horizontal + * scrolling on the specified line. + */ + public float getLineRight(int line) { + int dir = getParagraphDirection(line); + Alignment align = getParagraphAlignment(line); + + if (align == Alignment.ALIGN_NORMAL) { + if (dir == DIR_RIGHT_TO_LEFT) + return mWidth; + else + return getParagraphLeft(line) + getLineMax(line); + } else if (align == Alignment.ALIGN_OPPOSITE) { + if (dir == DIR_RIGHT_TO_LEFT) + return getLineMax(line); + else + return mWidth; + } else { /* align == Alignment.ALIGN_CENTER */ + int left = getParagraphLeft(line); + int right = getParagraphRight(line); + int max = ((int) getLineMax(line)) & ~1; + + return right - ((right - left) - max) / 2; + } + } + + /** + * Gets the horizontal extent of the specified line, excluding + * trailing whitespace. + */ + public float getLineMax(int line) { + return getLineMax(line, null, false); + } + + /** + * Gets the horizontal extent of the specified line, including + * trailing whitespace. + */ + public float getLineWidth(int line) { + return getLineMax(line, null, true); + } + + private float getLineMax(int line, Object[] tabs, boolean full) { + int start = getLineStart(line); + int end; + + if (full) { + end = getLineEnd(line); + } else { + end = getLineVisibleEnd(line); + } + boolean tab = getLineContainsTab(line); + + if (tabs == null && tab && mText instanceof Spanned) { + tabs = ((Spanned) mText).getSpans(start, end, TabStopSpan.class); + } + + return measureText(mPaint, mWorkPaint, + mText, start, end, null, tab, tabs); + } + + /** + * Get the line number corresponding to the specified vertical position. + * If you ask for a position above 0, you get 0; if you ask for a position + * below the bottom of the text, you get the last line. + */ + // FIXME: It may be faster to do a linear search for layouts without many lines. + public int getLineForVertical(int vertical) { + int high = getLineCount(), low = -1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + + if (getLineTop(guess) > vertical) + high = guess; + else + low = guess; + } + + if (low < 0) + return 0; + else + return low; + } + + /** + * Get the line number on which the specified text offset appears. + * If you ask for a position before 0, you get 0; if you ask for a position + * beyond the end of the text, you get the last line. + */ + public int getLineForOffset(int offset) { + int high = getLineCount(), low = -1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + + if (getLineStart(guess) > offset) + high = guess; + else + low = guess; + } + + if (low < 0) + return 0; + else + return low; + } + + /** + * Get the character offset on the specfied line whose position is + * closest to the specified horizontal position. + */ + public int getOffsetForHorizontal(int line, float horiz) { + int max = getLineEnd(line) - 1; + int min = getLineStart(line); + Directions dirs = getLineDirections(line); + + if (line == getLineCount() - 1) + max++; + + int best = min; + float bestdist = Math.abs(getPrimaryHorizontal(best) - horiz); + + int here = min; + for (int i = 0; i < dirs.mDirections.length; i++) { + int there = here + dirs.mDirections[i]; + int swap = ((i & 1) == 0) ? 1 : -1; + + if (there > max) + there = max; + + int high = there - 1 + 1, low = here + 1 - 1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + int adguess = getOffsetAtStartOf(guess); + + if (getPrimaryHorizontal(adguess) * swap >= horiz * swap) + high = guess; + else + low = guess; + } + + if (low < here + 1) + low = here + 1; + + if (low < there) { + low = getOffsetAtStartOf(low); + + float dist = Math.abs(getPrimaryHorizontal(low) - horiz); + + int aft = TextUtils.getOffsetAfter(mText, low); + if (aft < there) { + float other = Math.abs(getPrimaryHorizontal(aft) - horiz); + + if (other < dist) { + dist = other; + low = aft; + } + } + + if (dist < bestdist) { + bestdist = dist; + best = low; + } + } + + float dist = Math.abs(getPrimaryHorizontal(here) - horiz); + + if (dist < bestdist) { + bestdist = dist; + best = here; + } + + here = there; + } + + float dist = Math.abs(getPrimaryHorizontal(max) - horiz); + + if (dist < bestdist) { + bestdist = dist; + best = max; + } + + return best; + } + + /** + * Return the text offset after the last character on the specified line. + */ + public final int getLineEnd(int line) { + return getLineStart(line + 1); + } + + /** + * Return the text offset after the last visible character (so whitespace + * is not counted) on the specified line. + */ + public int getLineVisibleEnd(int line) { + return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1)); + } + + private int getLineVisibleEnd(int line, int start, int end) { + if (Config.DEBUG) { + Assert.assertTrue(getLineStart(line) == start && getLineStart(line+1) == end); + } + + CharSequence text = mText; + char ch; + if (line == getLineCount() - 1) { + return end; + } + + for (; end > start; end--) { + ch = text.charAt(end - 1); + + if (ch == '\n') { + return end - 1; + } + + if (ch != ' ' && ch != '\t') { + break; + } + + } + + return end; + } + + /** + * Return the vertical position of the bottom of the specified line. + */ + public final int getLineBottom(int line) { + return getLineTop(line + 1); + } + + /** + * Return the vertical position of the baseline of the specified line. + */ + public final int getLineBaseline(int line) { + // getLineTop(line+1) == getLineTop(line) + return getLineTop(line+1) - getLineDescent(line); + } + + /** + * Get the ascent of the text on the specified line. + * The return value is negative to match the Paint.ascent() convention. + */ + public final int getLineAscent(int line) { + // getLineTop(line+1) - getLineDescent(line) == getLineBaseLine(line) + return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line)); + } + + /** + * Return the text offset that would be reached by moving left + * (possibly onto another line) from the specified offset. + */ + public int getOffsetToLeftOf(int offset) { + int line = getLineForOffset(offset); + int start = getLineStart(line); + int end = getLineEnd(line); + Directions dirs = getLineDirections(line); + + if (line != getLineCount() - 1) + end--; + + float horiz = getPrimaryHorizontal(offset); + + int best = offset; + float besth = Integer.MIN_VALUE; + int candidate; + + candidate = TextUtils.getOffsetBefore(mText, offset); + if (candidate >= start && candidate <= end) { + float h = getPrimaryHorizontal(candidate); + + if (h < horiz && h > besth) { + best = candidate; + besth = h; + } + } + + candidate = TextUtils.getOffsetAfter(mText, offset); + if (candidate >= start && candidate <= end) { + float h = getPrimaryHorizontal(candidate); + + if (h < horiz && h > besth) { + best = candidate; + besth = h; + } + } + + int here = start; + for (int i = 0; i < dirs.mDirections.length; i++) { + int there = here + dirs.mDirections[i]; + if (there > end) + there = end; + + float h = getPrimaryHorizontal(here); + + if (h < horiz && h > besth) { + best = here; + besth = h; + } + + candidate = TextUtils.getOffsetAfter(mText, here); + if (candidate >= start && candidate <= end) { + h = getPrimaryHorizontal(candidate); + + if (h < horiz && h > besth) { + best = candidate; + besth = h; + } + } + + candidate = TextUtils.getOffsetBefore(mText, there); + if (candidate >= start && candidate <= end) { + h = getPrimaryHorizontal(candidate); + + if (h < horiz && h > besth) { + best = candidate; + besth = h; + } + } + + here = there; + } + + float h = getPrimaryHorizontal(end); + + if (h < horiz && h > besth) { + best = end; + besth = h; + } + + if (best != offset) + return best; + + int dir = getParagraphDirection(line); + + if (dir > 0) { + if (line == 0) + return best; + else + return getOffsetForHorizontal(line - 1, 10000); + } else { + if (line == getLineCount() - 1) + return best; + else + return getOffsetForHorizontal(line + 1, 10000); + } + } + + /** + * Return the text offset that would be reached by moving right + * (possibly onto another line) from the specified offset. + */ + public int getOffsetToRightOf(int offset) { + int line = getLineForOffset(offset); + int start = getLineStart(line); + int end = getLineEnd(line); + Directions dirs = getLineDirections(line); + + if (line != getLineCount() - 1) + end--; + + float horiz = getPrimaryHorizontal(offset); + + int best = offset; + float besth = Integer.MAX_VALUE; + int candidate; + + candidate = TextUtils.getOffsetBefore(mText, offset); + if (candidate >= start && candidate <= end) { + float h = getPrimaryHorizontal(candidate); + + if (h > horiz && h < besth) { + best = candidate; + besth = h; + } + } + + candidate = TextUtils.getOffsetAfter(mText, offset); + if (candidate >= start && candidate <= end) { + float h = getPrimaryHorizontal(candidate); + + if (h > horiz && h < besth) { + best = candidate; + besth = h; + } + } + + int here = start; + for (int i = 0; i < dirs.mDirections.length; i++) { + int there = here + dirs.mDirections[i]; + if (there > end) + there = end; + + float h = getPrimaryHorizontal(here); + + if (h > horiz && h < besth) { + best = here; + besth = h; + } + + candidate = TextUtils.getOffsetAfter(mText, here); + if (candidate >= start && candidate <= end) { + h = getPrimaryHorizontal(candidate); + + if (h > horiz && h < besth) { + best = candidate; + besth = h; + } + } + + candidate = TextUtils.getOffsetBefore(mText, there); + if (candidate >= start && candidate <= end) { + h = getPrimaryHorizontal(candidate); + + if (h > horiz && h < besth) { + best = candidate; + besth = h; + } + } + + here = there; + } + + float h = getPrimaryHorizontal(end); + + if (h > horiz && h < besth) { + best = end; + besth = h; + } + + if (best != offset) + return best; + + int dir = getParagraphDirection(line); + + if (dir > 0) { + if (line == getLineCount() - 1) + return best; + else + return getOffsetForHorizontal(line + 1, -10000); + } else { + if (line == 0) + return best; + else + return getOffsetForHorizontal(line - 1, -10000); + } + } + + private int getOffsetAtStartOf(int offset) { + if (offset == 0) + return 0; + + CharSequence text = mText; + char c = text.charAt(offset); + + if (c >= '\uDC00' && c <= '\uDFFF') { + char c1 = text.charAt(offset - 1); + + if (c1 >= '\uD800' && c1 <= '\uDBFF') + offset -= 1; + } + + if (mSpannedText) { + ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, + ReplacementSpan.class); + + for (int i = 0; i < spans.length; i++) { + int start = ((Spanned) text).getSpanStart(spans[i]); + int end = ((Spanned) text).getSpanEnd(spans[i]); + + if (start < offset && end > offset) + offset = start; + } + } + + return offset; + } + + /** + * Fills in the specified Path with a representation of a cursor + * at the specified offset. This will often be a vertical line + * but can be multiple discontinous lines in text with multiple + * directionalities. + */ + public void getCursorPath(int point, Path dest, + CharSequence editingBuffer) { + dest.reset(); + + int line = getLineForOffset(point); + int top = getLineTop(line); + int bottom = getLineTop(line+1); + + float h1 = getPrimaryHorizontal(point) - 0.5f; + float h2 = getSecondaryHorizontal(point) - 0.5f; + + int caps = TextKeyListener.getMetaState(editingBuffer, + KeyEvent.META_SHIFT_ON) | + TextKeyListener.getMetaState(editingBuffer, + TextKeyListener.META_SELECTING); + int fn = TextKeyListener.getMetaState(editingBuffer, + KeyEvent.META_ALT_ON); + int dist = 0; + + if (caps != 0 || fn != 0) { + dist = (bottom - top) >> 2; + + if (fn != 0) + top += dist; + if (caps != 0) + bottom -= dist; + } + + if (h1 < 0.5f) + h1 = 0.5f; + if (h2 < 0.5f) + h2 = 0.5f; + + if (h1 == h2) { + dest.moveTo(h1, top); + dest.lineTo(h1, bottom); + } else { + dest.moveTo(h1, top); + dest.lineTo(h1, (top + bottom) >> 1); + + dest.moveTo(h2, (top + bottom) >> 1); + dest.lineTo(h2, bottom); + } + + if (caps == 2) { + dest.moveTo(h2, bottom); + dest.lineTo(h2 - dist, bottom + dist); + dest.lineTo(h2, bottom); + dest.lineTo(h2 + dist, bottom + dist); + } else if (caps == 1) { + dest.moveTo(h2, bottom); + dest.lineTo(h2 - dist, bottom + dist); + + dest.moveTo(h2 - dist, bottom + dist - 0.5f); + dest.lineTo(h2 + dist, bottom + dist - 0.5f); + + dest.moveTo(h2 + dist, bottom + dist); + dest.lineTo(h2, bottom); + } + + if (fn == 2) { + dest.moveTo(h1, top); + dest.lineTo(h1 - dist, top - dist); + dest.lineTo(h1, top); + dest.lineTo(h1 + dist, top - dist); + } else if (fn == 1) { + dest.moveTo(h1, top); + dest.lineTo(h1 - dist, top - dist); + + dest.moveTo(h1 - dist, top - dist + 0.5f); + dest.lineTo(h1 + dist, top - dist + 0.5f); + + dest.moveTo(h1 + dist, top - dist); + dest.lineTo(h1, top); + } + } + + private void addSelection(int line, int start, int end, + int top, int bottom, Path dest) { + int linestart = getLineStart(line); + int lineend = getLineEnd(line); + Directions dirs = getLineDirections(line); + + if (lineend > linestart && mText.charAt(lineend - 1) == '\n') + lineend--; + + int here = linestart; + for (int i = 0; i < dirs.mDirections.length; i++) { + int there = here + dirs.mDirections[i]; + if (there > lineend) + there = lineend; + + if (start <= there && end >= here) { + int st = Math.max(start, here); + int en = Math.min(end, there); + + if (st != en) { + float h1 = getHorizontal(st, false, false, line); + float h2 = getHorizontal(en, true, false, line); + + dest.addRect(h1, top, h2, bottom, Path.Direction.CW); + } + } + + here = there; + } + } + + /** + * Fills in the specified Path with a representation of a highlight + * between the specified offsets. This will often be a rectangle + * or a potentially discontinuous set of rectangles. If the start + * and end are the same, the returned path is empty. + */ + public void getSelectionPath(int start, int end, Path dest) { + dest.reset(); + + if (start == end) + return; + + if (end < start) { + int temp = end; + end = start; + start = temp; + } + + int startline = getLineForOffset(start); + int endline = getLineForOffset(end); + + int top = getLineTop(startline); + int bottom = getLineBottom(endline); + + if (startline == endline) { + addSelection(startline, start, end, top, bottom, dest); + } else { + final float width = mWidth; + + addSelection(startline, start, getLineEnd(startline), + top, getLineBottom(startline), dest); + + if (getParagraphDirection(startline) == DIR_RIGHT_TO_LEFT) + dest.addRect(getLineLeft(startline), top, + 0, getLineBottom(startline), Path.Direction.CW); + else + dest.addRect(getLineRight(startline), top, + width, getLineBottom(startline), Path.Direction.CW); + + for (int i = startline + 1; i < endline; i++) { + top = getLineTop(i); + bottom = getLineBottom(i); + dest.addRect(0, top, width, bottom, Path.Direction.CW); + } + + top = getLineTop(endline); + bottom = getLineBottom(endline); + + addSelection(endline, getLineStart(endline), end, + top, bottom, dest); + + if (getParagraphDirection(endline) == DIR_RIGHT_TO_LEFT) + dest.addRect(width, top, getLineRight(endline), bottom, Path.Direction.CW); + else + dest.addRect(0, top, getLineLeft(endline), bottom, Path.Direction.CW); + } + } + + /** + * Get the alignment of the specified paragraph, taking into account + * markup attached to it. + */ + public final Alignment getParagraphAlignment(int line) { + Alignment align = mAlignment; + + if (mSpannedText) { + Spanned sp = (Spanned) mText; + AlignmentSpan[] spans = sp.getSpans(getLineStart(line), + getLineEnd(line), + AlignmentSpan.class); + + int spanLength = spans.length; + if (spanLength > 0) { + align = spans[spanLength-1].getAlignment(); + } + } + + return align; + } + + /** + * Get the left edge of the specified paragraph, inset by left margins. + */ + public final int getParagraphLeft(int line) { + int dir = getParagraphDirection(line); + + int left = 0; + + boolean par = false; + int off = getLineStart(line); + if (off == 0 || mText.charAt(off - 1) == '\n') + par = true; + + if (dir == DIR_LEFT_TO_RIGHT) { + if (mSpannedText) { + Spanned sp = (Spanned) mText; + LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), + getLineEnd(line), + LeadingMarginSpan.class); + + for (int i = 0; i < spans.length; i++) { + left += spans[i].getLeadingMargin(par); + } + } + } + + return left; + } + + /** + * Get the right edge of the specified paragraph, inset by right margins. + */ + public final int getParagraphRight(int line) { + int dir = getParagraphDirection(line); + + int right = mWidth; + + boolean par = false; + int off = getLineStart(line); + if (off == 0 || mText.charAt(off - 1) == '\n') + par = true; + + + if (dir == DIR_RIGHT_TO_LEFT) { + if (mSpannedText) { + Spanned sp = (Spanned) mText; + LeadingMarginSpan[] spans = sp.getSpans(getLineStart(line), + getLineEnd(line), + LeadingMarginSpan.class); + + for (int i = 0; i < spans.length; i++) { + right -= spans[i].getLeadingMargin(par); + } + } + } + + return right; + } + + private static void drawText(Canvas canvas, + CharSequence text, int start, int end, + int dir, Directions directions, + float x, int top, int y, int bottom, + TextPaint paint, + TextPaint workPaint, + boolean hasTabs, Object[] parspans) { + char[] buf; + if (!hasTabs) { + if (directions == DIRS_ALL_LEFT_TO_RIGHT) { + if (Config.DEBUG) { + Assert.assertTrue(DIR_LEFT_TO_RIGHT == dir); + } + Styled.drawText(canvas, text, start, end, dir, false, x, top, y, bottom, paint, workPaint, false); + return; + } + buf = null; + } else { + buf = TextUtils.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + } + + float h = 0; + + int here = 0; + for (int i = 0; i < directions.mDirections.length; i++) { + int there = here + directions.mDirections[i]; + if (there > end - start) + there = end - start; + + int segstart = here; + for (int j = hasTabs ? here : there; j <= there; j++) { + if (j == there || buf[j] == '\t') { + h += Styled.drawText(canvas, text, + start + segstart, start + j, + dir, (i & 1) != 0, x + h, + top, y, bottom, paint, workPaint, + start + j != end); + + if (j != there && buf[j] == '\t') + h = dir * nextTab(text, start, end, h * dir, parspans); + + segstart = j + 1; + } + } + + here = there; + } + + if (hasTabs) + TextUtils.recycle(buf); + } + + private static float measureText(TextPaint paint, + TextPaint workPaint, + CharSequence text, + int start, int offset, int end, + int dir, Directions directions, + boolean trailing, boolean alt, + boolean hasTabs, Object[] tabs) { + char[] buf = null; + + if (hasTabs) { + buf = TextUtils.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + } + + float h = 0; + + if (alt) { + if (dir == DIR_RIGHT_TO_LEFT) + trailing = !trailing; + } + + int here = 0; + for (int i = 0; i < directions.mDirections.length; i++) { + if (alt) + trailing = !trailing; + + int there = here + directions.mDirections[i]; + if (there > end - start) + there = end - start; + + int segstart = here; + for (int j = hasTabs ? here : there; j <= there; j++) { + if (j == there || buf[j] == '\t') { + float segw; + + if (offset < start + j || + (trailing && offset <= start + j)) { + if (dir == DIR_LEFT_TO_RIGHT && (i & 1) == 0) { + h += Styled.measureText(paint, workPaint, text, + start + segstart, offset, + null); + return h; + } + + if (dir == DIR_RIGHT_TO_LEFT && (i & 1) != 0) { + h -= Styled.measureText(paint, workPaint, text, + start + segstart, offset, + null); + return h; + } + } + + segw = Styled.measureText(paint, workPaint, text, + start + segstart, start + j, + null); + + if (offset < start + j || + (trailing && offset <= start + j)) { + if (dir == DIR_LEFT_TO_RIGHT) { + h += segw - Styled.measureText(paint, workPaint, + text, + start + segstart, + offset, null); + return h; + } + + if (dir == DIR_RIGHT_TO_LEFT) { + h -= segw - Styled.measureText(paint, workPaint, + text, + start + segstart, + offset, null); + return h; + } + } + + if (dir == DIR_RIGHT_TO_LEFT) + h -= segw; + else + h += segw; + + if (j != there && buf[j] == '\t') { + if (offset == start + j) + return h; + + h = dir * nextTab(text, start, end, h * dir, tabs); + } + + segstart = j + 1; + } + } + + here = there; + } + + if (hasTabs) + TextUtils.recycle(buf); + + return h; + } + + /* package */ static float measureText(TextPaint paint, + TextPaint workPaint, + CharSequence text, + int start, int end, + Paint.FontMetricsInt fm, + boolean hasTabs, Object[] tabs) { + char[] buf = null; + + if (hasTabs) { + buf = TextUtils.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + } + + int len = end - start; + + int here = 0; + float h = 0; + int ab = 0, be = 0; + int top = 0, bot = 0; + + if (fm != null) { + fm.ascent = 0; + fm.descent = 0; + } + + for (int i = hasTabs ? 0 : len; i <= len; i++) { + if (i == len || buf[i] == '\t') { + workPaint.baselineShift = 0; + + h += Styled.measureText(paint, workPaint, text, + start + here, start + i, + fm); + + if (fm != null) { + if (workPaint.baselineShift < 0) { + fm.ascent += workPaint.baselineShift; + fm.top += workPaint.baselineShift; + } else { + fm.descent += workPaint.baselineShift; + fm.bottom += workPaint.baselineShift; + } + } + + if (i != len) + h = nextTab(text, start, end, h, tabs); + + if (fm != null) { + if (fm.ascent < ab) { + ab = fm.ascent; + } + if (fm.descent > be) { + be = fm.descent; + } + + if (fm.top < top) { + top = fm.top; + } + if (fm.bottom > bot) { + bot = fm.bottom; + } + } + + here = i + 1; + } + } + + if (fm != null) { + fm.ascent = ab; + fm.descent = be; + fm.top = top; + fm.bottom = bot; + } + + if (hasTabs) + TextUtils.recycle(buf); + + return h; + } + + /* package */ static float nextTab(CharSequence text, int start, int end, + float h, Object[] tabs) { + float nh = Float.MAX_VALUE; + boolean alltabs = false; + + if (text instanceof Spanned) { + if (tabs == null) { + tabs = ((Spanned) text).getSpans(start, end, TabStopSpan.class); + alltabs = true; + } + + for (int i = 0; i < tabs.length; i++) { + if (!alltabs) { + if (!(tabs[i] instanceof TabStopSpan)) + continue; + } + + int where = ((TabStopSpan) tabs[i]).getTabStop(); + + if (where < nh && where > h) + nh = where; + } + + if (nh != Float.MAX_VALUE) + return nh; + } + + return ((int) ((h + TAB_INCREMENT) / TAB_INCREMENT)) * TAB_INCREMENT; + } + + protected final boolean isSpanned() { + return mSpannedText; + } + + private void ellipsize(int start, int end, int line, + char[] dest, int destoff) { + int ellipsisCount = getEllipsisCount(line); + + if (ellipsisCount == 0) { + return; + } + + int ellipsisStart = getEllipsisStart(line); + int linestart = getLineStart(line); + + for (int i = ellipsisStart; i < ellipsisStart + ellipsisCount; i++) { + char c; + + if (i == ellipsisStart) { + c = '\u2026'; // ellipsis + } else { + c = '\uFEFF'; // 0-width space + } + + int a = i + linestart; + + if (a >= start && a < end) { + dest[destoff + a - start] = c; + } + } + } + + /** + * Stores information about bidirectional (left-to-right or right-to-left) + * text within the layout of a line. TODO: This work is not complete + * or correct and will be fleshed out in a later revision. + */ + public static class Directions { + private short[] mDirections; + + /* package */ Directions(short[] dirs) { + mDirections = dirs; + } + } + + /** + * Return the offset of the first character to be ellipsized away, + * relative to the start of the line. (So 0 if the beginning of the + * line is ellipsized, not getLineStart().) + */ + public abstract int getEllipsisStart(int line); + /** + * Returns the number of characters to be ellipsized away, or 0 if + * no ellipsis is to take place. + */ + public abstract int getEllipsisCount(int line); + + /* package */ static class Ellipsizer implements CharSequence, GetChars { + /* package */ CharSequence mText; + /* package */ Layout mLayout; + /* package */ int mWidth; + /* package */ TextUtils.TruncateAt mMethod; + + public Ellipsizer(CharSequence s) { + mText = s; + } + + public char charAt(int off) { + char[] buf = TextUtils.obtain(1); + getChars(off, off + 1, buf, 0); + char ret = buf[0]; + + TextUtils.recycle(buf); + return ret; + } + + public void getChars(int start, int end, char[] dest, int destoff) { + int line1 = mLayout.getLineForOffset(start); + int line2 = mLayout.getLineForOffset(end); + + TextUtils.getChars(mText, start, end, dest, destoff); + + for (int i = line1; i <= line2; i++) { + mLayout.ellipsize(start, end, i, dest, destoff); + } + } + + public int length() { + return mText.length(); + } + + public CharSequence subSequence(int start, int end) { + char[] s = new char[end - start]; + getChars(start, end, s, 0); + return new String(s); + } + + public String toString() { + char[] s = new char[length()]; + getChars(0, length(), s, 0); + return new String(s); + } + + } + + /* package */ static class SpannedEllipsizer + extends Ellipsizer implements Spanned { + private Spanned mSpanned; + + public SpannedEllipsizer(CharSequence display) { + super(display); + mSpanned = (Spanned) display; + } + + public <T> T[] getSpans(int start, int end, Class<T> type) { + return mSpanned.getSpans(start, end, type); + } + + public int getSpanStart(Object tag) { + return mSpanned.getSpanStart(tag); + } + + public int getSpanEnd(Object tag) { + return mSpanned.getSpanEnd(tag); + } + + public int getSpanFlags(Object tag) { + return mSpanned.getSpanFlags(tag); + } + + public int nextSpanTransition(int start, int limit, Class type) { + return mSpanned.nextSpanTransition(start, limit, type); + } + + public CharSequence subSequence(int start, int end) { + char[] s = new char[end - start]; + getChars(start, end, s, 0); + + SpannableString ss = new SpannableString(new String(s)); + TextUtils.copySpansFrom(mSpanned, start, end, Object.class, ss, 0); + return ss; + } + } + + private CharSequence mText; + private TextPaint mPaint; + /* package */ TextPaint mWorkPaint; + private int mWidth; + private Alignment mAlignment = Alignment.ALIGN_NORMAL; + private float mSpacingMult; + private float mSpacingAdd; + private static Rect sTempRect = new Rect(); + private boolean mSpannedText; + + public static final int DIR_LEFT_TO_RIGHT = 1; + public static final int DIR_RIGHT_TO_LEFT = -1; + + public enum Alignment { + ALIGN_NORMAL, + ALIGN_OPPOSITE, + ALIGN_CENTER, + // XXX ALIGN_LEFT, + // XXX ALIGN_RIGHT, + } + + private static final int TAB_INCREMENT = 20; + + /* package */ static final Directions DIRS_ALL_LEFT_TO_RIGHT = + new Directions(new short[] { 32767 }); + /* package */ static final Directions DIRS_ALL_RIGHT_TO_LEFT = + new Directions(new short[] { 0, 32767 }); + +} + |
