From 54b6cfa9a9e5b861a9930af873580d6dc20f773c Mon Sep 17 00:00:00 2001
From: The Android Open Source Project This class is used by widgets to control text layout. You should not need
+ * to use this class directly unless you are implementing your own widget
+ * or custom display object, in which case
+ * you are encouraged to use a Layout instead of calling
+ * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
+ * Canvas.drawText()} directly.
+ * You do not instantiate this class directly; instead, retrieve it through
+ * {@link android.content.Context#getSystemService}.
+ *
+ * @see android.content.Context#getSystemService
+ */
+public class ClipboardManager {
+ private static IClipboard sService;
+
+ private Context mContext;
+
+ static private IClipboard getService() {
+ if (sService != null) {
+ return sService;
+ }
+ IBinder b = ServiceManager.getService("clipboard");
+ sService = IClipboard.Stub.asInterface(b);
+ return sService;
+ }
+
+ /** {@hide} */
+ public ClipboardManager(Context context, Handler handler) {
+ mContext = context;
+ }
+
+ /**
+ * Returns the text on the clipboard. It will eventually be possible
+ * to store types other than text too, in which case this will return
+ * null if the type cannot be coerced to text.
+ */
+ public CharSequence getText() {
+ try {
+ return getService().getClipboardText();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Sets the contents of the clipboard to the specified text.
+ */
+ public void setText(CharSequence text) {
+ try {
+ getService().setClipboardText(text);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Returns true if the clipboard contains text; false otherwise.
+ */
+ public boolean hasText() {
+ try {
+ return getService().hasClipboardText();
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+}
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
new file mode 100644
index 0000000..14e5655
--- /dev/null
+++ b/core/java/android/text/DynamicLayout.java
@@ -0,0 +1,503 @@
+/*
+ * 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.Paint;
+import android.text.style.UpdateLayout;
+import android.text.style.WrapTogetherSpan;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * DynamicLayout is a text layout that updates itself as the text is edited.
+ * This is used by widgets to control text layout. You should not need
+ * to use this class directly unless you are implementing your own widget
+ * or custom display object, or need to call
+ * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
+ * Canvas.drawText()} directly.
+ * Before the change is committed, each filter that was set with
+ * {@link #setFilters} is given the opportunity to modify the
+ *
+ * If This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
+ */
+ public static Spanned fromHtml(String source) {
+ return fromHtml(source, null, null);
+ }
+
+ /**
+ * Lazy initialization holder for HTML parser. This class will
+ * a) be preloaded by the zygote, or b) not loaded until absolutely
+ * necessary.
+ */
+ private static class HtmlParser {
+ private static final HTMLSchema schema = new HTMLSchema();
+ }
+
+ /**
+ * Returns displayable styled text from the provided HTML string.
+ * Any <img> tags in the HTML will use the specified ImageGetter
+ * to request a representation of the image (use null if you don't
+ * want this) and the specified TagHandler to handle unknown tags
+ * (specify null if you don't want this).
+ *
+ * This uses TagSoup to handle real HTML, including all of the brokenness found in the wild.
+ */
+ public static Spanned fromHtml(String source, ImageGetter imageGetter,
+ TagHandler tagHandler) {
+ Parser parser = new Parser();
+ try {
+ parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
+ } catch (org.xml.sax.SAXNotRecognizedException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ } catch (org.xml.sax.SAXNotSupportedException e) {
+ // Should not happen.
+ throw new RuntimeException(e);
+ }
+
+ HtmlToSpannedConverter converter =
+ new HtmlToSpannedConverter(source, imageGetter, tagHandler,
+ parser);
+ return converter.convert();
+ }
+
+ /**
+ * Returns an HTML representation of the provided Spanned text.
+ */
+ public static String toHtml(Spanned text) {
+ StringBuilder out = new StringBuilder();
+ int len = text.length();
+
+ int next;
+ for (int i = 0; i < text.length(); i = next) {
+ next = text.nextSpanTransition(i, len, QuoteSpan.class);
+ QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
+
+ for (QuoteSpan quote: quotes) {
+ out.append(" ");
+
+ int next;
+ for (int i = start; i < end; i = next) {
+ next = TextUtils.indexOf(text, '\n', i, end);
+ if (next < 0) {
+ next = end;
+ }
+
+ int nl = 0;
+
+ while (next < end && text.charAt(next) == '\n') {
+ nl++;
+ next++;
+ }
+
+ withinParagraph(out, text, i, next - nl, nl, next == end);
+ }
+
+ out.append("source
, except that the range of
+ * offsets substart
inclusive to subend
exclusive
+ * are mirrored instead from sub
, beginning at offset 0.
+ */
+ public static AlteredCharSequence make(CharSequence source, char[] sub,
+ int substart, int subend) {
+ if (source instanceof Spanned)
+ return new AlteredSpanned(source, sub, substart, subend);
+ else
+ return new AlteredCharSequence(source, sub, substart, subend);
+ }
+
+ private AlteredCharSequence(CharSequence source, char[] sub,
+ int substart, int subend) {
+ mSource = source;
+ mChars = sub;
+ mStart = substart;
+ mEnd = subend;
+ }
+
+ /* package */ void update(char[] sub, int substart, int subend) {
+ mChars = sub;
+ mStart = substart;
+ mEnd = subend;
+ }
+
+ private static class AlteredSpanned
+ extends AlteredCharSequence
+ implements Spanned
+ {
+ private AlteredSpanned(CharSequence source, char[] sub,
+ int substart, int subend) {
+ super(source, sub, substart, subend);
+ mSpanned = (Spanned) source;
+ }
+
+ public count
bytes of dest
with the
+ * directionalities from the first count
chars of src
.
+ * This is just like Character.getDirectionality() except it is a
+ * batch operation.
+ */
+ public native static void getDirectionalities(char[] src, byte[] dest,
+ int count);
+ /**
+ * Replace the specified slice of text
with the chars'
+ * right-to-left mirrors (if any), returning true if any
+ * replacements were made.
+ */
+ public native static boolean mirror(char[] text, int start, int count);
+
+ /**
+ * Return the right-to-left mirror (or the original char if none)
+ * of the specified char.
+ */
+ public native static char getMirror(char ch);
+}
diff --git a/core/java/android/text/Annotation.java b/core/java/android/text/Annotation.java
new file mode 100644
index 0000000..a3812a8
--- /dev/null
+++ b/core/java/android/text/Annotation.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2008 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;
+
+/**
+ * Annotations are simple key-value pairs that are preserved across
+ * TextView save/restore cycles and can be used to keep application-specific
+ * data that needs to be maintained for regions of text.
+ */
+public class Annotation {
+ private String mKey;
+ private String mValue;
+
+ public Annotation(String key, String value) {
+ mKey = key;
+ mValue = value;
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+}
diff --git a/core/java/android/text/AutoText.java b/core/java/android/text/AutoText.java
new file mode 100644
index 0000000..508d740
--- /dev/null
+++ b/core/java/android/text/AutoText.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2007 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.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import com.android.internal.util.XmlUtils;
+import android.view.View;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * This class accesses a dictionary of corrections to frequent misspellings.
+ */
+public class AutoText {
+ // struct trie {
+ // char c;
+ // int off;
+ // struct trie *child;
+ // struct trie *next;
+ // };
+
+ private static final int TRIE_C = 0;
+ private static final int TRIE_OFF = 1;
+ private static final int TRIE_CHILD = 2;
+ private static final int TRIE_NEXT = 3;
+
+ private static final int TRIE_SIZEOF = 4;
+ private static final char TRIE_NULL = (char) -1;
+ private static final int TRIE_ROOT = 0;
+
+ private static final int INCREMENT = 1024;
+
+ private static final int DEFAULT = 14337; // Size of the Trie 13 Aug 2007
+
+ private static final int RIGHT = 9300; // Size of 'right' 13 Aug 2007
+
+ private static AutoText sInstance = new AutoText(Resources.getSystem());
+ private static Object sLock = new Object();
+
+ // TODO:
+ //
+ // Note the assumption that the destination strings total less than
+ // 64K characters and that the trie for the source side totals less
+ // than 64K chars/offsets/child pointers/next pointers.
+ //
+ // This seems very safe for English (currently 7K of destination,
+ // 14K of trie) but may need to be revisited.
+
+ private char[] mTrie;
+ private char mTrieUsed;
+ private String mText;
+ private Locale mLocale;
+
+ private AutoText(Resources resources) {
+ mLocale = resources.getConfiguration().locale;
+ init(resources);
+ }
+
+ /**
+ * Retrieves a possible spelling correction for the specified range
+ * of text. Returns null if no correction can be found.
+ * The View is used to get the current Locale and Resources.
+ */
+ public static String get(CharSequence src, final int start, final int end,
+ View view) {
+ Resources res = view.getContext().getResources();
+ Locale locale = res.getConfiguration().locale;
+ AutoText instance;
+
+ synchronized (sLock) {
+ instance = sInstance;
+
+ if (!locale.equals(instance.mLocale)) {
+ instance = new AutoText(res);
+ sInstance = instance;
+ }
+ }
+
+ return instance.lookup(src, start, end);
+ }
+
+ private String lookup(CharSequence src, final int start, final int end) {
+ int here = mTrie[TRIE_ROOT];
+
+ for (int i = start; i < end; i++) {
+ char c = src.charAt(i);
+
+ for (; here != TRIE_NULL; here = mTrie[here + TRIE_NEXT]) {
+ if (c == mTrie[here + TRIE_C]) {
+ if ((i == end - 1)
+ && (mTrie[here + TRIE_OFF] != TRIE_NULL)) {
+ int off = mTrie[here + TRIE_OFF];
+ int len = mText.charAt(off);
+
+ return mText.substring(off + 1, off + 1 + len);
+ }
+
+ here = mTrie[here + TRIE_CHILD];
+ break;
+ }
+ }
+
+ if (here == TRIE_NULL) {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ private void init(Resources r) {
+ XmlResourceParser parser = r.getXml(com.android.internal.R.xml.autotext);
+
+ StringBuilder right = new StringBuilder(RIGHT);
+ mTrie = new char[DEFAULT];
+ mTrie[TRIE_ROOT] = TRIE_NULL;
+ mTrieUsed = TRIE_ROOT + 1;
+
+ try {
+ XmlUtils.beginDocument(parser, "words");
+ String odest = "";
+ char ooff = 0;
+
+ while (true) {
+ XmlUtils.nextElement(parser);
+
+ String element = parser.getName();
+ if (element == null || !(element.equals("word"))) {
+ break;
+ }
+
+ String src = parser.getAttributeValue(null, "src");
+ if (parser.next() == XmlPullParser.TEXT) {
+ String dest = parser.getText();
+ char off;
+
+ if (dest.equals(odest)) {
+ off = ooff;
+ } else {
+ off = (char) right.length();
+ right.append((char) dest.length());
+ right.append(dest);
+ }
+
+ add(src, off);
+ }
+ }
+
+ // Don't let Resources cache a copy of all these strings.
+ r.flushLayoutCache();
+ } catch (XmlPullParserException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ parser.close();
+ }
+
+ mText = right.toString();
+ }
+
+ private void add(String src, char off) {
+ int slen = src.length();
+ int herep = TRIE_ROOT;
+
+ for (int i = 0; i < slen; i++) {
+ char c = src.charAt(i);
+ boolean found = false;
+
+ for (; mTrie[herep] != TRIE_NULL;
+ herep = mTrie[herep] + TRIE_NEXT) {
+ if (c == mTrie[mTrie[herep] + TRIE_C]) {
+ // There is a node for this letter, and this is the
+ // end, so fill in the right hand side fields.
+
+ if (i == slen - 1) {
+ mTrie[mTrie[herep] + TRIE_OFF] = off;
+ return;
+ }
+
+ // There is a node for this letter, and we need
+ // to go deeper into it to fill in the rest.
+
+ herep = mTrie[herep] + TRIE_CHILD;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ // No node for this letter yet. Make one.
+
+ char node = newTrieNode();
+ mTrie[herep] = node;
+
+ mTrie[mTrie[herep] + TRIE_C] = c;
+ mTrie[mTrie[herep] + TRIE_OFF] = TRIE_NULL;
+ mTrie[mTrie[herep] + TRIE_NEXT] = TRIE_NULL;
+ mTrie[mTrie[herep] + TRIE_CHILD] = TRIE_NULL;
+
+ // If this is the end of the word, fill in the offset.
+
+ if (i == slen - 1) {
+ mTrie[mTrie[herep] + TRIE_OFF] = off;
+ return;
+ }
+
+ // Otherwise, step in deeper and go to the next letter.
+
+ herep = mTrie[herep] + TRIE_CHILD;
+ }
+ }
+ }
+
+ private char newTrieNode() {
+ if (mTrieUsed + TRIE_SIZEOF > mTrie.length) {
+ char[] copy = new char[mTrie.length + INCREMENT];
+ System.arraycopy(mTrie, 0, copy, 0, mTrie.length);
+ mTrie = copy;
+ }
+
+ char ret = mTrieUsed;
+ mTrieUsed += TRIE_SIZEOF;
+
+ return ret;
+ }
+}
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
new file mode 100644
index 0000000..2ee4f62
--- /dev/null
+++ b/core/java/android/text/BoringLayout.java
@@ -0,0 +1,388 @@
+/*
+ * 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.Path;
+import android.util.FloatMath;
+
+/**
+ * A BoringLayout is a very simple Layout implementation for text that
+ * fits on a single line and is all left-to-right characters.
+ * You will probably never want to make one of these yourself;
+ * if you do, be sure to call {@link #isBoring} first to make sure
+ * the text meets the criteria.
+ * st…en
) of text in this
+ * Editable with a copy of the slice start…end
from
+ * source
. The destination slice may be empty, in which case
+ * the operation is an insertion, or the source slice may be empty,
+ * in which case the operation is a deletion.
+ * source
text.
+ * source
+ * is Spanned, the spans from it are preserved into the Editable.
+ * Existing spans within the Editable that entirely cover the replaced
+ * range are retained, but any that were strictly within the range
+ * that was replaced are removed. As a special case, the cursor
+ * position is preserved even when the entire range where it is
+ * located is replaced.
+ * @return a reference to this object.
+ */
+ public Editable replace(int st, int en, CharSequence source, int start, int end);
+
+ /**
+ * Convenience for replace(st, en, text, 0, text.length())
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable replace(int st, int en, CharSequence text);
+
+ /**
+ * Convenience for replace(where, where, text, start, end)
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable insert(int where, CharSequence text, int start, int end);
+
+ /**
+ * Convenience for replace(where, where, text, 0, text.length());
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable insert(int where, CharSequence text);
+
+ /**
+ * Convenience for replace(st, en, "", 0, 0)
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable delete(int st, int en);
+
+ /**
+ * Convenience for replace(length(), length(), text, 0, text.length())
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable append(CharSequence text);
+
+ /**
+ * Convenience for replace(length(), length(), text, start, end)
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable append(CharSequence text, int start, int end);
+
+ /**
+ * Convenience for append(String.valueOf(text)).
+ * @see #replace(int, int, CharSequence, int, int)
+ */
+ public Editable append(char text);
+
+ /**
+ * Convenience for replace(0, length(), "", 0, 0)
+ * @see #replace(int, int, CharSequence, int, int)
+ * Note that this clears the text, not the spans;
+ * use {@link #clearSpans} if you need that.
+ */
+ public void clear();
+
+ /**
+ * Removes all spans from the Editable, as if by calling
+ * {@link #removeSpan} on each of them.
+ */
+ public void clearSpans();
+
+ /**
+ * Sets the series of filters that will be called in succession
+ * whenever the text of this Editable is changed, each of which has
+ * the opportunity to limit or transform the text that is being inserted.
+ */
+ public void setFilters(InputFilter[] filters);
+
+ /**
+ * Returns the array of input filters that are currently applied
+ * to changes to this Editable.
+ */
+ public InputFilter[] getFilters();
+
+ /**
+ * Factory used by TextView to create new Editables. You can subclass
+ * it to provide something other than SpannableStringBuilder.
+ */
+ public static class Factory {
+ private static Editable.Factory sInstance = new Editable.Factory();
+
+ /**
+ * Returns the standard Editable Factory.
+ */
+ public static Editable.Factory getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Returns a new SpannedStringBuilder from the specified
+ * CharSequence. You can override this to provide
+ * a different kind of Spanned.
+ */
+ public Editable newEditable(CharSequence source) {
+ return new SpannableStringBuilder(source);
+ }
+ }
+}
diff --git a/core/java/android/text/GetChars.java b/core/java/android/text/GetChars.java
new file mode 100644
index 0000000..348a911
--- /dev/null
+++ b/core/java/android/text/GetChars.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Please implement this interface if your CharSequence has a
+ * getChars() method like the one in String that is faster than
+ * calling charAt() multiple times.
+ */
+public interface GetChars
+extends CharSequence
+{
+ /**
+ * Exactly like String.getChars(): copy chars start
+ * through end - 1
from this CharSequence into dest
+ * beginning at offset destoff
.
+ */
+ public void getChars(int start, int end, char[] dest, int destoff);
+}
diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java
new file mode 100644
index 0000000..c3bd0ae
--- /dev/null
+++ b/core/java/android/text/GraphicsOperations.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2008 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;
+
+/**
+ * Please implement this interface if your CharSequence can do quick
+ * draw/measure/widths calculations from an internal array.
+ * {@hide}
+ */
+public interface GraphicsOperations
+extends CharSequence
+{
+ /**
+ * Just like {@link Canvas#drawText}.
+ */
+ void drawText(Canvas c, int start, int end,
+ float x, float y, Paint p);
+
+ /**
+ * Just like {@link Paint#measureText}.
+ */
+ float measureText(int start, int end, Paint p);
+
+
+ /**
+ * Just like {@link Paint#getTextWidths}.
+ */
+ public int getTextWidths(int start, int end, float[] widths, Paint p);
+}
diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java
new file mode 100644
index 0000000..90f5e4c
--- /dev/null
+++ b/core/java/android/text/Html.java
@@ -0,0 +1,750 @@
+/*
+ * Copyright (C) 2007 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 org.ccil.cowan.tagsoup.HTMLSchema;
+import org.ccil.cowan.tagsoup.Parser;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.ParagraphStyle;
+import android.text.style.QuoteSpan;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.SubscriptSpan;
+import android.text.style.SuperscriptSpan;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
+import android.text.style.UnderlineSpan;
+import com.android.internal.util.XmlUtils;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.CharBuffer;
+
+/**
+ * This class processes HTML strings into displayable styled text.
+ * Not all HTML tags are supported.
+ */
+public class Html {
+ /**
+ * Retrieves images for HTML <img> tags.
+ */
+ public static interface ImageGetter {
+ /**
+ * This methos is called when the HTML parser encounters an
+ * <img> tag. The source
argument is the
+ * string from the "src" attribute; the return value should be
+ * a Drawable representation of the image or null
+ * for a generic replacement image. Make sure you call
+ * setBounds() on your Drawable if it doesn't already have
+ * its bounds set.
+ */
+ public Drawable getDrawable(String source);
+ }
+
+ /**
+ * Is notified when HTML tags are encountered that the parser does
+ * not know how to interpret.
+ */
+ public static interface TagHandler {
+ /**
+ * This method will be called whenn the HTML parser encounters
+ * a tag that it does not know how to interpret.
+ */
+ public void handleTag(boolean opening, String tag,
+ Editable output, XMLReader xmlReader);
+ }
+
+ private Html() { }
+
+ /**
+ * Returns displayable styled text from the provided HTML string.
+ * Any <img> tags in the HTML will display as a generic
+ * replacement image which your program can then go through and
+ * replace with real images.
+ *
+ * ");
+ }
+
+ withinBlockquote(out, text, i, next);
+
+ for (QuoteSpan quote: quotes) {
+ out.append("
\n");
+ }
+ }
+
+ return out.toString();
+ }
+
+ private static void withinBlockquote(StringBuilder out, Spanned text,
+ int start, int end) {
+ out.append("");
+ }
+ if (style[j] instanceof URLSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof ImageSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof UnderlineSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof SubscriptSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof SuperscriptSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof TypefaceSpan) {
+ String s = ((TypefaceSpan) style[j]).getFamily();
+
+ if (s.equals("monospace")) {
+ out.append("");
+ }
+ }
+ if (style[j] instanceof StyleSpan) {
+ int s = ((StyleSpan) style[j]).getStyle();
+
+ if ((s & Typeface.BOLD) != 0) {
+ out.append("");
+ }
+ if ((s & Typeface.ITALIC) != 0) {
+ out.append("");
+ }
+ }
+ }
+ }
+
+ String p = last ? "" : "");
+
+ // Don't output the dummy character underlying the image.
+ i = next;
+ }
+ }
+
+ withinStyle(out, text, i, next);
+
+ for (int j = style.length - 1; j >= 0; j--) {
+ if (style[j] instanceof URLSpan) {
+ out.append("");
+ }
+ if (style[j] instanceof StrikethroughSpan) {
+ out.append("
";
+
+ if (nl == 1) {
+ out.append("
\n");
+ } else if (nl == 2) {
+ out.append(p);
+ } else {
+ for (int i = 2; i < nl; i++) {
+ out.append("
");
+ }
+
+ out.append(p);
+ }
+ }
+
+ private static void withinStyle(StringBuilder out, Spanned text,
+ int start, int end) {
+ for (int i = start; i < end; i++) {
+ char c = text.charAt(i);
+
+ if (c == '<') {
+ out.append("<");
+ } else if (c == '>') {
+ out.append(">");
+ } else if (c == '&') {
+ out.append("&");
+ } else if (c > 0x7E || c < ' ') {
+ out.append("" + ((int) c) + ";");
+ } else if (c == ' ') {
+ while (i + 1 < end && text.charAt(i + 1) == ' ') {
+ out.append(" ");
+ i++;
+ }
+
+ out.append(' ');
+ } else {
+ out.append(c);
+ }
+ }
+ }
+}
+
+class HtmlToSpannedConverter implements ContentHandler {
+
+ private static final float[] HEADER_SIZES = {
+ 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
+ };
+
+ private String mSource;
+ private XMLReader mReader;
+ private SpannableStringBuilder mSpannableStringBuilder;
+ private Html.ImageGetter mImageGetter;
+ private Html.TagHandler mTagHandler;
+
+ public HtmlToSpannedConverter(
+ String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
+ Parser parser) {
+ mSource = source;
+ mSpannableStringBuilder = new SpannableStringBuilder();
+ mImageGetter = imageGetter;
+ mTagHandler = tagHandler;
+ mReader = parser;
+ }
+
+ public Spanned convert() {
+
+ mReader.setContentHandler(this);
+ try {
+ mReader.parse(new InputSource(new StringReader(mSource)));
+ } catch (IOException e) {
+ // We are reading from a string. There should not be IO problems.
+ throw new RuntimeException(e);
+ } catch (SAXException e) {
+ // TagSoup doesn't throw parse exceptions.
+ throw new RuntimeException(e);
+ }
+
+ // Fix flags and range for paragraph-type markup.
+ Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
+ for (int i = 0; i < obj.length; i++) {
+ int start = mSpannableStringBuilder.getSpanStart(obj[i]);
+ int end = mSpannableStringBuilder.getSpanEnd(obj[i]);
+
+ // If the last line of the range is blank, back off by one.
+ if (end - 2 >= 0) {
+ if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
+ mSpannableStringBuilder.charAt(end - 2) == '\n') {
+ end--;
+ }
+ }
+
+ if (end == start) {
+ mSpannableStringBuilder.removeSpan(obj[i]);
+ } else {
+ mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH);
+ }
+ }
+
+ return mSpannableStringBuilder;
+ }
+
+ private void handleStartTag(String tag, Attributes attributes) {
+ if (tag.equalsIgnoreCase("br")) {
+ // We don't need to handle this. TagSoup will ensure that there's a for each
+ // so we can safely emite the linebreaks when we handle the close tag.
+ } else if (tag.equalsIgnoreCase("p")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("div")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("em")) {
+ start(mSpannableStringBuilder, new Bold());
+ } else if (tag.equalsIgnoreCase("b")) {
+ start(mSpannableStringBuilder, new Bold());
+ } else if (tag.equalsIgnoreCase("strong")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("cite")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("dfn")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("i")) {
+ start(mSpannableStringBuilder, new Italic());
+ } else if (tag.equalsIgnoreCase("big")) {
+ start(mSpannableStringBuilder, new Big());
+ } else if (tag.equalsIgnoreCase("small")) {
+ start(mSpannableStringBuilder, new Small());
+ } else if (tag.equalsIgnoreCase("font")) {
+ startFont(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("blockquote")) {
+ handleP(mSpannableStringBuilder);
+ start(mSpannableStringBuilder, new Blockquote());
+ } else if (tag.equalsIgnoreCase("tt")) {
+ start(mSpannableStringBuilder, new Monospace());
+ } else if (tag.equalsIgnoreCase("a")) {
+ startA(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("u")) {
+ start(mSpannableStringBuilder, new Underline());
+ } else if (tag.equalsIgnoreCase("sup")) {
+ start(mSpannableStringBuilder, new Super());
+ } else if (tag.equalsIgnoreCase("sub")) {
+ start(mSpannableStringBuilder, new Sub());
+ } else if (tag.length() == 2 &&
+ Character.toLowerCase(tag.charAt(0)) == 'h' &&
+ tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
+ handleP(mSpannableStringBuilder);
+ start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
+ } else if (tag.equalsIgnoreCase("img")) {
+ startImg(mSpannableStringBuilder, attributes, mImageGetter);
+ } else if (mTagHandler != null) {
+ mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
+ }
+ }
+
+ private void handleEndTag(String tag) {
+ if (tag.equalsIgnoreCase("br")) {
+ handleBr(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("p")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("div")) {
+ handleP(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("em")) {
+ end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
+ } else if (tag.equalsIgnoreCase("b")) {
+ end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
+ } else if (tag.equalsIgnoreCase("strong")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("cite")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("dfn")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("i")) {
+ end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
+ } else if (tag.equalsIgnoreCase("big")) {
+ end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
+ } else if (tag.equalsIgnoreCase("small")) {
+ end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
+ } else if (tag.equalsIgnoreCase("font")) {
+ endFont(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("blockquote")) {
+ handleP(mSpannableStringBuilder);
+ end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
+ } else if (tag.equalsIgnoreCase("tt")) {
+ end(mSpannableStringBuilder, Monospace.class,
+ new TypefaceSpan("monospace"));
+ } else if (tag.equalsIgnoreCase("a")) {
+ endA(mSpannableStringBuilder);
+ } else if (tag.equalsIgnoreCase("u")) {
+ end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
+ } else if (tag.equalsIgnoreCase("sup")) {
+ end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
+ } else if (tag.equalsIgnoreCase("sub")) {
+ end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
+ } else if (tag.length() == 2 &&
+ Character.toLowerCase(tag.charAt(0)) == 'h' &&
+ tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
+ handleP(mSpannableStringBuilder);
+ endHeader(mSpannableStringBuilder);
+ } else if (mTagHandler != null) {
+ mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
+ }
+ }
+
+ private static void handleP(SpannableStringBuilder text) {
+ int len = text.length();
+
+ if (len >= 1 && text.charAt(len - 1) == '\n') {
+ if (len >= 2 && text.charAt(len - 2) == '\n') {
+ return;
+ }
+
+ text.append("\n");
+ return;
+ }
+
+ if (len != 0) {
+ text.append("\n\n");
+ }
+ }
+
+ private static void handleBr(SpannableStringBuilder text) {
+ text.append("\n");
+ }
+
+ private static Object getLast(Spanned text, Class kind) {
+ /*
+ * This knows that the last returned object from getSpans()
+ * will be the most recently added.
+ */
+ Object[] objs = text.getSpans(0, text.length(), kind);
+
+ if (objs.length == 0) {
+ return null;
+ } else {
+ return objs[objs.length - 1];
+ }
+ }
+
+ private static void start(SpannableStringBuilder text, Object mark) {
+ int len = text.length();
+ text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void end(SpannableStringBuilder text, Class kind,
+ Object repl) {
+ int len = text.length();
+ Object obj = getLast(text, kind);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ return;
+ }
+
+ private static void startImg(SpannableStringBuilder text,
+ Attributes attributes, Html.ImageGetter img) {
+ String src = attributes.getValue("", "src");
+ Drawable d = null;
+
+ if (img != null) {
+ d = img.getDrawable(src);
+ }
+
+ if (d == null) {
+ d = Resources.getSystem().
+ getDrawable(com.android.internal.R.drawable.unknown_image);
+ d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
+ }
+
+ int len = text.length();
+ text.append("\uFFFC");
+
+ text.setSpan(new ImageSpan(d, src), len, text.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static void startFont(SpannableStringBuilder text,
+ Attributes attributes) {
+ String color = attributes.getValue("", "color");
+ String face = attributes.getValue("", "face");
+
+ int len = text.length();
+ text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void endFont(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Font.class);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ Font f = (Font) obj;
+
+ if (f.mColor != null) {
+ int c = -1;
+
+ if (f.mColor.equalsIgnoreCase("aqua")) {
+ c = 0x00FFFF;
+ } else if (f.mColor.equalsIgnoreCase("black")) {
+ c = 0x000000;
+ } else if (f.mColor.equalsIgnoreCase("blue")) {
+ c = 0x0000FF;
+ } else if (f.mColor.equalsIgnoreCase("fuchsia")) {
+ c = 0xFF00FF;
+ } else if (f.mColor.equalsIgnoreCase("green")) {
+ c = 0x008000;
+ } else if (f.mColor.equalsIgnoreCase("grey")) {
+ c = 0x808080;
+ } else if (f.mColor.equalsIgnoreCase("lime")) {
+ c = 0x00FF00;
+ } else if (f.mColor.equalsIgnoreCase("maroon")) {
+ c = 0x800000;
+ } else if (f.mColor.equalsIgnoreCase("navy")) {
+ c = 0x000080;
+ } else if (f.mColor.equalsIgnoreCase("olive")) {
+ c = 0x808000;
+ } else if (f.mColor.equalsIgnoreCase("purple")) {
+ c = 0x800080;
+ } else if (f.mColor.equalsIgnoreCase("red")) {
+ c = 0xFF0000;
+ } else if (f.mColor.equalsIgnoreCase("silver")) {
+ c = 0xC0C0C0;
+ } else if (f.mColor.equalsIgnoreCase("teal")) {
+ c = 0x008080;
+ } else if (f.mColor.equalsIgnoreCase("white")) {
+ c = 0xFFFFFF;
+ } else if (f.mColor.equalsIgnoreCase("yellow")) {
+ c = 0xFFFF00;
+ } else {
+ try {
+ c = XmlUtils.convertValueToInt(f.mColor, -1);
+ } catch (NumberFormatException nfe) {
+ // Can't understand the color, so just drop it.
+ }
+ }
+
+ if (c != -1) {
+ text.setSpan(new ForegroundColorSpan(c | 0xFF000000),
+ where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ if (f.mFace != null) {
+ text.setSpan(new TypefaceSpan(f.mFace), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ private static void startA(SpannableStringBuilder text, Attributes attributes) {
+ String href = attributes.getValue("", "href");
+
+ int len = text.length();
+ text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
+ }
+
+ private static void endA(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Href.class);
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ if (where != len) {
+ Href h = (Href) obj;
+
+ if (h.mHref != null) {
+ text.setSpan(new URLSpan(h.mHref), where, len,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ private static void endHeader(SpannableStringBuilder text) {
+ int len = text.length();
+ Object obj = getLast(text, Header.class);
+
+ int where = text.getSpanStart(obj);
+
+ text.removeSpan(obj);
+
+ // Back off not to change only the text, not the blank line.
+ while (len > where && text.charAt(len - 1) == '\n') {
+ len--;
+ }
+
+ if (where != len) {
+ Header h = (Header) obj;
+
+ text.setSpan(new RelativeSizeSpan(HEADER_SIZES[h.mLevel]),
+ where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ text.setSpan(new StyleSpan(Typeface.BOLD),
+ where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ public void setDocumentLocator(Locator locator) {
+ }
+
+ public void startDocument() throws SAXException {
+ }
+
+ public void endDocument() throws SAXException {
+ }
+
+ public void startPrefixMapping(String prefix, String uri) throws SAXException {
+ }
+
+ public void endPrefixMapping(String prefix) throws SAXException {
+ }
+
+ public void startElement(String uri, String localName, String qName, Attributes attributes)
+ throws SAXException {
+ handleStartTag(localName, attributes);
+ }
+
+ public void endElement(String uri, String localName, String qName) throws SAXException {
+ handleEndTag(localName);
+ }
+
+ public void characters(char ch[], int start, int length) throws SAXException {
+ mSpannableStringBuilder.append(CharBuffer.wrap(ch, start, length));
+ }
+
+ public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
+ }
+
+ public void processingInstruction(String target, String data) throws SAXException {
+ }
+
+ public void skippedEntity(String name) throws SAXException {
+ }
+
+ private static class Bold { }
+ private static class Italic { }
+ private static class Underline { }
+ private static class Big { }
+ private static class Small { }
+ private static class Monospace { }
+ private static class Blockquote { }
+ private static class Super { }
+ private static class Sub { }
+
+ private static class Font {
+ public String mColor;
+ public String mFace;
+
+ public Font(String color, String face) {
+ mColor = color;
+ mFace = face;
+ }
+ }
+
+ private static class Href {
+ public String mHref;
+
+ public Href(String href) {
+ mHref = href;
+ }
+ }
+
+ private static class Header {
+ private int mLevel;
+
+ public Header(int level) {
+ mLevel = level;
+ }
+ }
+}
diff --git a/core/java/android/text/IClipboard.aidl b/core/java/android/text/IClipboard.aidl
new file mode 100644
index 0000000..4deb5c8
--- /dev/null
+++ b/core/java/android/text/IClipboard.aidl
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2008, 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;
+
+/**
+ * Programming interface to the clipboard, which allows copying and pasting
+ * between applications.
+ * {@hide}
+ */
+interface IClipboard {
+ /**
+ * Returns the text on the clipboard. It will eventually be possible
+ * to store types other than text too, in which case this will return
+ * null if the type cannot be coerced to text.
+ */
+ CharSequence getClipboardText();
+
+ /**
+ * Sets the contents of the clipboard to the specified text.
+ */
+ void setClipboardText(CharSequence text);
+
+ /**
+ * Returns true if the clipboard contains text; false otherwise.
+ */
+ boolean hasClipboardText();
+}
+
diff --git a/core/java/android/text/InputFilter.java b/core/java/android/text/InputFilter.java
new file mode 100644
index 0000000..e1563ae
--- /dev/null
+++ b/core/java/android/text/InputFilter.java
@@ -0,0 +1,92 @@
+/*
+ * 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;
+
+/**
+ * InputFilters can be attached to {@link Editable}s to constrain the
+ * changes that can be made to them.
+ */
+public interface InputFilter
+{
+ /**
+ * This method is called when the buffer is going to replace the
+ * range dstart … dend
of dest
+ * with the new text from the range start … end
+ * of source
. Return the CharSequence that you would
+ * like to have placed there instead, including an empty string
+ * if appropriate, or null
to accept the original
+ * replacement. Be careful to not to reject 0-length replacements,
+ * as this is what happens when you delete text. Also beware that
+ * you should not attempt to make any changes to dest
+ * from this method; you may only examine it for context.
+ */
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend);
+
+ /**
+ * This filter will capitalize all the lower case letters that are added
+ * through edits.
+ */
+ public static class AllCaps implements InputFilter {
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ for (int i = start; i < end; i++) {
+ if (Character.isLowerCase(source.charAt(i))) {
+ char[] v = new char[end - start];
+ TextUtils.getChars(source, start, end, v, 0);
+ String s = new String(v).toUpperCase();
+
+ if (source instanceof Spanned) {
+ SpannableString sp = new SpannableString(s);
+ TextUtils.copySpansFrom((Spanned) source,
+ start, end, null, sp, 0);
+ return sp;
+ } else {
+ return s;
+ }
+ }
+ }
+
+ return null; // keep original
+ }
+ }
+
+ /**
+ * This filter will constrain edits not to make the length of the text
+ * greater than the specified length.
+ */
+ public static class LengthFilter implements InputFilter {
+ public LengthFilter(int max) {
+ mMax = max;
+ }
+
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ int keep = mMax - (dest.length() - (dend - dstart));
+
+ if (keep <= 0) {
+ return "";
+ } else if (keep >= end - start) {
+ return null; // keep original
+ } else {
+ return source.subSequence(start, start + keep);
+ }
+ }
+
+ private int mMax;
+ }
+}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
new file mode 100644
index 0000000..346db49
--- /dev/null
+++ b/core/java/android/text/Layout.java
@@ -0,0 +1,1745 @@
+/*
+ * 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.
+ *
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);
+ 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
+ * Its endpoints must be the start or end of the buffer or
+ * immediately after a \n character, and if the \n
+ * that anchors it is deleted, the endpoint is pulled to the
+ * next \n that follows in the buffer (or to the end of
+ * the buffer).
+ */
+ public static final int SPAN_PARAGRAPH = 0x33;
+
+ /**
+ * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand
+ * to include text inserted at their starting point but not at their
+ * ending point. When 0-length, they behave like marks.
+ */
+ public static final int SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_MARK;
+
+ /**
+ * Spans of type SPAN_INCLUSIVE_INCLUSIVE expand
+ * to include text inserted at either their starting or ending point.
+ */
+ public static final int SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_POINT;
+
+ /**
+ * Spans of type SPAN_EXCLUSIVE_EXCLUSIVE do not expand
+ * to include text inserted at either their starting or ending point.
+ * They can never have a length of 0 and are automatically removed
+ * from the buffer if all the text they cover is removed.
+ */
+ public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK;
+
+ /**
+ * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand
+ * to include text inserted at their ending point but not at their
+ * starting point. When 0-length, they behave like points.
+ */
+ public static final int SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_POINT;
+
+ /**
+ * The bits numbered SPAN_USER_SHIFT and above are available
+ * for callers to use to store scalar data associated with their
+ * span object.
+ */
+ public static final int SPAN_USER_SHIFT = 24;
+ /**
+ * The bits specified by the SPAN_USER bitfield are available
+ * for callers to use to store scalar data associated with their
+ * span object.
+ */
+ public static final int SPAN_USER = 0xFFFFFFFF << SPAN_USER_SHIFT;
+
+ /**
+ * The bits numbered just above SPAN_PRIORITY_SHIFT determine the order
+ * of change notifications -- higher numbers go first. You probably
+ * don't need to set this; it is used so that when text changes, the
+ * text layout gets the chance to update itself before any other
+ * callbacks can inquire about the layout of the text.
+ */
+ public static final int SPAN_PRIORITY_SHIFT = 16;
+ /**
+ * The bits specified by the SPAN_PRIORITY bitmap determine the order
+ * of change notifications -- higher numbers go first. You probably
+ * don't need to set this; it is used so that when text changes, the
+ * text layout gets the chance to update itself before any other
+ * callbacks can inquire about the layout of the text.
+ */
+ public static final int SPAN_PRIORITY = 0xFF << SPAN_PRIORITY_SHIFT;
+
+ /**
+ * Return an array of the markup objects attached to the specified
+ * slice of this CharSequence and whose type is the specified type
+ * or a subclass of it. Specify Object.class for the type if you
+ * want all the objects regardless of type.
+ */
+ public This is used by widgets to control text layout. You should not need
+ * to use this class directly unless you are implementing your own widget
+ * or custom display object, or would be tempted to call
+ * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
+ * Canvas.drawText()} directly. The most efficient way to use this class is:
+ *
+ * If the final character in the string to split is the delimiter then no empty string will
+ * be returned for the empty string after that delimeter. That is, splitting "a,b," on
+ * comma will return "a", "b", not "a", "b", "".
+ */
+ public static class SimpleStringSplitter implements StringSplitter, Iterator Provides classes that monitor or modify keypad input. You can use these classes to modify the type of keypad entry
+for your application, or decipher the keypresses entered for your specific
+entry method. For example: Provides classes used to render or track text and text spans on the screen. You can use these classes to design your own widgets that manage text,
+to handle arbitrary text spans for changes, or to handle drawing yourself
+for an existing widget. The Span… interfaces and classes are used to create or manage spans of
+text in a View item. You can use these to style the text or background, or to
+listen for changes. If creating your own widget, extend DynamicLayout, to manages
+the actual wrapping and drawing of your text.
+
+
diff --git a/core/java/android/text/style/AbsoluteSizeSpan.java b/core/java/android/text/style/AbsoluteSizeSpan.java
new file mode 100644
index 0000000..8f6ed5a
--- /dev/null
+++ b/core/java/android/text/style/AbsoluteSizeSpan.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class AbsoluteSizeSpan extends MetricAffectingSpan {
+
+ private int mSize;
+
+ public AbsoluteSizeSpan(int size) {
+ mSize = size;
+ }
+
+ public int getSize() {
+ return mSize;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setTextSize(mSize);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ ds.setTextSize(mSize);
+ }
+}
diff --git a/core/java/android/text/style/AlignmentSpan.java b/core/java/android/text/style/AlignmentSpan.java
new file mode 100644
index 0000000..d51edcc
--- /dev/null
+++ b/core/java/android/text/style/AlignmentSpan.java
@@ -0,0 +1,39 @@
+/*
+ * 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.style;
+
+import android.text.Layout;
+
+public interface AlignmentSpan
+extends ParagraphStyle
+{
+ public Layout.Alignment getAlignment();
+
+ public static class Standard
+ implements AlignmentSpan
+ {
+ public Standard(Layout.Alignment align) {
+ mAlignment = align;
+ }
+
+ public Layout.Alignment getAlignment() {
+ return mAlignment;
+ }
+
+ private Layout.Alignment mAlignment;
+ }
+}
diff --git a/core/java/android/text/style/BackgroundColorSpan.java b/core/java/android/text/style/BackgroundColorSpan.java
new file mode 100644
index 0000000..be6ef77
--- /dev/null
+++ b/core/java/android/text/style/BackgroundColorSpan.java
@@ -0,0 +1,37 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+
+public class BackgroundColorSpan extends CharacterStyle {
+
+ private int mColor;
+
+ public BackgroundColorSpan(int color) {
+ mColor = color;
+ }
+
+ public int getBackgroundColor() {
+ return mColor;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.bgColor = mColor;
+ }
+}
diff --git a/core/java/android/text/style/BulletSpan.java b/core/java/android/text/style/BulletSpan.java
new file mode 100644
index 0000000..70c4d33
--- /dev/null
+++ b/core/java/android/text/style/BulletSpan.java
@@ -0,0 +1,76 @@
+/*
+ * 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.style;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Layout;
+import android.text.Spanned;
+
+public class BulletSpan implements LeadingMarginSpan {
+
+ public BulletSpan() {
+ mGapWidth = STANDARD_GAP_WIDTH;
+ }
+
+ public BulletSpan(int gapWidth) {
+ mGapWidth = gapWidth;
+ }
+
+ public BulletSpan(int gapWidth, int color) {
+ mGapWidth = gapWidth;
+ mWantColor = true;
+ mColor = color;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return 2 * BULLET_RADIUS + mGapWidth;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout l) {
+ if (((Spanned) text).getSpanStart(this) == start) {
+ Paint.Style style = p.getStyle();
+ int oldcolor = 0;
+
+ if (mWantColor) {
+ oldcolor = p.getColor();
+ p.setColor(mColor);
+ }
+
+ p.setStyle(Paint.Style.FILL);
+
+ c.drawCircle(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f,
+ BULLET_RADIUS, p);
+
+ if (mWantColor) {
+ p.setColor(oldcolor);
+ }
+
+ p.setStyle(style);
+ }
+ }
+
+ private int mGapWidth;
+ private boolean mWantColor;
+ private int mColor;
+
+ private static final int BULLET_RADIUS = 3;
+ public static final int STANDARD_GAP_WIDTH = 2;
+}
diff --git a/core/java/android/text/style/CharacterStyle.java b/core/java/android/text/style/CharacterStyle.java
new file mode 100644
index 0000000..7620d29
--- /dev/null
+++ b/core/java/android/text/style/CharacterStyle.java
@@ -0,0 +1,87 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+/**
+ * The classes that affect character-level text formatting extend this
+ * class. Most also extend {@link MetricAffectingSpan}.
+ */
+public abstract class CharacterStyle {
+ public abstract void updateDrawState(TextPaint tp);
+
+ /**
+ * A given CharacterStyle can only applied to a single region of a given
+ * Spanned. If you need to attach the same CharacterStyle to multiple
+ * regions, you can use this method to wrap it with a new object that
+ * will have the same effect but be a distinct object so that it can
+ * also be attached without conflict.
+ */
+ public static CharacterStyle wrap(CharacterStyle cs) {
+ if (cs instanceof MetricAffectingSpan) {
+ return new MetricAffectingSpan.Passthrough((MetricAffectingSpan) cs);
+ } else {
+ return new Passthrough(cs);
+ }
+ }
+
+ /**
+ * Returns "this" for most CharacterStyles, but for CharacterStyles
+ * that were generated by {@link #wrap}, returns the underlying
+ * CharacterStyle.
+ */
+ public CharacterStyle getUnderlying() {
+ return this;
+ }
+
+ /**
+ * A Passthrough CharacterStyle is one that
+ * passes {@link #updateDrawState} calls through to the
+ * specified CharacterStyle while still being a distinct object,
+ * and is therefore able to be attached to the same Spannable
+ * to which the specified CharacterStyle is already attached.
+ */
+ private static class Passthrough extends CharacterStyle {
+ private CharacterStyle mStyle;
+
+ /**
+ * Creates a new Passthrough of the specfied CharacterStyle.
+ */
+ public Passthrough(CharacterStyle cs) {
+ mStyle = cs;
+ }
+
+ /**
+ * Passes updateDrawState through to the underlying CharacterStyle.
+ */
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ mStyle.updateDrawState(tp);
+ }
+
+ /**
+ * Returns the CharacterStyle underlying this one, or the one
+ * underlying it if it too is a Passthrough.
+ */
+ @Override
+ public CharacterStyle getUnderlying() {
+ return mStyle.getUnderlying();
+ }
+ }
+}
diff --git a/core/java/android/text/style/ClickableSpan.java b/core/java/android/text/style/ClickableSpan.java
new file mode 100644
index 0000000..a232ed7
--- /dev/null
+++ b/core/java/android/text/style/ClickableSpan.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008 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.style;
+
+import android.text.TextPaint;
+import android.view.View;
+
+/**
+ * If an object of this type is attached to the text of a TextView
+ * with a movement method of LinkMovementMethod, the affected spans of
+ * text can be selected. If clicked, the {@link #onClick} method will
+ * be called.
+ */
+public abstract class ClickableSpan extends CharacterStyle {
+
+ /**
+ * Performs the click action associated with this span.
+ */
+ public abstract void onClick(View widget);
+
+ /**
+ * Makes the text underlined and in the link color.
+ */
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setColor(ds.linkColor);
+ ds.setUnderlineText(true);
+ }
+}
diff --git a/core/java/android/text/style/DrawableMarginSpan.java b/core/java/android/text/style/DrawableMarginSpan.java
new file mode 100644
index 0000000..3c471a5
--- /dev/null
+++ b/core/java/android/text/style/DrawableMarginSpan.java
@@ -0,0 +1,79 @@
+/*
+ * 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.style;
+
+import android.graphics.drawable.Drawable;
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.text.Spanned;
+import android.text.Layout;
+
+public class DrawableMarginSpan
+implements LeadingMarginSpan, LineHeightSpan
+{
+ public DrawableMarginSpan(Drawable b) {
+ mDrawable = b;
+ }
+
+ public DrawableMarginSpan(Drawable b, int pad) {
+ mDrawable = b;
+ mPad = pad;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return mDrawable.getIntrinsicWidth() + mPad;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ int st = ((Spanned) text).getSpanStart(this);
+ int ix = (int)x;
+ int itop = (int)layout.getLineTop(layout.getLineForOffset(st));
+
+ int dw = mDrawable.getIntrinsicWidth();
+ int dh = mDrawable.getIntrinsicHeight();
+
+ if (dir < 0)
+ x -= dw;
+
+ // XXX What to do about Paint?
+ mDrawable.setBounds(ix, itop, ix+dw, itop+dh);
+ mDrawable.draw(c);
+ }
+
+ public void chooseHeight(CharSequence text, int start, int end,
+ int istartv, int v,
+ Paint.FontMetricsInt fm) {
+ if (end == ((Spanned) text).getSpanEnd(this)) {
+ int ht = mDrawable.getIntrinsicHeight();
+
+ int need = ht - (v + fm.descent - fm.ascent - istartv);
+ if (need > 0)
+ fm.descent += need;
+
+ need = ht - (v + fm.bottom - fm.top - istartv);
+ if (need > 0)
+ fm.bottom += need;
+ }
+ }
+
+ private Drawable mDrawable;
+ private int mPad;
+}
diff --git a/core/java/android/text/style/DynamicDrawableSpan.java b/core/java/android/text/style/DynamicDrawableSpan.java
new file mode 100644
index 0000000..3bcc335
--- /dev/null
+++ b/core/java/android/text/style/DynamicDrawableSpan.java
@@ -0,0 +1,82 @@
+/*
+ * 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.style;
+
+import java.lang.ref.WeakReference;
+
+import android.graphics.drawable.Drawable;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+/**
+ *
+ */
+public abstract class DynamicDrawableSpan
+extends ReplacementSpan
+{
+ /**
+ * Your subclass must implement this method to provide the bitmap
+ * to be drawn. The dimensions of the bitmap must be the same
+ * from each call to the next.
+ */
+ public abstract Drawable getDrawable();
+
+ public int getSize(Paint paint, CharSequence text,
+ int start, int end,
+ Paint.FontMetricsInt fm) {
+ Drawable b = getCachedDrawable();
+
+ if (fm != null) {
+ fm.ascent = -b.getIntrinsicHeight();
+ fm.descent = 0;
+
+ fm.top = fm.ascent;
+ fm.bottom = 0;
+ }
+
+ return b.getIntrinsicWidth();
+ }
+
+ public void draw(Canvas canvas, CharSequence text,
+ int start, int end, float x,
+ int top, int y, int bottom, Paint paint) {
+ Drawable b = getCachedDrawable();
+ canvas.save();
+
+ canvas.translate(x, bottom-b.getIntrinsicHeight());;
+ b.draw(canvas);
+ canvas.restore();
+ }
+
+ private Drawable getCachedDrawable() {
+ WeakReference wr = mDrawableRef;
+ Drawable b = null;
+
+ if (wr != null)
+ b = (Drawable) wr.get();
+
+ if (b == null) {
+ b = getDrawable();
+ mDrawableRef = new WeakReference(b);
+ }
+
+ return b;
+ }
+
+ private WeakReference mDrawableRef;
+}
+
diff --git a/core/java/android/text/style/ForegroundColorSpan.java b/core/java/android/text/style/ForegroundColorSpan.java
new file mode 100644
index 0000000..5cccd9c
--- /dev/null
+++ b/core/java/android/text/style/ForegroundColorSpan.java
@@ -0,0 +1,38 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class ForegroundColorSpan extends CharacterStyle {
+
+ private int mColor;
+
+ public ForegroundColorSpan(int color) {
+ mColor = color;
+ }
+
+ public int getForegroundColor() {
+ return mColor;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setColor(mColor);
+ }
+}
diff --git a/core/java/android/text/style/IconMarginSpan.java b/core/java/android/text/style/IconMarginSpan.java
new file mode 100644
index 0000000..c786a17
--- /dev/null
+++ b/core/java/android/text/style/IconMarginSpan.java
@@ -0,0 +1,73 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.text.Spanned;
+import android.text.Layout;
+
+public class IconMarginSpan
+implements LeadingMarginSpan, LineHeightSpan
+{
+ public IconMarginSpan(Bitmap b) {
+ mBitmap = b;
+ }
+
+ public IconMarginSpan(Bitmap b, int pad) {
+ mBitmap = b;
+ mPad = pad;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return mBitmap.getWidth() + mPad;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ int st = ((Spanned) text).getSpanStart(this);
+ int itop = layout.getLineTop(layout.getLineForOffset(st));
+
+ if (dir < 0)
+ x -= mBitmap.getWidth();
+
+ c.drawBitmap(mBitmap, x, itop, p);
+ }
+
+ public void chooseHeight(CharSequence text, int start, int end,
+ int istartv, int v,
+ Paint.FontMetricsInt fm) {
+ if (end == ((Spanned) text).getSpanEnd(this)) {
+ int ht = mBitmap.getHeight();
+
+ int need = ht - (v + fm.descent - fm.ascent - istartv);
+ if (need > 0)
+ fm.descent += need;
+
+ need = ht - (v + fm.bottom - fm.top - istartv);
+ if (need > 0)
+ fm.bottom += need;
+ }
+ }
+
+ private Bitmap mBitmap;
+ private int mPad;
+}
diff --git a/core/java/android/text/style/ImageSpan.java b/core/java/android/text/style/ImageSpan.java
new file mode 100644
index 0000000..de067b1
--- /dev/null
+++ b/core/java/android/text/style/ImageSpan.java
@@ -0,0 +1,99 @@
+/*
+ * 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.style;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.InputStream;
+
+public class ImageSpan extends DynamicDrawableSpan {
+ private Drawable mDrawable;
+ private Uri mContentUri;
+ private int mResourceId;
+ private Context mContext;
+ private String mSource;
+
+
+ public ImageSpan(Bitmap b) {
+ mDrawable = new BitmapDrawable(b);
+ mDrawable.setBounds(0, 0, mDrawable.getIntrinsicWidth(),
+ mDrawable.getIntrinsicHeight());
+ }
+
+ public ImageSpan(Drawable d) {
+ mDrawable = d;
+ }
+
+ public ImageSpan(Drawable d, String source) {
+ mDrawable = d;
+ mSource = source;
+ }
+
+ public ImageSpan(Context context, Uri uri) {
+ mContext = context;
+ mContentUri = uri;
+ }
+
+ public ImageSpan(Context context, int resourceId) {
+ mContext = context;
+ mResourceId = resourceId;
+ }
+
+ @Override
+ public Drawable getDrawable() {
+ Drawable drawable = null;
+
+ if (mDrawable != null) {
+ drawable = mDrawable;
+ } else if (mContentUri != null) {
+ Bitmap bitmap = null;
+ try {
+ InputStream is = mContext.getContentResolver().openInputStream(
+ mContentUri);
+ bitmap = BitmapFactory.decodeStream(is);
+ drawable = new BitmapDrawable(bitmap);
+ is.close();
+ } catch (Exception e) {
+ Log.e("sms", "Failed to loaded content " + mContentUri, e);
+ }
+ } else {
+ try {
+ drawable = mContext.getResources().getDrawable(mResourceId);
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight());
+ } catch (Exception e) {
+ Log.e("sms", "Unable to find resource: " + mResourceId);
+ }
+ }
+
+ return drawable;
+ }
+
+ /**
+ * Returns the source string that was saved during construction.
+ */
+ public String getSource() {
+ return mSource;
+ }
+
+}
diff --git a/core/java/android/text/style/LeadingMarginSpan.java b/core/java/android/text/style/LeadingMarginSpan.java
new file mode 100644
index 0000000..85a27dc
--- /dev/null
+++ b/core/java/android/text/style/LeadingMarginSpan.java
@@ -0,0 +1,59 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.text.Layout;
+
+public interface LeadingMarginSpan
+extends ParagraphStyle
+{
+ public int getLeadingMargin(boolean first);
+ public void drawLeadingMargin(Canvas c, Paint p,
+ int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout);
+
+ public static class Standard
+ implements LeadingMarginSpan
+ {
+ public Standard(int first, int rest) {
+ mFirst = first;
+ mRest = rest;
+ }
+
+ public Standard(int every) {
+ this(every, every);
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return first ? mFirst : mRest;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p,
+ int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ ;
+ }
+
+ private int mFirst, mRest;
+ }
+}
diff --git a/core/java/android/text/style/LineBackgroundSpan.java b/core/java/android/text/style/LineBackgroundSpan.java
new file mode 100644
index 0000000..854aeaf
--- /dev/null
+++ b/core/java/android/text/style/LineBackgroundSpan.java
@@ -0,0 +1,30 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+
+public interface LineBackgroundSpan
+extends ParagraphStyle
+{
+ public void drawBackground(Canvas c, Paint p,
+ int left, int right,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ int lnum);
+}
diff --git a/core/java/android/text/style/LineHeightSpan.java b/core/java/android/text/style/LineHeightSpan.java
new file mode 100644
index 0000000..c0ef97c
--- /dev/null
+++ b/core/java/android/text/style/LineHeightSpan.java
@@ -0,0 +1,29 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.text.Layout;
+
+public interface LineHeightSpan
+extends ParagraphStyle, WrapTogetherSpan
+{
+ public void chooseHeight(CharSequence text, int start, int end,
+ int spanstartv, int v,
+ Paint.FontMetricsInt fm);
+}
diff --git a/core/java/android/text/style/MaskFilterSpan.java b/core/java/android/text/style/MaskFilterSpan.java
new file mode 100644
index 0000000..781bcec
--- /dev/null
+++ b/core/java/android/text/style/MaskFilterSpan.java
@@ -0,0 +1,39 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.MaskFilter;
+import android.text.TextPaint;
+
+public class MaskFilterSpan extends CharacterStyle {
+
+ private MaskFilter mFilter;
+
+ public MaskFilterSpan(MaskFilter filter) {
+ mFilter = filter;
+ }
+
+ public MaskFilter getMaskFilter() {
+ return mFilter;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setMaskFilter(mFilter);
+ }
+}
diff --git a/core/java/android/text/style/MetricAffectingSpan.java b/core/java/android/text/style/MetricAffectingSpan.java
new file mode 100644
index 0000000..92558eb
--- /dev/null
+++ b/core/java/android/text/style/MetricAffectingSpan.java
@@ -0,0 +1,85 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+/**
+ * The classes that affect character-level text formatting in a way that
+ * changes the width or height of characters extend this class.
+ */
+public abstract class MetricAffectingSpan
+extends CharacterStyle
+implements UpdateLayout {
+
+ public abstract void updateMeasureState(TextPaint p);
+
+ /**
+ * Returns "this" for most MetricAffectingSpans, but for
+ * MetricAffectingSpans that were generated by {@link #wrap},
+ * returns the underlying MetricAffectingSpan.
+ */
+ @Override
+ public MetricAffectingSpan getUnderlying() {
+ return this;
+ }
+
+ /**
+ * A Passthrough MetricAffectingSpan is one that
+ * passes {@link #updateDrawState} and {@link #updateMeasureState}
+ * calls through to the specified MetricAffectingSpan
+ * while still being a distinct object,
+ * and is therefore able to be attached to the same Spannable
+ * to which the specified MetricAffectingSpan is already attached.
+ */
+ /* package */ static class Passthrough extends MetricAffectingSpan {
+ private MetricAffectingSpan mStyle;
+
+ /**
+ * Creates a new Passthrough of the specfied MetricAffectingSpan.
+ */
+ public Passthrough(MetricAffectingSpan cs) {
+ mStyle = cs;
+ }
+
+ /**
+ * Passes updateDrawState through to the underlying MetricAffectingSpan.
+ */
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ mStyle.updateDrawState(tp);
+ }
+
+ /**
+ * Passes updateMeasureState through to the underlying MetricAffectingSpan.
+ */
+ @Override
+ public void updateMeasureState(TextPaint tp) {
+ mStyle.updateMeasureState(tp);
+ }
+
+ /**
+ * Returns the MetricAffectingSpan underlying this one, or the one
+ * underlying it if it too is a Passthrough.
+ */
+ @Override
+ public MetricAffectingSpan getUnderlying() {
+ return mStyle.getUnderlying();
+ }
+ }
+}
diff --git a/core/java/android/text/style/ParagraphStyle.java b/core/java/android/text/style/ParagraphStyle.java
new file mode 100644
index 0000000..423156e
--- /dev/null
+++ b/core/java/android/text/style/ParagraphStyle.java
@@ -0,0 +1,26 @@
+/*
+ * 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.style;
+
+/**
+ * The classes that affect paragraph-level text formatting implement
+ * this interface.
+ */
+public interface ParagraphStyle
+{
+
+}
diff --git a/core/java/android/text/style/QuoteSpan.java b/core/java/android/text/style/QuoteSpan.java
new file mode 100644
index 0000000..3f4a32f
--- /dev/null
+++ b/core/java/android/text/style/QuoteSpan.java
@@ -0,0 +1,64 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.text.Layout;
+
+public class QuoteSpan
+implements LeadingMarginSpan
+{
+ private static final int STRIPE_WIDTH = 2;
+ private static final int GAP_WIDTH = 2;
+
+ private int mColor = 0xff0000ff;
+
+ public QuoteSpan() {
+ super();
+ }
+
+ public QuoteSpan(int color) {
+ this();
+ mColor = color;
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+
+ public int getLeadingMargin(boolean first) {
+ return STRIPE_WIDTH + GAP_WIDTH;
+ }
+
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
+ int top, int baseline, int bottom,
+ CharSequence text, int start, int end,
+ boolean first, Layout layout) {
+ Paint.Style style = p.getStyle();
+ int color = p.getColor();
+
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(mColor);
+
+ c.drawRect(x, top, x + dir * STRIPE_WIDTH, bottom, p);
+
+ p.setStyle(style);
+ p.setColor(color);
+ }
+}
diff --git a/core/java/android/text/style/RasterizerSpan.java b/core/java/android/text/style/RasterizerSpan.java
new file mode 100644
index 0000000..193c700
--- /dev/null
+++ b/core/java/android/text/style/RasterizerSpan.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2007 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Rasterizer;
+import android.text.TextPaint;
+
+public class RasterizerSpan extends CharacterStyle {
+
+ private Rasterizer mRasterizer;
+
+ public RasterizerSpan(Rasterizer r) {
+ mRasterizer = r;
+ }
+
+ public Rasterizer getRasterizer() {
+ return mRasterizer;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setRasterizer(mRasterizer);
+ }
+}
diff --git a/core/java/android/text/style/RelativeSizeSpan.java b/core/java/android/text/style/RelativeSizeSpan.java
new file mode 100644
index 0000000..a8ad076
--- /dev/null
+++ b/core/java/android/text/style/RelativeSizeSpan.java
@@ -0,0 +1,43 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class RelativeSizeSpan extends MetricAffectingSpan {
+
+ private float mProportion;
+
+ public RelativeSizeSpan(float proportion) {
+ mProportion = proportion;
+ }
+
+ public float getSizeChange() {
+ return mProportion;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setTextSize(ds.getTextSize() * mProportion);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ ds.setTextSize(ds.getTextSize() * mProportion);
+ }
+}
diff --git a/core/java/android/text/style/ReplacementSpan.java b/core/java/android/text/style/ReplacementSpan.java
new file mode 100644
index 0000000..26c725f
--- /dev/null
+++ b/core/java/android/text/style/ReplacementSpan.java
@@ -0,0 +1,43 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.text.TextPaint;
+
+public abstract class ReplacementSpan extends MetricAffectingSpan {
+
+ public abstract int getSize(Paint paint, CharSequence text,
+ int start, int end,
+ Paint.FontMetricsInt fm);
+ public abstract void draw(Canvas canvas, CharSequence text,
+ int start, int end, float x,
+ int top, int y, int bottom, Paint paint);
+
+ /**
+ * This method does nothing, since ReplacementSpans are measured
+ * explicitly instead of affecting Paint properties.
+ */
+ public void updateMeasureState(TextPaint p) { }
+
+ /**
+ * This method does nothing, since ReplacementSpans are drawn
+ * explicitly instead of affecting Paint properties.
+ */
+ public void updateDrawState(TextPaint ds) { }
+}
diff --git a/core/java/android/text/style/ScaleXSpan.java b/core/java/android/text/style/ScaleXSpan.java
new file mode 100644
index 0000000..ac9e35d
--- /dev/null
+++ b/core/java/android/text/style/ScaleXSpan.java
@@ -0,0 +1,43 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+public class ScaleXSpan extends MetricAffectingSpan {
+
+ private float mProportion;
+
+ public ScaleXSpan(float proportion) {
+ mProportion = proportion;
+ }
+
+ public float getScaleX() {
+ return mProportion;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setTextScaleX(ds.getTextScaleX() * mProportion);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ ds.setTextScaleX(ds.getTextScaleX() * mProportion);
+ }
+}
diff --git a/core/java/android/text/style/StrikethroughSpan.java b/core/java/android/text/style/StrikethroughSpan.java
new file mode 100644
index 0000000..01ae38c
--- /dev/null
+++ b/core/java/android/text/style/StrikethroughSpan.java
@@ -0,0 +1,27 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+
+public class StrikethroughSpan extends CharacterStyle {
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setStrikeThruText(true);
+ }
+}
diff --git a/core/java/android/text/style/StyleSpan.java b/core/java/android/text/style/StyleSpan.java
new file mode 100644
index 0000000..cc8b06c
--- /dev/null
+++ b/core/java/android/text/style/StyleSpan.java
@@ -0,0 +1,93 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+
+/**
+ *
+ * Describes a style in a span.
+ * Note that styles are cumulative -- if both bold and italic are set in
+ * separate spans, or if the base style is bold and a span calls for italic,
+ * you get bold italic. You can't turn off a style from the base style.
+ *
+ */
+public class StyleSpan extends MetricAffectingSpan {
+
+ private int mStyle;
+
+ /**
+ *
+ * @param style An integer constant describing the style for this span. Examples
+ * include bold, italic, and normal. Values are constants defined
+ * in {@link android.graphics.Typeface}.
+ */
+ public StyleSpan(int style) {
+ mStyle = style;
+ }
+
+ /**
+ * Returns the style constant defined in {@link android.graphics.Typeface}.
+ */
+ public int getStyle() {
+ return mStyle;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ apply(ds, mStyle);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint paint) {
+ apply(paint, mStyle);
+ }
+
+ private static void apply(Paint paint, int style) {
+ int oldStyle;
+
+ Typeface old = paint.getTypeface();
+ if (old == null) {
+ oldStyle = 0;
+ } else {
+ oldStyle = old.getStyle();
+ }
+
+ int want = oldStyle | style;
+
+ Typeface tf;
+ if (old == null) {
+ tf = Typeface.defaultFromStyle(want);
+ } else {
+ tf = Typeface.create(old, want);
+ }
+
+ int fake = want & ~tf.getStyle();
+
+ if ((fake & Typeface.BOLD) != 0) {
+ paint.setFakeBoldText(true);
+ }
+
+ if ((fake & Typeface.ITALIC) != 0) {
+ paint.setTextSkewX(-0.25f);
+ }
+
+ paint.setTypeface(tf);
+ }
+}
diff --git a/core/java/android/text/style/SubscriptSpan.java b/core/java/android/text/style/SubscriptSpan.java
new file mode 100644
index 0000000..78d6ba9
--- /dev/null
+++ b/core/java/android/text/style/SubscriptSpan.java
@@ -0,0 +1,31 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+
+public class SubscriptSpan extends MetricAffectingSpan {
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ tp.baselineShift -= (int) (tp.ascent() / 2);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint tp) {
+ tp.baselineShift -= (int) (tp.ascent() / 2);
+ }
+}
diff --git a/core/java/android/text/style/SuperscriptSpan.java b/core/java/android/text/style/SuperscriptSpan.java
new file mode 100644
index 0000000..79be4de
--- /dev/null
+++ b/core/java/android/text/style/SuperscriptSpan.java
@@ -0,0 +1,31 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+
+public class SuperscriptSpan extends MetricAffectingSpan {
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ tp.baselineShift += (int) (tp.ascent() / 2);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint tp) {
+ tp.baselineShift += (int) (tp.ascent() / 2);
+ }
+}
diff --git a/core/java/android/text/style/TabStopSpan.java b/core/java/android/text/style/TabStopSpan.java
new file mode 100644
index 0000000..e5b7644
--- /dev/null
+++ b/core/java/android/text/style/TabStopSpan.java
@@ -0,0 +1,37 @@
+/*
+ * 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.style;
+
+public interface TabStopSpan
+extends ParagraphStyle
+{
+ public int getTabStop();
+
+ public static class Standard
+ implements TabStopSpan
+ {
+ public Standard(int where) {
+ mTab = where;
+ }
+
+ public int getTabStop() {
+ return mTab;
+ }
+
+ private int mTab;
+ }
+}
diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java
new file mode 100644
index 0000000..c4ec976
--- /dev/null
+++ b/core/java/android/text/style/TextAppearanceSpan.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2008 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.style;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+
+/**
+ * Sets the text color, size, style, and typeface to match a TextAppearance
+ * resource.
+ */
+public class TextAppearanceSpan extends MetricAffectingSpan {
+ private String mTypeface;
+ private int mStyle;
+ private int mTextSize;
+ private ColorStateList mTextColor;
+ private ColorStateList mTextColorLink;
+
+ /**
+ * Uses the specified TextAppearance resource to determine the
+ * text appearance. The Provides classes used to view or change the style of a span of text in a View object.
+The classes with a subclass Standard are passed in to {@link android.text.SpannableString#setSpan(java.lang.Object, int, int, int)
+SpannableString.setSpan()} or {@link android.text.SpannableStringBuilder#setSpan(java.lang.Object, int, int, int)
+SpannableStringBuilder.setSpan()} to add a new styled span to a string in a View object.
+
+
+
diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java
new file mode 100644
index 0000000..79ecfbd
--- /dev/null
+++ b/core/java/android/text/util/Linkify.java
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2007 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.util;
+
+import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
+import android.text.style.URLSpan;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.webkit.WebView;
+import android.widget.TextView;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Linkify take a piece of text and a regular expression and turns all of the
+ * regex matches in the text into clickable links. This is particularly
+ * useful for matching things like email addresses, web urls, etc. and making
+ * them actionable.
+ *
+ * Alone with the pattern that is to be matched, a url scheme prefix is also
+ * required. Any pattern match that does not begin with the supplied scheme
+ * will have the scheme prepended to the matched text when the clickable url
+ * is created. For instance, if you are matching web urls you would supply
+ * the scheme dstart … dend
of dest
+ * with the new text from the range start … end
+ * of source
. Returns the CharSequence that we want
+ * placed there instead, including an empty string
+ * if appropriate, or null
to accept the original
+ * replacement. Be careful to not to reject 0-length replacements,
+ * as this is what happens when you delete text.
+ */
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ char[] out = new char[end - start]; // reserve enough space for whole string
+ int outidx = 0;
+ boolean changed = false;
+
+ onStart();
+
+ // Scan through beginning characters in dest, calling onInvalidCharacter()
+ // for each invalid character.
+ for (int i = 0; i < dstart; i++) {
+ char c = dest.charAt(i);
+ if (!isAllowed(c)) onInvalidCharacter(c);
+ }
+
+ // Scan through changed characters rejecting disallowed chars
+ for (int i = start; i < end; i++) {
+ char c = source.charAt(i);
+ if (isAllowed(c)) {
+ // Character allowed. Add it to the sequence.
+ out[outidx++] = c;
+ } else {
+ if (mAppendInvalid) out[outidx++] = c;
+ else changed = true; // we changed the original string
+ onInvalidCharacter(c);
+ }
+ }
+
+ // Scan through remaining characters in dest, calling onInvalidCharacter()
+ // for each invalid character.
+ for (int i = dend; i < dest.length(); i++) {
+ char c = dest.charAt(i);
+ if (!isAllowed(c)) onInvalidCharacter(c);
+ }
+
+ onStop();
+
+ return changed ? new String(out, 0, outidx) : null;
+ }
+
+ /**
+ * Called when we start processing filter.
+ */
+ public void onStart() {
+
+ }
+
+ /**
+ * Called whenever we encounter an invalid character.
+ * @param c the invalid character
+ */
+ public void onInvalidCharacter(char c) {
+
+ }
+
+ /**
+ * Called when we're done processing filter
+ */
+ public void onStop() {
+
+ }
+
+ /**
+ * Returns whether or not we allow character c.
+ * Subclasses must override this method.
+ */
+ public abstract boolean isAllowed(char c);
+
+ /**
+ * This filter rejects characters in the user name that are not compatible with GMail
+ * account creation. It prevents the user from entering user names with characters other than
+ * [a-zA-Z0-9.].
+ *
+ */
+ public static class UsernameFilterGMail extends LoginFilter {
+
+ public UsernameFilterGMail() {
+ super(false);
+ }
+
+ public UsernameFilterGMail(boolean appendInvalid) {
+ super(appendInvalid);
+ }
+
+ @Override
+ public boolean isAllowed(char c) {
+ // Allow [a-zA-Z0-9@.]
+ if ('0' <= c && c <= '9')
+ return true;
+ if ('a' <= c && c <= 'z')
+ return true;
+ if ('A' <= c && c <= 'Z')
+ return true;
+ if ('.' == c)
+ return true;
+ return false;
+ }
+ }
+
+ /**
+ * This filter rejects characters in the user name that are not compatible with Google login.
+ * It is slightly less restrictive than the above filter in that it allows [a-zA-Z0-9._-].
+ *
+ */
+ public static class UsernameFilterGeneric extends LoginFilter {
+ private static final String mAllowed = "@_-."; // Additional characters
+
+ public UsernameFilterGeneric() {
+ super(false);
+ }
+
+ public UsernameFilterGeneric(boolean appendInvalid) {
+ super(appendInvalid);
+ }
+
+ @Override
+ public boolean isAllowed(char c) {
+ // Allow [a-zA-Z0-9@.]
+ if ('0' <= c && c <= '9')
+ return true;
+ if ('a' <= c && c <= 'z')
+ return true;
+ if ('A' <= c && c <= 'Z')
+ return true;
+ if (mAllowed.indexOf(c) != -1)
+ return true;
+ return false;
+ }
+ }
+
+ /**
+ * This filter is compatible with GMail passwords which restricts characters to
+ * the Latin-1 (ISO8859-1) char set.
+ *
+ */
+ public static class PasswordFilterGMail extends LoginFilter {
+
+ public PasswordFilterGMail() {
+ super(false);
+ }
+
+ public PasswordFilterGMail(boolean appendInvalid) {
+ super(appendInvalid);
+ }
+
+ // We should reject anything not in the Latin-1 (ISO8859-1) charset
+ @Override
+ public boolean isAllowed(char c) {
+ if (32 <= c && c <= 127)
+ return true; // standard charset
+ // if (128 <= c && c <= 159) return true; // nonstandard (Windows(TM)(R)) charset
+ if (160 <= c && c <= 255)
+ return true; // extended charset
+ return false;
+ }
+ }
+}
diff --git a/core/java/android/text/PackedIntVector.java b/core/java/android/text/PackedIntVector.java
new file mode 100644
index 0000000..d87f600
--- /dev/null
+++ b/core/java/android/text/PackedIntVector.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2007 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 com.android.internal.util.ArrayUtils;
+
+
+/**
+ * PackedIntVector stores a two-dimensional array of integers,
+ * optimized for inserting and deleting rows and for
+ * offsetting the values in segments of a given column.
+ */
+class PackedIntVector {
+ private final int mColumns;
+ private int mRows;
+
+ private int mRowGapStart;
+ private int mRowGapLength;
+
+ private int[] mValues;
+ private int[] mValueGap; // starts, followed by lengths
+
+ /**
+ * Creates a new PackedIntVector with the specified width and
+ * a height of 0.
+ *
+ * @param columns the width of the PackedIntVector.
+ */
+ public PackedIntVector(int columns) {
+ mColumns = columns;
+ mRows = 0;
+
+ mRowGapStart = 0;
+ mRowGapLength = mRows;
+
+ mValues = null;
+ mValueGap = new int[2 * columns];
+ }
+
+ /**
+ * Returns the value at the specified row and column.
+ *
+ * @param row the index of the row to return.
+ * @param column the index of the column to return.
+ *
+ * @return the value stored at the specified position.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (row < 0 || row >= size()) or the column is out of range
+ * (column < 0 || column >= width()).
+ */
+ public int getValue(int row, int column) {
+ final int columns = mColumns;
+
+ if (((row | column) < 0) || (row >= size()) || (column >= columns)) {
+ throw new IndexOutOfBoundsException(row + ", " + column);
+ }
+
+ if (row >= mRowGapStart) {
+ row += mRowGapLength;
+ }
+
+ int value = mValues[row * columns + column];
+
+ int[] valuegap = mValueGap;
+ if (row >= valuegap[column]) {
+ value += valuegap[column + columns];
+ }
+
+ return value;
+ }
+
+ /**
+ * Sets the value at the specified row and column.
+ *
+ * @param row the index of the row to set.
+ * @param column the index of the column to set.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (row < 0 || row >= size()) or the column is out of range
+ * (column < 0 || column >= width()).
+ */
+ public void setValue(int row, int column, int value) {
+ if (((row | column) < 0) || (row >= size()) || (column >= mColumns)) {
+ throw new IndexOutOfBoundsException(row + ", " + column);
+ }
+
+ if (row >= mRowGapStart) {
+ row += mRowGapLength;
+ }
+
+ int[] valuegap = mValueGap;
+ if (row >= valuegap[column]) {
+ value -= valuegap[column + mColumns];
+ }
+
+ mValues[row * mColumns + column] = value;
+ }
+
+ /**
+ * Sets the value at the specified row and column.
+ * Private internal version: does not check args.
+ *
+ * @param row the index of the row to set.
+ * @param column the index of the column to set.
+ *
+ */
+ private void setValueInternal(int row, int column, int value) {
+ if (row >= mRowGapStart) {
+ row += mRowGapLength;
+ }
+
+ int[] valuegap = mValueGap;
+ if (row >= valuegap[column]) {
+ value -= valuegap[column + mColumns];
+ }
+
+ mValues[row * mColumns + column] = value;
+ }
+
+
+ /**
+ * Increments all values in the specified column whose row >= the
+ * specified row by the specified delta.
+ *
+ * @param startRow the row at which to begin incrementing.
+ * This may be == size(), which case there is no effect.
+ * @param column the index of the column to set.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (startRow < 0 || startRow > size()) or the column
+ * is out of range (column < 0 || column >= width()).
+ */
+ public void adjustValuesBelow(int startRow, int column, int delta) {
+ if (((startRow | column) < 0) || (startRow > size()) ||
+ (column >= width())) {
+ throw new IndexOutOfBoundsException(startRow + ", " + column);
+ }
+
+ if (startRow >= mRowGapStart) {
+ startRow += mRowGapLength;
+ }
+
+ moveValueGapTo(column, startRow);
+ mValueGap[column + mColumns] += delta;
+ }
+
+ /**
+ * Inserts a new row of values at the specified row offset.
+ *
+ * @param row the row above which to insert the new row.
+ * This may be == size(), which case the new row is added
+ * at the end.
+ * @param values the new values to be added. If this is null,
+ * a row of zeroes is added.
+ *
+ * @throws IndexOutOfBoundsException if the row is out of range
+ * (row < 0 || row > size()) or if the length of the
+ * values array is too small (values.length < width()).
+ */
+ public void insertAt(int row, int[] values) {
+ if ((row < 0) || (row > size())) {
+ throw new IndexOutOfBoundsException("row " + row);
+ }
+
+ if ((values != null) && (values.length < width())) {
+ throw new IndexOutOfBoundsException("value count " + values.length);
+ }
+
+ moveRowGapTo(row);
+
+ if (mRowGapLength == 0) {
+ growBuffer();
+ }
+
+ mRowGapStart++;
+ mRowGapLength--;
+
+ if (values == null) {
+ for (int i = mColumns - 1; i >= 0; i--) {
+ setValueInternal(row, i, 0);
+ }
+ } else {
+ for (int i = mColumns - 1; i >= 0; i--) {
+ setValueInternal(row, i, values[i]);
+ }
+ }
+ }
+
+ /**
+ * Deletes the specified number of rows starting with the specified
+ * row.
+ *
+ * @param row the index of the first row to be deleted.
+ * @param count the number of rows to delete.
+ *
+ * @throws IndexOutOfBoundsException if any of the rows to be deleted
+ * are out of range (row < 0 || count < 0 ||
+ * row + count > size()).
+ */
+ public void deleteAt(int row, int count) {
+ if (((row | count) < 0) || (row + count > size())) {
+ throw new IndexOutOfBoundsException(row + ", " + count);
+ }
+
+ moveRowGapTo(row + count);
+
+ mRowGapStart -= count;
+ mRowGapLength += count;
+
+ // TODO: Reclaim memory when the new height is much smaller
+ // than the allocated size.
+ }
+
+ /**
+ * Returns the number of rows in the PackedIntVector. This number
+ * will change as rows are inserted and deleted.
+ *
+ * @return the number of rows.
+ */
+ public int size() {
+ return mRows - mRowGapLength;
+ }
+
+ /**
+ * Returns the width of the PackedIntVector. This number is set
+ * at construction and will not change.
+ *
+ * @return the number of columns.
+ */
+ public int width() {
+ return mColumns;
+ }
+
+ /**
+ * Grows the value and gap arrays to be large enough to store at least
+ * one more than the current number of rows.
+ */
+ private final void growBuffer() {
+ final int columns = mColumns;
+ int newsize = size() + 1;
+ newsize = ArrayUtils.idealIntArraySize(newsize * columns) / columns;
+ int[] newvalues = new int[newsize * columns];
+
+ final int[] valuegap = mValueGap;
+ final int rowgapstart = mRowGapStart;
+
+ int after = mRows - (rowgapstart + mRowGapLength);
+
+ if (mValues != null) {
+ System.arraycopy(mValues, 0, newvalues, 0, columns * rowgapstart);
+ System.arraycopy(mValues, (mRows - after) * columns,
+ newvalues, (newsize - after) * columns,
+ after * columns);
+ }
+
+ for (int i = 0; i < columns; i++) {
+ if (valuegap[i] >= rowgapstart) {
+ valuegap[i] += newsize - mRows;
+
+ if (valuegap[i] < rowgapstart) {
+ valuegap[i] = rowgapstart;
+ }
+ }
+ }
+
+ mRowGapLength += newsize - mRows;
+ mRows = newsize;
+ mValues = newvalues;
+ }
+
+ /**
+ * Moves the gap in the values of the specified column to begin at
+ * the specified row.
+ */
+ private final void moveValueGapTo(int column, int where) {
+ final int[] valuegap = mValueGap;
+ final int[] values = mValues;
+ final int columns = mColumns;
+
+ if (where == valuegap[column]) {
+ return;
+ } else if (where > valuegap[column]) {
+ for (int i = valuegap[column]; i < where; i++) {
+ values[i * columns + column] += valuegap[column + columns];
+ }
+ } else /* where < valuegap[column] */ {
+ for (int i = where; i < valuegap[column]; i++) {
+ values[i * columns + column] -= valuegap[column + columns];
+ }
+ }
+
+ valuegap[column] = where;
+ }
+
+ /**
+ * Moves the gap in the row indices to begin at the specified row.
+ */
+ private final void moveRowGapTo(int where) {
+ if (where == mRowGapStart) {
+ return;
+ } else if (where > mRowGapStart) {
+ int moving = where + mRowGapLength - (mRowGapStart + mRowGapLength);
+ final int columns = mColumns;
+ final int[] valuegap = mValueGap;
+ final int[] values = mValues;
+ final int gapend = mRowGapStart + mRowGapLength;
+
+ for (int i = gapend; i < gapend + moving; i++) {
+ int destrow = i - gapend + mRowGapStart;
+
+ for (int j = 0; j < columns; j++) {
+ int val = values[i * columns+ j];
+
+ if (i >= valuegap[j]) {
+ val += valuegap[j + columns];
+ }
+
+ if (destrow >= valuegap[j]) {
+ val -= valuegap[j + columns];
+ }
+
+ values[destrow * columns + j] = val;
+ }
+ }
+ } else /* where < mRowGapStart */ {
+ int moving = mRowGapStart - where;
+ final int columns = mColumns;
+ final int[] valuegap = mValueGap;
+ final int[] values = mValues;
+ final int gapend = mRowGapStart + mRowGapLength;
+
+ for (int i = where + moving - 1; i >= where; i--) {
+ int destrow = i - where + gapend - moving;
+
+ for (int j = 0; j < columns; j++) {
+ int val = values[i * columns+ j];
+
+ if (i >= valuegap[j]) {
+ val += valuegap[j + columns];
+ }
+
+ if (destrow >= valuegap[j]) {
+ val -= valuegap[j + columns];
+ }
+
+ values[destrow * columns + j] = val;
+ }
+ }
+ }
+
+ mRowGapStart = where;
+ }
+}
diff --git a/core/java/android/text/PackedObjectVector.java b/core/java/android/text/PackedObjectVector.java
new file mode 100644
index 0000000..a29df09
--- /dev/null
+++ b/core/java/android/text/PackedObjectVector.java
@@ -0,0 +1,188 @@
+/*
+ * 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 com.android.internal.util.ArrayUtils;
+
+class PackedObjectVectorstart
and the selection edge
+ * to stop
.
+ */
+ public static void setSelection(Spannable text, int start, int stop) {
+ // int len = text.length();
+ // start = pin(start, 0, len); XXX remove unless we really need it
+ // stop = pin(stop, 0, len);
+
+ int ostart = getSelectionStart(text);
+ int oend = getSelectionEnd(text);
+
+ if (ostart != start || oend != stop) {
+ text.setSpan(SELECTION_START, start, start,
+ Spanned.SPAN_POINT_POINT);
+ text.setSpan(SELECTION_END, stop, stop,
+ Spanned.SPAN_POINT_POINT);
+ }
+ }
+
+ /**
+ * Move the cursor to offset index
.
+ */
+ public static final void setSelection(Spannable text, int index) {
+ setSelection(text, index, index);
+ }
+
+ /**
+ * Select the entire text.
+ */
+ public static final void selectAll(Spannable text) {
+ setSelection(text, 0, text.length());
+ }
+
+ /**
+ * Move the selection edge to offset index
.
+ */
+ public static final void extendSelection(Spannable text, int index) {
+ if (text.getSpanStart(SELECTION_END) != index)
+ text.setSpan(SELECTION_END, index, index, Spanned.SPAN_POINT_POINT);
+ }
+
+ /**
+ * Remove the selection or cursor, if any, from the text.
+ */
+ public static final void removeSelection(Spannable text) {
+ text.removeSpan(SELECTION_START);
+ text.removeSpan(SELECTION_END);
+ }
+
+ /*
+ * Moving the selection within the layout
+ */
+
+ /**
+ * Move the cursor to the buffer offset physically above the current
+ * offset, or return false if the cursor is already on the top line.
+ */
+ public static boolean moveUp(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ int min = Math.min(start, end);
+ int max = Math.max(start, end);
+
+ setSelection(text, min);
+
+ if (min == 0 && max == text.length()) {
+ return false;
+ }
+
+ return true;
+ } else {
+ int line = layout.getLineForOffset(end);
+
+ if (line > 0) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line - 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line - 1, h);
+ } else {
+ move = layout.getLineStart(line - 1);
+ }
+
+ setSelection(text, move);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the cursor to the buffer offset physically below the current
+ * offset, or return false if the cursor is already on the bottom line.
+ */
+ public static boolean moveDown(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ int min = Math.min(start, end);
+ int max = Math.max(start, end);
+
+ setSelection(text, max);
+
+ if (min == 0 && max == text.length()) {
+ return false;
+ }
+
+ return true;
+ } else {
+ int line = layout.getLineForOffset(end);
+
+ if (line < layout.getLineCount() - 1) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line + 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line + 1, h);
+ } else {
+ move = layout.getLineStart(line + 1);
+ }
+
+ setSelection(text, move);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the cursor to the buffer offset physically to the left of
+ * the current offset, or return false if the cursor is already
+ * at the left edge of the line and there is not another line to move it to.
+ */
+ public static boolean moveLeft(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ setSelection(text, chooseHorizontal(layout, -1, start, end));
+ return true;
+ } else {
+ int to = layout.getOffsetToLeftOf(end);
+
+ if (to != end) {
+ setSelection(text, to);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the cursor to the buffer offset physically to the right of
+ * the current offset, or return false if the cursor is already at
+ * at the right edge of the line and there is not another line
+ * to move it to.
+ */
+ public static boolean moveRight(Spannable text, Layout layout) {
+ int start = getSelectionStart(text);
+ int end = getSelectionEnd(text);
+
+ if (start != end) {
+ setSelection(text, chooseHorizontal(layout, 1, start, end));
+ return true;
+ } else {
+ int to = layout.getOffsetToRightOf(end);
+
+ if (to != end) {
+ setSelection(text, to);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically above
+ * the current selection end.
+ */
+ public static boolean extendUp(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int line = layout.getLineForOffset(end);
+
+ if (line > 0) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line - 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line - 1, h);
+ } else {
+ move = layout.getLineStart(line - 1);
+ }
+
+ extendSelection(text, move);
+ return true;
+ } else if (end != 0) {
+ extendSelection(text, 0);
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically below
+ * the current selection end.
+ */
+ public static boolean extendDown(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int line = layout.getLineForOffset(end);
+
+ if (line < layout.getLineCount() - 1) {
+ int move;
+
+ if (layout.getParagraphDirection(line) ==
+ layout.getParagraphDirection(line + 1)) {
+ float h = layout.getPrimaryHorizontal(end);
+ move = layout.getOffsetForHorizontal(line + 1, h);
+ } else {
+ move = layout.getLineStart(line + 1);
+ }
+
+ extendSelection(text, move);
+ return true;
+ } else if (end != text.length()) {
+ extendSelection(text, text.length());
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically to the left of
+ * the current selection end.
+ */
+ public static boolean extendLeft(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int to = layout.getOffsetToLeftOf(end);
+
+ if (to != end) {
+ extendSelection(text, to);
+ return true;
+ }
+
+ return true;
+ }
+
+ /**
+ * Move the selection end to the buffer offset physically to the right of
+ * the current selection end.
+ */
+ public static boolean extendRight(Spannable text, Layout layout) {
+ int end = getSelectionEnd(text);
+ int to = layout.getOffsetToRightOf(end);
+
+ if (to != end) {
+ extendSelection(text, to);
+ return true;
+ }
+
+ return true;
+ }
+
+ public static boolean extendToLeftEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, -1);
+ extendSelection(text, where);
+ return true;
+ }
+
+ public static boolean extendToRightEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, 1);
+ extendSelection(text, where);
+ return true;
+ }
+
+ public static boolean moveToLeftEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, -1);
+ setSelection(text, where);
+ return true;
+ }
+
+ public static boolean moveToRightEdge(Spannable text, Layout layout) {
+ int where = findEdge(text, layout, 1);
+ setSelection(text, where);
+ return true;
+ }
+
+ private static int findEdge(Spannable text, Layout layout, int dir) {
+ int pt = getSelectionEnd(text);
+ int line = layout.getLineForOffset(pt);
+ int pdir = layout.getParagraphDirection(line);
+
+ if (dir * pdir < 0) {
+ return layout.getLineStart(line);
+ } else {
+ int end = layout.getLineEnd(line);
+
+ if (line == layout.getLineCount() - 1)
+ return end;
+ else
+ return end - 1;
+ }
+ }
+
+ private static int chooseHorizontal(Layout layout, int direction,
+ int off1, int off2) {
+ int line1 = layout.getLineForOffset(off1);
+ int line2 = layout.getLineForOffset(off2);
+
+ if (line1 == line2) {
+ // same line, so it goes by pure physical direction
+
+ float h1 = layout.getPrimaryHorizontal(off1);
+ float h2 = layout.getPrimaryHorizontal(off2);
+
+ if (direction < 0) {
+ // to left
+
+ if (h1 < h2)
+ return off1;
+ else
+ return off2;
+ } else {
+ // to right
+
+ if (h1 > h2)
+ return off1;
+ else
+ return off2;
+ }
+ } else {
+ // different line, so which line is "left" and which is "right"
+ // depends upon the directionality of the text
+
+ // This only checks at one end, but it's not clear what the
+ // right thing to do is if the ends don't agree. Even if it
+ // is wrong it should still not be too bad.
+ int line = layout.getLineForOffset(off1);
+ int textdir = layout.getParagraphDirection(line);
+
+ if (textdir == direction)
+ return Math.max(off1, off2);
+ else
+ return Math.min(off1, off2);
+ }
+ }
+
+ /*
+ * Public constants
+ */
+
+ public static final Object SELECTION_START = new Object();
+ public static final Object SELECTION_END = new Object();
+}
diff --git a/core/java/android/text/SpanWatcher.java b/core/java/android/text/SpanWatcher.java
new file mode 100644
index 0000000..f99882a
--- /dev/null
+++ b/core/java/android/text/SpanWatcher.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+/**
+ * When an object of this type is attached to a Spannable, its methods
+ * will be called to notify it that other markup objects have been
+ * added, changed, or removed.
+ */
+public interface SpanWatcher {
+ /**
+ * This method is called to notify you that the specified object
+ * has been attached to the specified range of the text.
+ */
+ public void onSpanAdded(Spannable text, Object what, int start, int end);
+ /**
+ * This method is called to notify you that the specified object
+ * has been detached from the specified range of the text.
+ */
+ public void onSpanRemoved(Spannable text, Object what, int start, int end);
+ /**
+ * This method is called to notify you that the specified object
+ * has been relocated from the range ostart…oend
+ * to the new range nstart…nend
of the text.
+ */
+ public void onSpanChanged(Spannable text, Object what, int ostart, int oend,
+ int nstart, int nend);
+}
diff --git a/core/java/android/text/Spannable.java b/core/java/android/text/Spannable.java
new file mode 100644
index 0000000..ae5d356
--- /dev/null
+++ b/core/java/android/text/Spannable.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+/**
+ * This is the interface for text to which markup objects can be
+ * attached and detached. Not all Spannable classes have mutable text;
+ * see {@link Editable} for that.
+ */
+public interface Spannable
+extends Spanned
+{
+ /**
+ * Attach the specified markup object to the range start…end
+ * of the text, or move the object to that range if it was already
+ * attached elsewhere. See {@link Spanned} for an explanation of
+ * what the flags mean. The object can be one that has meaning only
+ * within your application, or it can be one that the text system will
+ * use to affect text display or behavior. Some noteworthy ones are
+ * the subclasses of {@link android.text.style.CharacterStyle} and
+ * {@link android.text.style.ParagraphStyle}, and
+ * {@link android.text.TextWatcher} and
+ * {@link android.text.SpanWatcher}.
+ */
+ public void setSpan(Object what, int start, int end, int flags);
+
+ /**
+ * Remove the specified object from the range of text to which it
+ * was attached, if any. It is OK to remove an object that was never
+ * attached in the first place.
+ */
+ public void removeSpan(Object what);
+
+ /**
+ * Factory used by TextView to create new Spannables. You can subclass
+ * it to provide something other than SpannableString.
+ */
+ public static class Factory {
+ private static Spannable.Factory sInstance = new Spannable.Factory();
+
+ /**
+ * Returns the standard Spannable Factory.
+ */
+ public static Spannable.Factory getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Returns a new SpannableString from the specified CharSequence.
+ * You can override this to provide a different kind of Spannable.
+ */
+ public Spannable newSpannable(CharSequence source) {
+ return new SpannableString(source);
+ }
+ }
+}
diff --git a/core/java/android/text/SpannableString.java b/core/java/android/text/SpannableString.java
new file mode 100644
index 0000000..56d0946
--- /dev/null
+++ b/core/java/android/text/SpannableString.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+
+/**
+ * This is the class for text whose content is immutable but to which
+ * markup objects can be attached and detached.
+ * For mutable text, see {@link SpannableStringBuilder}.
+ */
+public class SpannableString
+extends SpannableStringInternal
+implements CharSequence, GetChars, Spannable
+{
+ public SpannableString(CharSequence source) {
+ super(source, 0, source.length());
+ }
+
+ private SpannableString(CharSequence source, int start, int end) {
+ super(source, start, end);
+ }
+
+ public static SpannableString valueOf(CharSequence source) {
+ if (source instanceof SpannableString) {
+ return (SpannableString) source;
+ } else {
+ return new SpannableString(source);
+ }
+ }
+
+ public void setSpan(Object what, int start, int end, int flags) {
+ super.setSpan(what, start, end, flags);
+ }
+
+ public void removeSpan(Object what) {
+ super.removeSpan(what);
+ }
+
+ public final CharSequence subSequence(int start, int end) {
+ return new SpannableString(this, start, end);
+ }
+}
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
new file mode 100644
index 0000000..223ce2f
--- /dev/null
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -0,0 +1,1136 @@
+/*
+ * 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 com.android.internal.util.ArrayUtils;
+import android.graphics.Paint;
+import android.graphics.Canvas;
+
+import java.lang.reflect.Array;
+
+/**
+ * This is the class for text whose content and markup can both be changed.
+ */
+public class SpannableStringBuilder
+implements CharSequence, GetChars, Spannable, Editable, Appendable,
+ GraphicsOperations
+{
+ /**
+ * Create a new SpannableStringBuilder with empty contents
+ */
+ public SpannableStringBuilder() {
+ this("");
+ }
+
+ /**
+ * Create a new SpannableStringBuilder containing a copy of the
+ * specified text, including its spans if any.
+ */
+ public SpannableStringBuilder(CharSequence text) {
+ this(text, 0, text.length());
+ }
+
+ /**
+ * Create a new SpannableStringBuilder containing a copy of the
+ * specified slice of the specified text, including its spans if any.
+ */
+ public SpannableStringBuilder(CharSequence text, int start, int end) {
+ int srclen = end - start;
+
+ int len = ArrayUtils.idealCharArraySize(srclen + 1);
+ mText = new char[len];
+ mGapStart = srclen;
+ mGapLength = len - srclen;
+
+ TextUtils.getChars(text, start, end, mText, 0);
+
+ mSpanCount = 0;
+ int alloc = ArrayUtils.idealIntArraySize(0);
+ mSpans = new Object[alloc];
+ mSpanStarts = new int[alloc];
+ mSpanEnds = new int[alloc];
+ mSpanFlags = new int[alloc];
+
+ if (text instanceof Spanned) {
+ Spanned sp = (Spanned) text;
+ Object[] spans = sp.getSpans(start, end, Object.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = sp.getSpanStart(spans[i]) - start;
+ int en = sp.getSpanEnd(spans[i]) - start;
+ int fl = sp.getSpanFlags(spans[i]);
+
+ if (st < 0)
+ st = 0;
+ if (st > end - start)
+ st = end - start;
+
+ if (en < 0)
+ en = 0;
+ if (en > end - start)
+ en = end - start;
+
+ setSpan(spans[i], st, en, fl);
+ }
+ }
+ }
+
+ public static SpannableStringBuilder valueOf(CharSequence source) {
+ if (source instanceof SpannableStringBuilder) {
+ return (SpannableStringBuilder) source;
+ } else {
+ return new SpannableStringBuilder(source);
+ }
+ }
+
+ /**
+ * Return the char at the specified offset within the buffer.
+ */
+ public char charAt(int where) {
+ int len = length();
+ if (where < 0) {
+ throw new IndexOutOfBoundsException("charAt: " + where + " < 0");
+ } else if (where >= len) {
+ throw new IndexOutOfBoundsException("charAt: " + where +
+ " >= length " + len);
+ }
+
+ if (where >= mGapStart)
+ return mText[where + mGapLength];
+ else
+ return mText[where];
+ }
+
+ /**
+ * Return the number of chars in the buffer.
+ */
+ public int length() {
+ return mText.length - mGapLength;
+ }
+
+ private void resizeFor(int size) {
+ int newlen = ArrayUtils.idealCharArraySize(size + 1);
+ char[] newtext = new char[newlen];
+
+ int after = mText.length - (mGapStart + mGapLength);
+
+ System.arraycopy(mText, 0, newtext, 0, mGapStart);
+ System.arraycopy(mText, mText.length - after,
+ newtext, newlen - after, after);
+
+ for (int i = 0; i < mSpanCount; i++) {
+ if (mSpanStarts[i] > mGapStart)
+ mSpanStarts[i] += newlen - mText.length;
+ if (mSpanEnds[i] > mGapStart)
+ mSpanEnds[i] += newlen - mText.length;
+ }
+
+ int oldlen = mText.length;
+ mText = newtext;
+ mGapLength += mText.length - oldlen;
+
+ if (mGapLength < 1)
+ new Exception("mGapLength < 1").printStackTrace();
+ }
+
+ private void moveGapTo(int where) {
+ if (where == mGapStart)
+ return;
+
+ boolean atend = (where == length());
+
+ if (where < mGapStart) {
+ int overlap = mGapStart - where;
+
+ System.arraycopy(mText, where,
+ mText, mGapStart + mGapLength - overlap, overlap);
+ } else /* where > mGapStart */ {
+ int overlap = where - mGapStart;
+
+ System.arraycopy(mText, where + mGapLength - overlap,
+ mText, mGapStart, overlap);
+ }
+
+ // XXX be more clever
+ for (int i = 0; i < mSpanCount; i++) {
+ int start = mSpanStarts[i];
+ int end = mSpanEnds[i];
+
+ if (start > mGapStart)
+ start -= mGapLength;
+ if (start > where)
+ start += mGapLength;
+ else if (start == where) {
+ int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT;
+
+ if (flag == POINT || (atend && flag == PARAGRAPH))
+ start += mGapLength;
+ }
+
+ if (end > mGapStart)
+ end -= mGapLength;
+ if (end > where)
+ end += mGapLength;
+ else if (end == where) {
+ int flag = (mSpanFlags[i] & END_MASK);
+
+ if (flag == POINT || (atend && flag == PARAGRAPH))
+ end += mGapLength;
+ }
+
+ mSpanStarts[i] = start;
+ mSpanEnds[i] = end;
+ }
+
+ mGapStart = where;
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
+ return replace(where, where, tb, start, end);
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder insert(int where, CharSequence tb) {
+ return replace(where, where, tb, 0, tb.length());
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder delete(int start, int end) {
+ SpannableStringBuilder ret = replace(start, end, "", 0, 0);
+
+ if (mGapLength > 2 * length())
+ resizeFor(length());
+
+ return ret; // == this
+ }
+
+ // Documentation from interface
+ public void clear() {
+ replace(0, length(), "", 0, 0);
+ }
+
+ // Documentation from interface
+ public void clearSpans() {
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ Object what = mSpans[i];
+ int ostart = mSpanStarts[i];
+ int oend = mSpanEnds[i];
+
+ if (ostart > mGapStart)
+ ostart -= mGapLength;
+ if (oend > mGapStart)
+ oend -= mGapLength;
+
+ mSpanCount = i;
+ mSpans[i] = null;
+
+ sendSpanRemoved(what, ostart, oend);
+ }
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder append(CharSequence text) {
+ int length = length();
+ return replace(length, length, text, 0, text.length());
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder append(CharSequence text, int start, int end) {
+ int length = length();
+ return replace(length, length, text, start, end);
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder append(char text) {
+ return append(String.valueOf(text));
+ }
+
+ private int change(int start, int end,
+ CharSequence tb, int tbstart, int tbend) {
+ return change(true, start, end, tb, tbstart, tbend);
+ }
+
+ private int change(boolean notify, int start, int end,
+ CharSequence tb, int tbstart, int tbend) {
+ checkRange("replace", start, end);
+ int ret = tbend - tbstart;
+ TextWatcher[] recipients = null;
+
+ if (notify)
+ recipients = sendTextWillChange(start, end - start,
+ tbend - tbstart);
+
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) {
+ int st = mSpanStarts[i];
+ if (st > mGapStart)
+ st -= mGapLength;
+
+ int en = mSpanEnds[i];
+ if (en > mGapStart)
+ en -= mGapLength;
+
+ int ost = st;
+ int oen = en;
+ int clen = length();
+
+ if (st > start && st <= end) {
+ for (st = end; st < clen; st++)
+ if (st > end && charAt(st - 1) == '\n')
+ break;
+ }
+
+ if (en > start && en <= end) {
+ for (en = end; en < clen; en++)
+ if (en > end && charAt(en - 1) == '\n')
+ break;
+ }
+
+ if (st != ost || en != oen)
+ setSpan(mSpans[i], st, en, mSpanFlags[i]);
+ }
+ }
+
+ moveGapTo(end);
+
+ if (tbend - tbstart >= mGapLength + (end - start))
+ resizeFor(mText.length - mGapLength +
+ tbend - tbstart - (end - start));
+
+ mGapStart += tbend - tbstart - (end - start);
+ mGapLength -= tbend - tbstart - (end - start);
+
+ if (mGapLength < 1)
+ new Exception("mGapLength < 1").printStackTrace();
+
+ TextUtils.getChars(tb, tbstart, tbend, mText, start);
+
+ if (tb instanceof Spanned) {
+ Spanned sp = (Spanned) tb;
+ Object[] spans = sp.getSpans(tbstart, tbend, Object.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = sp.getSpanStart(spans[i]);
+ int en = sp.getSpanEnd(spans[i]);
+
+ if (st < tbstart)
+ st = tbstart;
+ if (en > tbend)
+ en = tbend;
+
+ if (getSpanStart(spans[i]) < 0) {
+ setSpan(false, spans[i],
+ st - tbstart + start,
+ en - tbstart + start,
+ sp.getSpanFlags(spans[i]));
+ }
+ }
+ }
+
+ // no need for span fixup on pure insertion
+ if (tbend > tbstart && end - start == 0) {
+ if (notify) {
+ sendTextChange(recipients, start, end - start, tbend - tbstart);
+ sendTextHasChanged(recipients);
+ }
+
+ return ret;
+ }
+
+ boolean atend = (mGapStart + mGapLength == mText.length);
+
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if (mSpanStarts[i] >= start &&
+ mSpanStarts[i] < mGapStart + mGapLength) {
+ int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT;
+
+ if (flag == POINT || (flag == PARAGRAPH && atend))
+ mSpanStarts[i] = mGapStart + mGapLength;
+ else
+ mSpanStarts[i] = start;
+ }
+
+ if (mSpanEnds[i] >= start &&
+ mSpanEnds[i] < mGapStart + mGapLength) {
+ int flag = (mSpanFlags[i] & END_MASK);
+
+ if (flag == POINT || (flag == PARAGRAPH && atend))
+ mSpanEnds[i] = mGapStart + mGapLength;
+ else
+ mSpanEnds[i] = start;
+ }
+
+ // remove 0-length SPAN_EXCLUSIVE_EXCLUSIVE
+ // XXX send notification on removal
+
+ if (mSpanEnds[i] < mSpanStarts[i]) {
+ System.arraycopy(mSpans, i + 1,
+ mSpans, i, mSpanCount - (i + 1));
+ System.arraycopy(mSpanStarts, i + 1,
+ mSpanStarts, i, mSpanCount - (i + 1));
+ System.arraycopy(mSpanEnds, i + 1,
+ mSpanEnds, i, mSpanCount - (i + 1));
+ System.arraycopy(mSpanFlags, i + 1,
+ mSpanFlags, i, mSpanCount - (i + 1));
+
+ mSpanCount--;
+ }
+ }
+
+ if (notify) {
+ sendTextChange(recipients, start, end - start, tbend - tbstart);
+ sendTextHasChanged(recipients);
+ }
+
+ return ret;
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
+ return replace(start, end, tb, 0, tb.length());
+ }
+
+ // Documentation from interface
+ public SpannableStringBuilder replace(final int start, final int end,
+ CharSequence tb, int tbstart, int tbend) {
+ int filtercount = mFilters.length;
+ for (int i = 0; i < filtercount; i++) {
+ CharSequence repl = mFilters[i].filter(tb, tbstart, tbend,
+ this, start, end);
+
+ if (repl != null) {
+ tb = repl;
+ tbstart = 0;
+ tbend = repl.length();
+ }
+ }
+
+ if (end == start && tbstart == tbend) {
+ return this;
+ }
+
+ if (end == start || tbstart == tbend) {
+ change(start, end, tb, tbstart, tbend);
+ } else {
+ int selstart = Selection.getSelectionStart(this);
+ int selend = Selection.getSelectionEnd(this);
+
+ // XXX just make the span fixups in change() do the right thing
+ // instead of this madness!
+
+ checkRange("replace", start, end);
+ moveGapTo(end);
+ TextWatcher[] recipients;
+
+ recipients = sendTextWillChange(start, end - start,
+ tbend - tbstart);
+
+ int origlen = end - start;
+
+ if (mGapLength < 2)
+ resizeFor(length() + 1);
+
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if (mSpanStarts[i] == mGapStart)
+ mSpanStarts[i]++;
+
+ if (mSpanEnds[i] == mGapStart)
+ mSpanEnds[i]++;
+ }
+
+ mText[mGapStart] = ' ';
+ mGapStart++;
+ mGapLength--;
+
+ if (mGapLength < 1)
+ new Exception("mGapLength < 1").printStackTrace();
+
+ int oldlen = (end + 1) - start;
+
+ int inserted = change(false, start + 1, start + 1,
+ tb, tbstart, tbend);
+ change(false, start, start + 1, "", 0, 0);
+ change(false, start + inserted, start + inserted + oldlen - 1,
+ "", 0, 0);
+
+ /*
+ * Special case to keep the cursor in the same position
+ * if it was somewhere in the middle of the replaced region.
+ * If it was at the start or the end or crossing the whole
+ * replacement, it should already be where it belongs.
+ * TODO: Is there some more general mechanism that could
+ * accomplish this?
+ */
+ if (selstart > start && selstart < end) {
+ long off = selstart - start;
+
+ off = off * inserted / (end - start);
+ selstart = (int) off + start;
+
+ setSpan(false, Selection.SELECTION_START, selstart, selstart,
+ Spanned.SPAN_POINT_POINT);
+ }
+ if (selend > start && selend < end) {
+ long off = selend - start;
+
+ off = off * inserted / (end - start);
+ selend = (int) off + start;
+
+ setSpan(false, Selection.SELECTION_END, selend, selend,
+ Spanned.SPAN_POINT_POINT);
+ }
+
+ sendTextChange(recipients, start, origlen, inserted);
+ sendTextHasChanged(recipients);
+ }
+ return this;
+ }
+
+ /**
+ * Mark the specified range of text with the specified object.
+ * The flags determine how the span will behave when text is
+ * inserted at the start or end of the span's range.
+ */
+ public void setSpan(Object what, int start, int end, int flags) {
+ setSpan(true, what, start, end, flags);
+ }
+
+ private void setSpan(boolean send,
+ Object what, int start, int end, int flags) {
+ int nstart = start;
+ int nend = end;
+
+ checkRange("setSpan", start, end);
+
+ if ((flags & START_MASK) == (PARAGRAPH << START_SHIFT)) {
+ if (start != 0 && start != length()) {
+ char c = charAt(start - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must start at paragraph boundary");
+ }
+ }
+
+ if ((flags & END_MASK) == PARAGRAPH) {
+ if (end != 0 && end != length()) {
+ char c = charAt(end - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must end at paragraph boundary");
+ }
+ }
+
+ if (start > mGapStart)
+ start += mGapLength;
+ else if (start == mGapStart) {
+ int flag = (flags & START_MASK) >> START_SHIFT;
+
+ if (flag == POINT || (flag == PARAGRAPH && start == length()))
+ start += mGapLength;
+ }
+
+ if (end > mGapStart)
+ end += mGapLength;
+ else if (end == mGapStart) {
+ int flag = (flags & END_MASK);
+
+ if (flag == POINT || (flag == PARAGRAPH && end == length()))
+ end += mGapLength;
+ }
+
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = 0; i < count; i++) {
+ if (spans[i] == what) {
+ int ostart = mSpanStarts[i];
+ int oend = mSpanEnds[i];
+
+ if (ostart > mGapStart)
+ ostart -= mGapLength;
+ if (oend > mGapStart)
+ oend -= mGapLength;
+
+ mSpanStarts[i] = start;
+ mSpanEnds[i] = end;
+ mSpanFlags[i] = flags;
+
+ if (send)
+ sendSpanChanged(what, ostart, oend, nstart, nend);
+
+ return;
+ }
+ }
+
+ if (mSpanCount + 1 >= mSpans.length) {
+ int newsize = ArrayUtils.idealIntArraySize(mSpanCount + 1);
+ Object[] newspans = new Object[newsize];
+ int[] newspanstarts = new int[newsize];
+ int[] newspanends = new int[newsize];
+ int[] newspanflags = new int[newsize];
+
+ System.arraycopy(mSpans, 0, newspans, 0, mSpanCount);
+ System.arraycopy(mSpanStarts, 0, newspanstarts, 0, mSpanCount);
+ System.arraycopy(mSpanEnds, 0, newspanends, 0, mSpanCount);
+ System.arraycopy(mSpanFlags, 0, newspanflags, 0, mSpanCount);
+
+ mSpans = newspans;
+ mSpanStarts = newspanstarts;
+ mSpanEnds = newspanends;
+ mSpanFlags = newspanflags;
+ }
+
+ mSpans[mSpanCount] = what;
+ mSpanStarts[mSpanCount] = start;
+ mSpanEnds[mSpanCount] = end;
+ mSpanFlags[mSpanCount] = flags;
+ mSpanCount++;
+
+ if (send)
+ sendSpanAdded(what, nstart, nend);
+ }
+
+ /**
+ * Remove the specified markup object from the buffer.
+ */
+ public void removeSpan(Object what) {
+ for (int i = mSpanCount - 1; i >= 0; i--) {
+ if (mSpans[i] == what) {
+ int ostart = mSpanStarts[i];
+ int oend = mSpanEnds[i];
+
+ if (ostart > mGapStart)
+ ostart -= mGapLength;
+ if (oend > mGapStart)
+ oend -= mGapLength;
+
+ int count = mSpanCount - (i + 1);
+
+ System.arraycopy(mSpans, i + 1, mSpans, i, count);
+ System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count);
+ System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count);
+ System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count);
+
+ mSpanCount--;
+ mSpans[mSpanCount] = null;
+
+ sendSpanRemoved(what, ostart, oend);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Return the buffer offset of the beginning of the specified
+ * markup object, or -1 if it is not attached to this buffer.
+ */
+ public int getSpanStart(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ int where = mSpanStarts[i];
+
+ if (where > mGapStart)
+ where -= mGapLength;
+
+ return where;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Return the buffer offset of the end of the specified
+ * markup object, or -1 if it is not attached to this buffer.
+ */
+ public int getSpanEnd(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ int where = mSpanEnds[i];
+
+ if (where > mGapStart)
+ where -= mGapLength;
+
+ return where;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Return the flags of the end of the specified
+ * markup object, or 0 if it is not attached to this buffer.
+ */
+ public int getSpanFlags(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return mSpanFlags[i];
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Return an array of the spans of the specified type that overlap
+ * the specified range of the buffer. The kind may be Object.class to get
+ * a list of all the spans regardless of type.
+ */
+ public start
but less than or
+ * equal to limit
where a span of the specified type
+ * begins or ends.
+ */
+ public int nextSpanTransition(int start, int limit, Class kind) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] starts = mSpanStarts;
+ int[] ends = mSpanEnds;
+ int gapstart = mGapStart;
+ int gaplen = mGapLength;
+
+ if (kind == null) {
+ kind = Object.class;
+ }
+
+ for (int i = 0; i < count; i++) {
+ int st = starts[i];
+ int en = ends[i];
+
+ if (st > gapstart)
+ st -= gaplen;
+ if (en > gapstart)
+ en -= gaplen;
+
+ if (st > start && st < limit && kind.isInstance(spans[i]))
+ limit = st;
+ if (en > start && en < limit && kind.isInstance(spans[i]))
+ limit = en;
+ }
+
+ return limit;
+ }
+
+ /**
+ * Return a new CharSequence containing a copy of the specified
+ * range of this buffer, including the overlapping spans.
+ */
+ public CharSequence subSequence(int start, int end) {
+ return new SpannableStringBuilder(this, start, end);
+ }
+
+ /**
+ * Copy the specified range of chars from this buffer into the
+ * specified array, beginning at the specified offset.
+ */
+ public void getChars(int start, int end, char[] dest, int destoff) {
+ checkRange("getChars", start, end);
+
+ if (end <= mGapStart) {
+ System.arraycopy(mText, start, dest, destoff, end - start);
+ } else if (start >= mGapStart) {
+ System.arraycopy(mText, start + mGapLength,
+ dest, destoff, end - start);
+ } else {
+ System.arraycopy(mText, start, dest, destoff, mGapStart - start);
+ System.arraycopy(mText, mGapStart + mGapLength,
+ dest, destoff + (mGapStart - start),
+ end - mGapStart);
+ }
+ }
+
+ /**
+ * Return a String containing a copy of the chars in this buffer.
+ */
+ public String toString() {
+ int len = length();
+ char[] buf = new char[len];
+
+ getChars(0, len, buf, 0);
+ return new String(buf);
+ }
+
+ private TextWatcher[] sendTextWillChange(int start, int before, int after) {
+ TextWatcher[] recip = getSpans(start, start + before, TextWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].beforeTextChanged(this, start, before, after);
+ }
+
+ return recip;
+ }
+
+ private void sendTextChange(TextWatcher[] recip, int start, int before,
+ int after) {
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onTextChanged(this, start, before, after);
+ }
+ }
+
+ private void sendTextHasChanged(TextWatcher[] recip) {
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].afterTextChanged(this);
+ }
+ }
+
+ private void sendSpanAdded(Object what, int start, int end) {
+ SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanAdded(this, what, start, end);
+ }
+ }
+
+ private void sendSpanRemoved(Object what, int start, int end) {
+ SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanRemoved(this, what, start, end);
+ }
+ }
+
+ private void sendSpanChanged(Object what, int s, int e, int st, int en) {
+ SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en),
+ SpanWatcher.class);
+ int n = recip.length;
+
+ for (int i = 0; i < n; i++) {
+ recip[i].onSpanChanged(this, what, s, e, st, en);
+ }
+ }
+
+ private static String region(int start, int end) {
+ return "(" + start + " ... " + end + ")";
+ }
+
+ private void checkRange(final String operation, int start, int end) {
+ if (end < start) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " has end before start");
+ }
+
+ int len = length();
+
+ if (start > len || end > len) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " ends beyond length " + len);
+ }
+
+ if (start < 0 || end < 0) {
+ throw new IndexOutOfBoundsException(operation + " " +
+ region(start, end) +
+ " starts before 0");
+ }
+ }
+
+ private boolean isprint(char c) { // XXX
+ if (c >= ' ' && c <= '~')
+ return true;
+ else
+ return false;
+ }
+
+/*
+ private static final int startFlag(int flag) {
+ return (flag >> 4) & 0x0F;
+ }
+
+ private static final int endFlag(int flag) {
+ return flag & 0x0F;
+ }
+
+ public void dump() { // XXX
+ for (int i = 0; i < mGapStart; i++) {
+ System.out.print('|');
+ System.out.print(' ');
+ System.out.print(isprint(mText[i]) ? mText[i] : '.');
+ System.out.print(' ');
+ }
+
+ for (int i = mGapStart; i < mGapStart + mGapLength; i++) {
+ System.out.print('|');
+ System.out.print('(');
+ System.out.print(isprint(mText[i]) ? mText[i] : '.');
+ System.out.print(')');
+ }
+
+ for (int i = mGapStart + mGapLength; i < mText.length; i++) {
+ System.out.print('|');
+ System.out.print(' ');
+ System.out.print(isprint(mText[i]) ? mText[i] : '.');
+ System.out.print(' ');
+ }
+
+ System.out.print('\n');
+
+ for (int i = 0; i < mText.length + 1; i++) {
+ int found = 0;
+ int wfound = 0;
+
+ for (int j = 0; j < mSpanCount; j++) {
+ if (mSpanStarts[j] == i) {
+ found = 1;
+ wfound = j;
+ break;
+ }
+
+ if (mSpanEnds[j] == i) {
+ found = 2;
+ wfound = j;
+ break;
+ }
+ }
+
+ if (found == 1) {
+ if (startFlag(mSpanFlags[wfound]) == MARK)
+ System.out.print("( ");
+ if (startFlag(mSpanFlags[wfound]) == PARAGRAPH)
+ System.out.print("< ");
+ else
+ System.out.print("[ ");
+ } else if (found == 2) {
+ if (endFlag(mSpanFlags[wfound]) == POINT)
+ System.out.print(") ");
+ if (endFlag(mSpanFlags[wfound]) == PARAGRAPH)
+ System.out.print("> ");
+ else
+ System.out.print("] ");
+ } else {
+ System.out.print(" ");
+ }
+ }
+
+ System.out.print("\n");
+ }
+*/
+
+ /**
+ * Don't call this yourself -- exists for Canvas to use internally.
+ * {@hide}
+ */
+ public void drawText(Canvas c, int start, int end,
+ float x, float y, Paint p) {
+ checkRange("drawText", start, end);
+
+ if (end <= mGapStart) {
+ c.drawText(mText, start, end - start, x, y, p);
+ } else if (start >= mGapStart) {
+ c.drawText(mText, start + mGapLength, end - start, x, y, p);
+ } else {
+ char[] buf = TextUtils.obtain(end - start);
+
+ getChars(start, end, buf, 0);
+ c.drawText(buf, 0, end - start, x, y, p);
+ TextUtils.recycle(buf);
+ }
+ }
+
+ /**
+ * Don't call this yourself -- exists for Paint to use internally.
+ * {@hide}
+ */
+ public float measureText(int start, int end, Paint p) {
+ checkRange("measureText", start, end);
+
+ float ret;
+
+ if (end <= mGapStart) {
+ ret = p.measureText(mText, start, end - start);
+ } else if (start >= mGapStart) {
+ ret = p.measureText(mText, start + mGapLength, end - start);
+ } else {
+ char[] buf = TextUtils.obtain(end - start);
+
+ getChars(start, end, buf, 0);
+ ret = p.measureText(buf, 0, end - start);
+ TextUtils.recycle(buf);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Don't call this yourself -- exists for Paint to use internally.
+ * {@hide}
+ */
+ public int getTextWidths(int start, int end, float[] widths, Paint p) {
+ checkRange("getTextWidths", start, end);
+
+ int ret;
+
+ if (end <= mGapStart) {
+ ret = p.getTextWidths(mText, start, end - start, widths);
+ } else if (start >= mGapStart) {
+ ret = p.getTextWidths(mText, start + mGapLength, end - start,
+ widths);
+ } else {
+ char[] buf = TextUtils.obtain(end - start);
+
+ getChars(start, end, buf, 0);
+ ret = p.getTextWidths(buf, 0, end - start, widths);
+ TextUtils.recycle(buf);
+ }
+
+ return ret;
+ }
+
+ // Documentation from interface
+ public void setFilters(InputFilter[] filters) {
+ if (filters == null) {
+ throw new IllegalArgumentException();
+ }
+
+ mFilters = filters;
+ }
+
+ // Documentation from interface
+ public InputFilter[] getFilters() {
+ return mFilters;
+ }
+
+ private static final InputFilter[] NO_FILTERS = new InputFilter[0];
+ private InputFilter[] mFilters = NO_FILTERS;
+
+ private char[] mText;
+ private int mGapStart;
+ private int mGapLength;
+
+ private Object[] mSpans;
+ private int[] mSpanStarts;
+ private int[] mSpanEnds;
+ private int[] mSpanFlags;
+ private int mSpanCount;
+
+ private static final int MARK = 1;
+ private static final int POINT = 2;
+ private static final int PARAGRAPH = 3;
+
+ private static final int START_MASK = 0xF0;
+ private static final int END_MASK = 0x0F;
+ private static final int START_SHIFT = 4;
+}
diff --git a/core/java/android/text/SpannableStringInternal.java b/core/java/android/text/SpannableStringInternal.java
new file mode 100644
index 0000000..0412285
--- /dev/null
+++ b/core/java/android/text/SpannableStringInternal.java
@@ -0,0 +1,372 @@
+/*
+ * 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 com.android.internal.util.ArrayUtils;
+
+import java.lang.reflect.Array;
+
+/* package */ abstract class SpannableStringInternal
+{
+ /* package */ SpannableStringInternal(CharSequence source,
+ int start, int end) {
+ if (start == 0 && end == source.length())
+ mText = source.toString();
+ else
+ mText = source.toString().substring(start, end);
+
+ int initial = ArrayUtils.idealIntArraySize(0);
+ mSpans = new Object[initial];
+ mSpanData = new int[initial * 3];
+
+ if (source instanceof Spanned) {
+ Spanned sp = (Spanned) source;
+ Object[] spans = sp.getSpans(start, end, Object.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = sp.getSpanStart(spans[i]);
+ int en = sp.getSpanEnd(spans[i]);
+ int fl = sp.getSpanFlags(spans[i]);
+
+ if (st < start)
+ st = start;
+ if (en > end)
+ en = end;
+
+ setSpan(spans[i], st - start, en - start, fl);
+ }
+ }
+ }
+
+ public final int length() {
+ return mText.length();
+ }
+
+ public final char charAt(int i) {
+ return mText.charAt(i);
+ }
+
+ public final String toString() {
+ return mText;
+ }
+
+ /* subclasses must do subSequence() to preserve type */
+
+ public final void getChars(int start, int end, char[] dest, int off) {
+ mText.getChars(start, end, dest, off);
+ }
+
+ /* package */ void setSpan(Object what, int start, int end, int flags) {
+ int nstart = start;
+ int nend = end;
+
+ checkRange("setSpan", start, end);
+
+ if ((flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH) {
+ if (start != 0 && start != length()) {
+ char c = charAt(start - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must start at paragraph boundary" +
+ " (" + start + " follows " + c + ")");
+ }
+
+ if (end != 0 && end != length()) {
+ char c = charAt(end - 1);
+
+ if (c != '\n')
+ throw new RuntimeException(
+ "PARAGRAPH span must end at paragraph boundary" +
+ " (" + end + " follows " + c + ")");
+ }
+ }
+
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = 0; i < count; i++) {
+ if (spans[i] == what) {
+ int ostart = data[i * COLUMNS + START];
+ int oend = data[i * COLUMNS + END];
+
+ data[i * COLUMNS + START] = start;
+ data[i * COLUMNS + END] = end;
+ data[i * COLUMNS + FLAGS] = flags;
+
+ sendSpanChanged(what, ostart, oend, nstart, nend);
+ return;
+ }
+ }
+
+ if (mSpanCount + 1 >= mSpans.length) {
+ int newsize = ArrayUtils.idealIntArraySize(mSpanCount + 1);
+ Object[] newtags = new Object[newsize];
+ int[] newdata = new int[newsize * 3];
+
+ System.arraycopy(mSpans, 0, newtags, 0, mSpanCount);
+ System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3);
+
+ mSpans = newtags;
+ mSpanData = newdata;
+ }
+
+ mSpans[mSpanCount] = what;
+ mSpanData[mSpanCount * COLUMNS + START] = start;
+ mSpanData[mSpanCount * COLUMNS + END] = end;
+ mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
+ mSpanCount++;
+
+ if (this instanceof Spannable)
+ sendSpanAdded(what, nstart, nend);
+ }
+
+ /* package */ void removeSpan(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ int ostart = data[i * COLUMNS + START];
+ int oend = data[i * COLUMNS + END];
+
+ int c = count - (i + 1);
+
+ System.arraycopy(spans, i + 1, spans, i, c);
+ System.arraycopy(data, (i + 1) * COLUMNS,
+ data, i * COLUMNS, c * COLUMNS);
+
+ mSpanCount--;
+
+ sendSpanRemoved(what, ostart, oend);
+ return;
+ }
+ }
+ }
+
+ public int getSpanStart(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return data[i * COLUMNS + START];
+ }
+ }
+
+ return -1;
+ }
+
+ public int getSpanEnd(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return data[i * COLUMNS + END];
+ }
+ }
+
+ return -1;
+ }
+
+ public int getSpanFlags(Object what) {
+ int count = mSpanCount;
+ Object[] spans = mSpans;
+ int[] data = mSpanData;
+
+ for (int i = count - 1; i >= 0; i--) {
+ if (spans[i] == what) {
+ return data[i * COLUMNS + FLAGS];
+ }
+ }
+
+ return 0;
+ }
+
+ public start
+ * where a markup object of class type
begins or ends,
+ * or limit
if there are no starts or ends greater than or
+ * equal to start
but less than limit
. Specify
+ * null
or Object.class for the type if you want every
+ * transition regardless of type.
+ */
+ public int nextSpanTransition(int start, int limit, Class type);
+}
diff --git a/core/java/android/text/SpannedString.java b/core/java/android/text/SpannedString.java
new file mode 100644
index 0000000..afed221
--- /dev/null
+++ b/core/java/android/text/SpannedString.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+
+/**
+ * This is the class for text whose content and markup are immutable.
+ * For mutable markup, see {@link SpannableString}; for mutable text,
+ * see {@link SpannableStringBuilder}.
+ */
+public final class SpannedString
+extends SpannableStringInternal
+implements CharSequence, GetChars, Spanned
+{
+ public SpannedString(CharSequence source) {
+ super(source, 0, source.length());
+ }
+
+ private SpannedString(CharSequence source, int start, int end) {
+ super(source, start, end);
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ return new SpannedString(this, start, end);
+ }
+
+ public static SpannedString valueOf(CharSequence source) {
+ if (source instanceof SpannedString) {
+ return (SpannedString) source;
+ } else {
+ return new SpannedString(source);
+ }
+ }
+}
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
new file mode 100644
index 0000000..2d18575
--- /dev/null
+++ b/core/java/android/text/StaticLayout.java
@@ -0,0 +1,1118 @@
+/*
+ * 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.Paint;
+import com.android.internal.util.ArrayUtils;
+import android.util.Log;
+import android.text.style.LeadingMarginSpan;
+import android.text.style.LineHeightSpan;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.ReplacementSpan;
+
+/**
+ * StaticLayout is a Layout for text that will not be edited after it
+ * is laid out. Use {@link DynamicLayout} for text that may change.
+ *
+ * // Once
+ * TextUtils.StringSplitter splitter = new TextUtils.SimpleStringSplitter(delimiter);
+ *
+ * // Once per string to split
+ * splitter.setString(string);
+ * for (String s : splitter) {
+ * ...
+ * }
+ *
+ */
+ public interface StringSplitter extends Iterabletemplate
CharSequence with the corresponding
+ * values
. "^^" is used to produce a single caret in
+ * the output. Only up to 9 replacement values are supported,
+ * "^10" will be produce the first replacement value followed by a
+ * '0'.
+ *
+ * @param template the input text containing "^1"-style
+ * placeholder values. This object is not modified; a copy is
+ * returned.
+ *
+ * @param values CharSequences substituted into the template. The
+ * first is substituted for "^1", the second for "^2", and so on.
+ *
+ * @return the new CharSequence produced by doing the replacement
+ *
+ * @throws IllegalArgumentException if the template requests a
+ * value that was not provided, or if more than 9 values are
+ * provided.
+ */
+ public static CharSequence expandTemplate(CharSequence template,
+ CharSequence... values) {
+ if (values.length > 9) {
+ throw new IllegalArgumentException("max of 9 values are supported");
+ }
+
+ SpannableStringBuilder ssb = new SpannableStringBuilder(template);
+
+ try {
+ int i = 0;
+ while (i < ssb.length()) {
+ if (ssb.charAt(i) == '^') {
+ char next = ssb.charAt(i+1);
+ if (next == '^') {
+ ssb.delete(i+1, i+2);
+ ++i;
+ continue;
+ } else if (Character.isDigit(next)) {
+ int which = Character.getNumericValue(next) - 1;
+ if (which < 0) {
+ throw new IllegalArgumentException(
+ "template requests value ^" + (which+1));
+ }
+ if (which >= values.length) {
+ throw new IllegalArgumentException(
+ "template requests value ^" + (which+1) +
+ "; only " + values.length + " provided");
+ }
+ ssb.replace(i, i+2, values[which]);
+ i += values[which].length();
+ continue;
+ }
+ }
+ ++i;
+ }
+ } catch (IndexOutOfBoundsException ignore) {
+ // happens when ^ is the last character in the string.
+ }
+ return ssb;
+ }
+
+ public static int getOffsetBefore(CharSequence text, int offset) {
+ if (offset == 0)
+ return 0;
+ if (offset == 1)
+ return 0;
+
+ char c = text.charAt(offset - 1);
+
+ if (c >= '\uDC00' && c <= '\uDFFF') {
+ char c1 = text.charAt(offset - 2);
+
+ if (c1 >= '\uD800' && c1 <= '\uDBFF')
+ offset -= 2;
+ else
+ offset -= 1;
+ } else {
+ offset -= 1;
+ }
+
+ if (text instanceof Spanned) {
+ 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;
+ }
+
+ public static int getOffsetAfter(CharSequence text, int offset) {
+ int len = text.length();
+
+ if (offset == len)
+ return len;
+ if (offset == len - 1)
+ return len;
+
+ char c = text.charAt(offset);
+
+ if (c >= '\uD800' && c <= '\uDBFF') {
+ char c1 = text.charAt(offset + 1);
+
+ if (c1 >= '\uDC00' && c1 <= '\uDFFF')
+ offset += 2;
+ else
+ offset += 1;
+ } else {
+ offset += 1;
+ }
+
+ if (text instanceof Spanned) {
+ 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 = end;
+ }
+ }
+
+ return offset;
+ }
+
+ private static void readSpan(Parcel p, Spannable sp, Object o) {
+ sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
+ }
+
+ public static void copySpansFrom(Spanned source, int start, int end,
+ Class kind,
+ Spannable dest, int destoff) {
+ if (kind == null) {
+ kind = Object.class;
+ }
+
+ Object[] spans = source.getSpans(start, end, kind);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = source.getSpanStart(spans[i]);
+ int en = source.getSpanEnd(spans[i]);
+ int fl = source.getSpanFlags(spans[i]);
+
+ if (st < start)
+ st = start;
+ if (en > end)
+ en = end;
+
+ dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
+ fl);
+ }
+ }
+
+ public enum TruncateAt {
+ START,
+ MIDDLE,
+ END,
+ }
+
+ public interface EllipsizeCallback {
+ /**
+ * This method is called to report that the specified region of
+ * text was ellipsized away by a call to {@link #ellipsize}.
+ */
+ public void ellipsized(int start, int end);
+ }
+
+ private static String sEllipsis = null;
+
+ /**
+ * Returns the original text if it fits in the specified width
+ * given the properties of the specified Paint,
+ * or, if it does not fit, a truncated
+ * copy with ellipsis character added at the specified edge or center.
+ */
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint p,
+ float avail, TruncateAt where) {
+ return ellipsize(text, p, avail, where, false, null);
+ }
+
+ /**
+ * Returns the original text if it fits in the specified width
+ * given the properties of the specified Paint,
+ * or, if it does not fit, a copy with ellipsis character added
+ * at the specified edge or center.
+ * If preserveLength
is specified, the returned copy
+ * will be padded with zero-width spaces to preserve the original
+ * length and offsets instead of truncating.
+ * If callback
is non-null, it will be called to
+ * report the start and end of the ellipsized range.
+ */
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint p,
+ float avail, TruncateAt where,
+ boolean preserveLength,
+ EllipsizeCallback callback) {
+ if (sEllipsis == null) {
+ Resources r = Resources.getSystem();
+ sEllipsis = r.getString(R.string.ellipsis);
+ }
+
+ int len = text.length();
+
+ // Use Paint.breakText() for the non-Spanned case to avoid having
+ // to allocate memory and accumulate the character widths ourselves.
+
+ if (!(text instanceof Spanned)) {
+ float wid = p.measureText(text, 0, len);
+
+ if (wid <= avail) {
+ if (callback != null) {
+ callback.ellipsized(0, 0);
+ }
+
+ return text;
+ }
+
+ float ellipsiswid = p.measureText(sEllipsis);
+
+ if (ellipsiswid > avail) {
+ if (callback != null) {
+ callback.ellipsized(0, len);
+ }
+
+ if (preserveLength) {
+ char[] buf = obtain(len);
+ for (int i = 0; i < len; i++) {
+ buf[i] = '\uFEFF';
+ }
+ String ret = new String(buf, 0, len);
+ recycle(buf);
+ return ret;
+ } else {
+ return "";
+ }
+ }
+
+ if (where == TruncateAt.START) {
+ int fit = p.breakText(text, 0, len, false,
+ avail - ellipsiswid, null);
+
+ if (callback != null) {
+ callback.ellipsized(0, len - fit);
+ }
+
+ if (preserveLength) {
+ return blank(text, 0, len - fit);
+ } else {
+ return sEllipsis + text.toString().substring(len - fit, len);
+ }
+ } else if (where == TruncateAt.END) {
+ int fit = p.breakText(text, 0, len, true,
+ avail - ellipsiswid, null);
+
+ if (callback != null) {
+ callback.ellipsized(fit, len);
+ }
+
+ if (preserveLength) {
+ return blank(text, fit, len);
+ } else {
+ return text.toString().substring(0, fit) + sEllipsis;
+ }
+ } else /* where == TruncateAt.MIDDLE */ {
+ int right = p.breakText(text, 0, len, false,
+ (avail - ellipsiswid) / 2, null);
+ float used = p.measureText(text, len - right, len);
+ int left = p.breakText(text, 0, len - right, true,
+ avail - ellipsiswid - used, null);
+
+ if (callback != null) {
+ callback.ellipsized(left, len - right);
+ }
+
+ if (preserveLength) {
+ return blank(text, left, len - right);
+ } else {
+ String s = text.toString();
+ return s.substring(0, left) + sEllipsis +
+ s.substring(len - right, len);
+ }
+ }
+ }
+
+ // But do the Spanned cases by hand, because it's such a pain
+ // to iterate the span transitions backwards and getTextWidths()
+ // will give us the information we need.
+
+ // getTextWidths() always writes into the start of the array,
+ // so measure each span into the first half and then copy the
+ // results into the second half to use later.
+
+ float[] wid = new float[len * 2];
+ TextPaint temppaint = new TextPaint();
+ Spanned sp = (Spanned) text;
+
+ int next;
+ for (int i = 0; i < len; i = next) {
+ next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
+
+ Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
+ System.arraycopy(wid, 0, wid, len + i, next - i);
+ }
+
+ float sum = 0;
+ for (int i = 0; i < len; i++) {
+ sum += wid[len + i];
+ }
+
+ if (sum <= avail) {
+ if (callback != null) {
+ callback.ellipsized(0, 0);
+ }
+
+ return text;
+ }
+
+ float ellipsiswid = p.measureText(sEllipsis);
+
+ if (ellipsiswid > avail) {
+ if (callback != null) {
+ callback.ellipsized(0, len);
+ }
+
+ if (preserveLength) {
+ char[] buf = obtain(len);
+ for (int i = 0; i < len; i++) {
+ buf[i] = '\uFEFF';
+ }
+ SpannableString ss = new SpannableString(new String(buf, 0, len));
+ recycle(buf);
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ return "";
+ }
+ }
+
+ if (where == TruncateAt.START) {
+ sum = 0;
+ int i;
+
+ for (i = len; i >= 0; i--) {
+ float w = wid[len + i - 1];
+
+ if (w + sum + ellipsiswid > avail) {
+ break;
+ }
+
+ sum += w;
+ }
+
+ if (callback != null) {
+ callback.ellipsized(0, i);
+ }
+
+ if (preserveLength) {
+ SpannableString ss = new SpannableString(blank(text, 0, i));
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
+ out.insert(1, text, i, len);
+
+ return out;
+ }
+ } else if (where == TruncateAt.END) {
+ sum = 0;
+ int i;
+
+ for (i = 0; i < len; i++) {
+ float w = wid[len + i];
+
+ if (w + sum + ellipsiswid > avail) {
+ break;
+ }
+
+ sum += w;
+ }
+
+ if (callback != null) {
+ callback.ellipsized(i, len);
+ }
+
+ if (preserveLength) {
+ SpannableString ss = new SpannableString(blank(text, i, len));
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
+ out.insert(0, text, 0, i);
+
+ return out;
+ }
+ } else /* where = TruncateAt.MIDDLE */ {
+ float lsum = 0, rsum = 0;
+ int left = 0, right = len;
+
+ float ravail = (avail - ellipsiswid) / 2;
+ for (right = len; right >= 0; right--) {
+ float w = wid[len + right - 1];
+
+ if (w + rsum > ravail) {
+ break;
+ }
+
+ rsum += w;
+ }
+
+ float lavail = avail - ellipsiswid - rsum;
+ for (left = 0; left < right; left++) {
+ float w = wid[len + left];
+
+ if (w + lsum > lavail) {
+ break;
+ }
+
+ lsum += w;
+ }
+
+ if (callback != null) {
+ callback.ellipsized(left, right);
+ }
+
+ if (preserveLength) {
+ SpannableString ss = new SpannableString(blank(text, left, right));
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(sEllipsis);
+ out.insert(0, text, 0, left);
+ out.insert(out.length(), text, right, len);
+
+ return out;
+ }
+ }
+ }
+
+ private static String blank(CharSequence source, int start, int end) {
+ int len = source.length();
+ char[] buf = obtain(len);
+
+ if (start != 0) {
+ getChars(source, 0, start, buf, 0);
+ }
+ if (end != len) {
+ getChars(source, end, len, buf, end);
+ }
+
+ if (start != end) {
+ buf[start] = '\u2026';
+
+ for (int i = start + 1; i < end; i++) {
+ buf[i] = '\uFEFF';
+ }
+ }
+
+ String ret = new String(buf, 0, len);
+ recycle(buf);
+
+ return ret;
+ }
+
+ /**
+ * Converts a CharSequence of the comma-separated form "Andy, Bob,
+ * Charles, David" that is too wide to fit into the specified width
+ * into one like "Andy, Bob, 2 more".
+ *
+ * @param text the text to truncate
+ * @param p the Paint with which to measure the text
+ * @param avail the horizontal width available for the text
+ * @param oneMore the string for "1 more" in the current locale
+ * @param more the string for "%d more" in the current locale
+ */
+ public static CharSequence commaEllipsize(CharSequence text,
+ TextPaint p, float avail,
+ String oneMore,
+ String more) {
+ int len = text.length();
+ char[] buf = new char[len];
+ TextUtils.getChars(text, 0, len, buf, 0);
+
+ int commaCount = 0;
+ for (int i = 0; i < len; i++) {
+ if (buf[i] == ',') {
+ commaCount++;
+ }
+ }
+
+ float[] wid;
+
+ if (text instanceof Spanned) {
+ Spanned sp = (Spanned) text;
+ TextPaint temppaint = new TextPaint();
+ wid = new float[len * 2];
+
+ int next;
+ for (int i = 0; i < len; i = next) {
+ next = sp.nextSpanTransition(i, len, MetricAffectingSpan.class);
+
+ Styled.getTextWidths(p, temppaint, sp, i, next, wid, null);
+ System.arraycopy(wid, 0, wid, len + i, next - i);
+ }
+
+ System.arraycopy(wid, len, wid, 0, len);
+ } else {
+ wid = new float[len];
+ p.getTextWidths(text, 0, len, wid);
+ }
+
+ int ok = 0;
+ int okRemaining = commaCount + 1;
+ String okFormat = "";
+
+ int w = 0;
+ int count = 0;
+
+ for (int i = 0; i < len; i++) {
+ w += wid[i];
+
+ if (buf[i] == ',') {
+ count++;
+
+ int remaining = commaCount - count + 1;
+ float moreWid;
+ String format;
+
+ if (remaining == 1) {
+ format = " " + oneMore;
+ } else {
+ format = " " + String.format(more, remaining);
+ }
+
+ moreWid = p.measureText(format);
+
+ if (w + moreWid <= avail) {
+ ok = i + 1;
+ okRemaining = remaining;
+ okFormat = format;
+ }
+ }
+ }
+
+ if (w <= avail) {
+ return text;
+ } else {
+ SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
+ out.insert(0, text, 0, ok);
+ return out;
+ }
+ }
+
+ /* package */ static char[] obtain(int len) {
+ char[] buf;
+
+ synchronized (sLock) {
+ buf = sTemp;
+ sTemp = null;
+ }
+
+ if (buf == null || buf.length < len)
+ buf = new char[ArrayUtils.idealCharArraySize(len)];
+
+ return buf;
+ }
+
+ /* package */ static void recycle(char[] temp) {
+ if (temp.length > 1000)
+ return;
+
+ synchronized (sLock) {
+ sTemp = temp;
+ }
+ }
+
+ /**
+ * Html-encode the string.
+ * @param s the string to be encoded
+ * @return the encoded string
+ */
+ public static String htmlEncode(String s) {
+ StringBuilder sb = new StringBuilder();
+ char c;
+ for (int i = 0; i < s.length(); i++) {
+ c = s.charAt(i);
+ switch (c) {
+ case '<':
+ sb.append("<"); //$NON-NLS-1$
+ break;
+ case '>':
+ sb.append(">"); //$NON-NLS-1$
+ break;
+ case '&':
+ sb.append("&"); //$NON-NLS-1$
+ break;
+ case '\\':
+ sb.append("'"); //$NON-NLS-1$
+ break;
+ case '"':
+ sb.append("""); //$NON-NLS-1$
+ break;
+ default:
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns a CharSequence concatenating the specified CharSequences,
+ * retaining their spans if any.
+ */
+ public static CharSequence concat(CharSequence... text) {
+ if (text.length == 0) {
+ return "";
+ }
+
+ if (text.length == 1) {
+ return text[0];
+ }
+
+ boolean spanned = false;
+ for (int i = 0; i < text.length; i++) {
+ if (text[i] instanceof Spanned) {
+ spanned = true;
+ break;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < text.length; i++) {
+ sb.append(text[i]);
+ }
+
+ if (!spanned) {
+ return sb.toString();
+ }
+
+ SpannableString ss = new SpannableString(sb);
+ int off = 0;
+ for (int i = 0; i < text.length; i++) {
+ int len = text[i].length();
+
+ if (text[i] instanceof Spanned) {
+ copySpansFrom((Spanned) text[i], 0, len, Object.class, ss, off);
+ }
+
+ off += len;
+ }
+
+ return new SpannedString(ss);
+ }
+
+ /**
+ * Returns whether the given CharSequence contains any printable characters.
+ */
+ public static boolean isGraphic(CharSequence str) {
+ final int len = str.length();
+ for (int i=0; icount
characters beginning at start
+ * are about to be replaced by new text with length after
.
+ * It is an error to attempt to make changes to s
from
+ * this callback.
+ */
+ public void beforeTextChanged(CharSequence s, int start,
+ int count, int after);
+ /**
+ * This method is called to notify you that, within s
,
+ * the count
characters beginning at start
+ * have just replaced old text that had length before
.
+ * It is an error to attempt to make changes to s
from
+ * this callback.
+ */
+ public void onTextChanged(CharSequence s, int start, int before, int count);
+
+ /**
+ * This method is called to notify you that, somewhere within
+ * s
, the text has been changed.
+ * It is legitimate to make further changes to s
from
+ * this callback, but be careful not to get yourself into an infinite
+ * loop, because any changes you make will cause this method to be
+ * called again recursively.
+ * (You are not told where the change took place because other
+ * afterTextChanged() methods may already have made other changes
+ * and invalidated the offsets. But if you need to know here,
+ * you can use {@link Spannable#setSpan} in {@link #onTextChanged}
+ * to mark your place and then look up from here where the span
+ * ended up.
+ */
+ public void afterTextChanged(Editable s);
+}
diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java
new file mode 100644
index 0000000..ac2e499
--- /dev/null
+++ b/core/java/android/text/method/ArrowKeyMovementMethod.java
@@ -0,0 +1,266 @@
+/*
+ * 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.method;
+
+import android.view.KeyEvent;
+import android.text.*;
+import android.widget.TextView;
+import android.view.View;
+import android.view.MotionEvent;
+
+// XXX this doesn't extend MetaKeyKeyListener because the signatures
+// don't match. Need to figure that out. Meanwhile the meta keys
+// won't work in fields that don't take input.
+
+public class
+ArrowKeyMovementMethod
+implements MovementMethod
+{
+ private boolean up(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ Selection.extendSelection(buffer, 0);
+ return true;
+ } else {
+ return Selection.extendUp(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ Selection.setSelection(buffer, 0);
+ return true;
+ } else {
+ return Selection.moveUp(buffer, layout);
+ }
+ }
+ }
+
+ private boolean down(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ Selection.extendSelection(buffer, buffer.length());
+ return true;
+ } else {
+ return Selection.extendDown(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ Selection.setSelection(buffer, buffer.length());
+ return true;
+ } else {
+ return Selection.moveDown(buffer, layout);
+ }
+ }
+ }
+
+ private boolean left(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ return Selection.extendToLeftEdge(buffer, layout);
+ } else {
+ return Selection.extendLeft(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ return Selection.moveToLeftEdge(buffer, layout);
+ } else {
+ return Selection.moveLeft(buffer, layout);
+ }
+ }
+ }
+
+ private boolean right(TextView widget, Spannable buffer) {
+ boolean cap = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_SHIFT_ON) == 1;
+ boolean alt = MetaKeyKeyListener.getMetaState(buffer,
+ KeyEvent.META_ALT_ON) == 1;
+ Layout layout = widget.getLayout();
+
+ if (cap) {
+ if (alt) {
+ return Selection.extendToRightEdge(buffer, layout);
+ } else {
+ return Selection.extendRight(buffer, layout);
+ }
+ } else {
+ if (alt) {
+ return Selection.moveToRightEdge(buffer, layout);
+ } else {
+ return Selection.moveRight(buffer, layout);
+ }
+ }
+ }
+
+ public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
+ boolean handled = false;
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_UP:
+ handled |= up(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ handled |= down(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ handled |= left(widget, buffer);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ handled |= right(widget, buffer);
+ break;
+ }
+
+ if (handled) {
+ MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
+ MetaKeyKeyListener.resetLockedMeta(buffer);
+ }
+
+ return handled;
+ }
+
+ public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ public boolean onTrackballEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ boolean handled = false;
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ for (; y < 0; y++) {
+ handled |= up(widget, buffer);
+ }
+ for (; y > 0; y--) {
+ handled |= down(widget, buffer);
+ }
+
+ for (; x < 0; x++) {
+ handled |= left(widget, buffer);
+ }
+ for (; x > 0; x--) {
+ handled |= right(widget, buffer);
+ }
+
+ if (handled) {
+ MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
+ MetaKeyKeyListener.resetLockedMeta(buffer);
+ }
+
+ return handled;
+ }
+
+ public boolean onTouchEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ boolean handled = Touch.onTouchEvent(widget, buffer, event);
+
+ if (widget.isFocused()) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= widget.getTotalPaddingLeft();
+ y -= widget.getTotalPaddingTop();
+
+ x += widget.getScrollX();
+ y += widget.getScrollY();
+
+ Layout layout = widget.getLayout();
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ boolean cap = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0;
+
+ if (cap) {
+ Selection.extendSelection(buffer, off);
+ } else {
+ Selection.setSelection(buffer, off);
+ }
+
+ MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
+ MetaKeyKeyListener.resetLockedMeta(buffer);
+
+ return true;
+ }
+ }
+
+ return handled;
+ }
+
+ public boolean canSelectArbitrarily() {
+ return true;
+ }
+
+ public void initialize(TextView widget, Spannable text) {
+ Selection.setSelection(text, 0);
+ }
+
+ public void onTakeFocus(TextView view, Spannable text, int dir) {
+ if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
+ Layout layout = view.getLayout();
+
+ if (layout == null) {
+ /*
+ * This shouldn't be null, but do something sensible if it is.
+ */
+ Selection.setSelection(text, text.length());
+ } else {
+ /*
+ * Put the cursor at the end of the first line, which is
+ * either the last offset if there is only one line, or the
+ * offset before the first character of the second line
+ * if there is more than one line.
+ */
+ if (layout.getLineCount() == 1) {
+ Selection.setSelection(text, text.length());
+ } else {
+ Selection.setSelection(text, layout.getLineStart(1) - 1);
+ }
+ }
+ } else {
+ Selection.setSelection(text, text.length());
+ }
+ }
+
+ public static MovementMethod getInstance() {
+ if (sInstance == null)
+ sInstance = new ArrowKeyMovementMethod();
+
+ return sInstance;
+ }
+
+ private static ArrowKeyMovementMethod sInstance;
+}
diff --git a/core/java/android/text/method/BaseKeyListener.java b/core/java/android/text/method/BaseKeyListener.java
new file mode 100644
index 0000000..3e92b7b
--- /dev/null
+++ b/core/java/android/text/method/BaseKeyListener.java
@@ -0,0 +1,112 @@
+/*
+ * 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.method;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.os.Message;
+import android.util.Log;
+import android.text.*;
+import android.widget.TextView;
+
+public abstract class BaseKeyListener
+extends MetaKeyKeyListener
+implements KeyListener {
+ /* package */ static final Object OLD_SEL_START = new Object();
+
+ /**
+ * Performs the action that happens when you press the DEL key in
+ * a TextView. If there is a selection, deletes the selection;
+ * otherwise, DEL alone deletes the character before the cursor,
+ * if any;
+ * ALT+DEL deletes everything on the line the cursor is on.
+ *
+ * @return true if anything was deleted; false otherwise.
+ */
+ public boolean backspace(View view, Editable content, int keyCode,
+ KeyEvent event) {
+ int selStart, selEnd;
+ boolean result = true;
+
+ {
+ int a = Selection.getSelectionStart(content);
+ int b = Selection.getSelectionEnd(content);
+
+ selStart = Math.min(a, b);
+ selEnd = Math.max(a, b);
+ }
+
+ if (selStart != selEnd) {
+ content.delete(selStart, selEnd);
+ } else if (altBackspace(view, content, keyCode, event)) {
+ result = true;
+ } else {
+ int to = TextUtils.getOffsetBefore(content, selEnd);
+
+ if (to != selEnd) {
+ content.delete(Math.min(to, selEnd), Math.max(to, selEnd));
+ }
+ else {
+ result = false;
+ }
+ }
+
+ if (result)
+ adjustMetaAfterKeypress(content);
+
+ return result;
+ }
+
+ private boolean altBackspace(View view, Editable content, int keyCode,
+ KeyEvent event) {
+ if (getMetaState(content, META_ALT_ON) != 1) {
+ return false;
+ }
+
+ if (!(view instanceof TextView)) {
+ return false;
+ }
+
+ Layout layout = ((TextView) view).getLayout();
+
+ if (layout == null) {
+ return false;
+ }
+
+ int l = layout.getLineForOffset(Selection.getSelectionStart(content));
+ int start = layout.getLineStart(l);
+ int end = layout.getLineEnd(l);
+
+ if (end == start) {
+ return false;
+ }
+
+ content.delete(start, end);
+ return true;
+ }
+
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_DEL) {
+ backspace(view, content, keyCode, event);
+ return true;
+ }
+
+ return super.onKeyDown(view, content, keyCode, event);
+ }
+}
+
diff --git a/core/java/android/text/method/CharacterPickerDialog.java b/core/java/android/text/method/CharacterPickerDialog.java
new file mode 100644
index 0000000..d787132
--- /dev/null
+++ b/core/java/android/text/method/CharacterPickerDialog.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2008 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.method;
+
+import com.android.internal.R;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.*;
+import android.view.LayoutInflater;
+import android.view.View.OnClickListener;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.GridView;
+import android.widget.TextView;
+
+/**
+ * Dialog for choosing accented characters related to a base character.
+ */
+public class CharacterPickerDialog extends Dialog
+ implements OnItemClickListener, OnClickListener {
+ private View mView;
+ private Editable mText;
+ private String mOptions;
+ private boolean mInsert;
+ private LayoutInflater mInflater;
+
+ /**
+ * Creates a new CharacterPickerDialog that presents the specified
+ * options
for insertion or replacement (depending on
+ * the sense of insert
) into text
.
+ */
+ public CharacterPickerDialog(Context context, View view,
+ Editable text, String options,
+ boolean insert) {
+ super(context);
+
+ mView = view;
+ mText = text;
+ mOptions = options;
+ mInsert = insert;
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ WindowManager.LayoutParams params = getWindow().getAttributes();
+ params.token = mView.getApplicationWindowToken();
+ params.type = params.TYPE_APPLICATION_PANEL;
+
+ setTitle(R.string.select_character);
+ setContentView(R.layout.character_picker);
+
+ GridView grid = (GridView) findViewById(R.id.characterPicker);
+ grid.setAdapter(new OptionsAdapter(getContext()));
+ grid.setOnItemClickListener(this);
+
+ findViewById(R.id.cancel).setOnClickListener(this);
+ }
+
+ /**
+ * Handles clicks on the character buttons.
+ */
+ public void onItemClick(AdapterView parent, View view, int position, long id) {
+ int selEnd = Selection.getSelectionEnd(mText);
+ String result = String.valueOf(mOptions.charAt(position));
+
+ if (mInsert || selEnd == 0) {
+ mText.insert(selEnd, result);
+ } else {
+ mText.replace(selEnd - 1, selEnd, result);
+ }
+
+ dismiss();
+ }
+
+ /**
+ * Handles clicks on the Cancel button.
+ */
+ public void onClick(View v) {
+ dismiss();
+ }
+
+ private class OptionsAdapter extends BaseAdapter {
+ private Context mContext;
+
+ public OptionsAdapter(Context context) {
+ super();
+ mContext = context;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Button b = (Button)
+ mInflater.inflate(R.layout.character_picker_button, null);
+ b.setText(String.valueOf(mOptions.charAt(position)));
+ return b;
+ }
+
+ public final int getCount() {
+ return mOptions.length();
+ }
+
+ public final Object getItem(int position) {
+ return String.valueOf(mOptions.charAt(position));
+ }
+
+ public final long getItemId(int position) {
+ return position;
+ }
+ }
+}
diff --git a/core/java/android/text/method/DateKeyListener.java b/core/java/android/text/method/DateKeyListener.java
new file mode 100644
index 0000000..0ca0332
--- /dev/null
+++ b/core/java/android/text/method/DateKeyListener.java
@@ -0,0 +1,52 @@
+/*
+ * 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.method;
+
+import android.view.KeyEvent;
+
+/**
+ * For entering dates in a text field.
+ */
+public class DateKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static DateKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new DateKeyListener();
+ return sInstance;
+ }
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ '/', '-', '.'
+ };
+
+ private static DateKeyListener sInstance;
+}
diff --git a/core/java/android/text/method/DateTimeKeyListener.java b/core/java/android/text/method/DateTimeKeyListener.java
new file mode 100644
index 0000000..304d326
--- /dev/null
+++ b/core/java/android/text/method/DateTimeKeyListener.java
@@ -0,0 +1,52 @@
+/*
+ * 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.method;
+
+import android.view.KeyEvent;
+
+/**
+ * For entering dates and times in the same text field.
+ */
+public class DateTimeKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static DateTimeKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new DateTimeKeyListener();
+ return sInstance;
+ }
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm',
+ 'p', ':', '/', '-', ' '
+ };
+
+ private static DateTimeKeyListener sInstance;
+}
diff --git a/core/java/android/text/method/DialerKeyListener.java b/core/java/android/text/method/DialerKeyListener.java
new file mode 100644
index 0000000..e805ad7
--- /dev/null
+++ b/core/java/android/text/method/DialerKeyListener.java
@@ -0,0 +1,109 @@
+/*
+ * 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.method;
+
+import android.view.KeyEvent;
+import android.view.KeyCharacterMap.KeyData;
+import android.util.SparseIntArray;
+import android.text.Spannable;
+
+/**
+ * For dialing-only text entry
+ */
+public class DialerKeyListener extends NumberKeyListener
+{
+ @Override
+ protected char[] getAcceptedChars()
+ {
+ return CHARACTERS;
+ }
+
+ public static DialerKeyListener getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new DialerKeyListener();
+ return sInstance;
+ }
+
+ /**
+ * Overrides the superclass's lookup method to prefer the number field
+ * from the KeyEvent.
+ */
+ protected int lookup(KeyEvent event, Spannable content) {
+ int meta = getMetaState(content);
+ int number = event.getNumber();
+
+ /*
+ * Prefer number if no meta key is active, or if it produces something
+ * valid and the meta lookup does not.
+ */
+ if ((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) {
+ if (number != 0) {
+ return number;
+ }
+ }
+
+ int match = super.lookup(event, content);
+
+ if (match != 0) {
+ return match;
+ } else {
+ /*
+ * If a meta key is active but the lookup with the meta key
+ * did not produce anything, try some other meta keys, because
+ * the user might have pressed SHIFT when they meant ALT,
+ * or vice versa.
+ */
+
+ if (meta != 0) {
+ KeyData kd = new KeyData();
+ char[] accepted = getAcceptedChars();
+
+ if (event.getKeyData(kd)) {
+ for (int i = 1; i < kd.meta.length; i++) {
+ if (ok(accepted, kd.meta[i])) {
+ return kd.meta[i];
+ }
+ }
+ }
+ }
+
+ /*
+ * Otherwise, use the number associated with the key, since
+ * whatever they wanted to do with the meta key does not
+ * seem to be valid here.
+ */
+
+ return number;
+ }
+ }
+
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ public static final char[] CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*',
+ '+', '-', '(', ')', ',', '/', 'N', '.', ' '
+ };
+
+ private static DialerKeyListener sInstance;
+}
diff --git a/core/java/android/text/method/DigitsKeyListener.java b/core/java/android/text/method/DigitsKeyListener.java
new file mode 100644
index 0000000..99a3f1a
--- /dev/null
+++ b/core/java/android/text/method/DigitsKeyListener.java
@@ -0,0 +1,206 @@
+/*
+ * 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.method;
+
+import android.text.Spanned;
+import android.text.SpannableStringBuilder;
+import android.view.KeyEvent;
+
+
+/**
+ * For digits-only text entry
+ */
+public class DigitsKeyListener extends NumberKeyListener
+{
+ private char[] mAccepted;
+ private boolean mSign;
+ private boolean mDecimal;
+
+ private static final int SIGN = 1;
+ private static final int DECIMAL = 2;
+
+ @Override
+ protected char[] getAcceptedChars() {
+ return mAccepted;
+ }
+
+ /**
+ * The characters that are used.
+ *
+ * @see KeyEvent#getMatch
+ * @see #getAcceptedChars
+ */
+ private static final char[][] CHARACTERS = new char[][] {
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-' },
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' },
+ new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.' },
+ };
+
+ /**
+ * Allocates a DigitsKeyListener that accepts the digits 0 through 9.
+ */
+ public DigitsKeyListener() {
+ this(false, false);
+ }
+
+ /**
+ * Allocates a DigitsKeyListener that accepts the digits 0 through 9,
+ * plus the minus sign (only at the beginning) and/or decimal point
+ * (only one per field) if specified.
+ */
+ public DigitsKeyListener(boolean sign, boolean decimal) {
+ mSign = sign;
+ mDecimal = decimal;
+
+ int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
+ mAccepted = CHARACTERS[kind];
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts the digits 0 through 9.
+ */
+ public static DigitsKeyListener getInstance() {
+ return getInstance(false, false);
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts the digits 0 through 9,
+ * plus the minus sign (only at the beginning) and/or decimal point
+ * (only one per field) if specified.
+ */
+ public static DigitsKeyListener getInstance(boolean sign, boolean decimal) {
+ int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
+
+ if (sInstance[kind] != null)
+ return sInstance[kind];
+
+ sInstance[kind] = new DigitsKeyListener(sign, decimal);
+ return sInstance[kind];
+ }
+
+ /**
+ * Returns a DigitsKeyListener that accepts only the characters
+ * that appear in the specified String. Note that not all characters
+ * may be available on every keyboard.
+ */
+ public static DigitsKeyListener getInstance(String accepted) {
+ // TODO: do we need a cache of these to avoid allocating?
+
+ DigitsKeyListener dim = new DigitsKeyListener();
+
+ dim.mAccepted = new char[accepted.length()];
+ accepted.getChars(0, accepted.length(), dim.mAccepted, 0);
+
+ return dim;
+ }
+
+ @Override
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ CharSequence out = super.filter(source, start, end, dest, dstart, dend);
+
+ if (mSign == false && mDecimal == false) {
+ return out;
+ }
+
+ if (out != null) {
+ source = out;
+ start = 0;
+ end = out.length();
+ }
+
+ int sign = -1;
+ int decimal = -1;
+ int dlen = dest.length();
+
+ /*
+ * Find out if the existing text has '-' or '.' characters.
+ */
+
+ for (int i = 0; i < dstart; i++) {
+ char c = dest.charAt(i);
+
+ if (c == '-') {
+ sign = i;
+ } else if (c == '.') {
+ decimal = i;
+ }
+ }
+ for (int i = dend; i < dlen; i++) {
+ char c = dest.charAt(i);
+
+ if (c == '-') {
+ return ""; // Nothing can be inserted in front of a '-'.
+ } else if (c == '.') {
+ decimal = i;
+ }
+ }
+
+ /*
+ * If it does, we must strip them out from the source.
+ * In addition, '-' must be the very first character,
+ * and nothing can be inserted before an existing '-'.
+ * Go in reverse order so the offsets are stable.
+ */
+
+ SpannableStringBuilder stripped = null;
+
+ for (int i = end - 1; i >= start; i--) {
+ char c = source.charAt(i);
+ boolean strip = false;
+
+ if (c == '-') {
+ if (i != start || dstart != 0) {
+ strip = true;
+ } else if (sign >= 0) {
+ strip = true;
+ } else {
+ sign = i;
+ }
+ } else if (c == '.') {
+ if (decimal >= 0) {
+ strip = true;
+ } else {
+ decimal = i;
+ }
+ }
+
+ if (strip) {
+ if (end == start + 1) {
+ return ""; // Only one character, and it was stripped.
+ }
+
+ if (stripped == null) {
+ stripped = new SpannableStringBuilder(source, start, end);
+ }
+
+ stripped.delete(i - start, i + 1 - start);
+ }
+ }
+
+ if (stripped != null) {
+ return stripped;
+ } else if (out != null) {
+ return out;
+ } else {
+ return null;
+ }
+ }
+
+ private static DigitsKeyListener[] sInstance = new DigitsKeyListener[4];
+}
diff --git a/core/java/android/text/method/HideReturnsTransformationMethod.java b/core/java/android/text/method/HideReturnsTransformationMethod.java
new file mode 100644
index 0000000..ce18692
--- /dev/null
+++ b/core/java/android/text/method/HideReturnsTransformationMethod.java
@@ -0,0 +1,59 @@
+/*
+ * 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.method;
+
+import android.graphics.Rect;
+import android.text.GetChars;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.view.View;
+
+/**
+ * This transformation method causes any carriage return characters (\r)
+ * to be hidden by displaying them as zero-width non-breaking space
+ * characters (\uFEFF).
+ */
+public class HideReturnsTransformationMethod
+extends ReplacementTransformationMethod {
+ private static char[] ORIGINAL = new char[] { '\r' };
+ private static char[] REPLACEMENT = new char[] { '\uFEFF' };
+
+ /**
+ * The character to be replaced is \r.
+ */
+ protected char[] getOriginal() {
+ return ORIGINAL;
+ }
+
+ /**
+ * The character that \r is replaced with is \uFEFF.
+ */
+ protected char[] getReplacement() {
+ return REPLACEMENT;
+ }
+
+ public static HideReturnsTransformationMethod getInstance() {
+ if (sInstance != null)
+ return sInstance;
+
+ sInstance = new HideReturnsTransformationMethod();
+ return sInstance;
+ }
+
+ private static HideReturnsTransformationMethod sInstance;
+}
diff --git a/core/java/android/text/method/KeyListener.java b/core/java/android/text/method/KeyListener.java
new file mode 100644
index 0000000..05ab72d
--- /dev/null
+++ b/core/java/android/text/method/KeyListener.java
@@ -0,0 +1,42 @@
+/*
+ * 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.method;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.os.Message;
+import android.text.*;
+import android.widget.TextView;
+
+public interface KeyListener
+{
+ /**
+ * If the key listener wants to handle this key, return true,
+ * otherwise return false and the caller (i.e. the widget host)
+ * will handle the key.
+ */
+ public boolean onKeyDown(View view, Editable text,
+ int keyCode, KeyEvent event);
+
+ /**
+ * If the key listener wants to handle this key release, return true,
+ * otherwise return false and the caller (i.e. the widget host)
+ * will handle the key.
+ */
+ public boolean onKeyUp(View view, Editable text,
+ int keyCode, KeyEvent event);
+}
diff --git a/core/java/android/text/method/LinkMovementMethod.java b/core/java/android/text/method/LinkMovementMethod.java
new file mode 100644
index 0000000..92ac531
--- /dev/null
+++ b/core/java/android/text/method/LinkMovementMethod.java
@@ -0,0 +1,256 @@
+/*
+ * 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.method;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.text.*;
+import android.text.style.*;
+import android.view.View;
+import android.widget.TextView;
+
+public class
+LinkMovementMethod
+extends ScrollingMovementMethod
+{
+ private static final int CLICK = 1;
+ private static final int UP = 2;
+ private static final int DOWN = 3;
+
+ @Override
+ public boolean onKeyDown(TextView widget, Spannable buffer,
+ int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ENTER:
+ if (event.getRepeatCount() == 0) {
+ if (action(CLICK, widget, buffer)) {
+ return true;
+ }
+ }
+ }
+
+ return super.onKeyDown(widget, buffer, keyCode, event);
+ }
+
+ @Override
+ protected boolean up(TextView widget, Spannable buffer) {
+ if (action(UP, widget, buffer)) {
+ return true;
+ }
+
+ return super.up(widget, buffer);
+ }
+
+ @Override
+ protected boolean down(TextView widget, Spannable buffer) {
+ if (action(DOWN, widget, buffer)) {
+ return true;
+ }
+
+ return super.down(widget, buffer);
+ }
+
+ @Override
+ protected boolean left(TextView widget, Spannable buffer) {
+ if (action(UP, widget, buffer)) {
+ return true;
+ }
+
+ return super.left(widget, buffer);
+ }
+
+ @Override
+ protected boolean right(TextView widget, Spannable buffer) {
+ if (action(DOWN, widget, buffer)) {
+ return true;
+ }
+
+ return super.right(widget, buffer);
+ }
+
+ private boolean action(int what, TextView widget, Spannable buffer) {
+ boolean handled = false;
+
+ Layout layout = widget.getLayout();
+
+ int padding = widget.getTotalPaddingTop() +
+ widget.getTotalPaddingBottom();
+ int areatop = widget.getScrollY();
+ int areabot = areatop + widget.getHeight() - padding;
+
+ int linetop = layout.getLineForVertical(areatop);
+ int linebot = layout.getLineForVertical(areabot);
+
+ int first = layout.getLineStart(linetop);
+ int last = layout.getLineEnd(linebot);
+
+ ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
+
+ int a = Selection.getSelectionStart(buffer);
+ int b = Selection.getSelectionEnd(buffer);
+
+ int selStart = Math.min(a, b);
+ int selEnd = Math.max(a, b);
+
+ if (selStart < 0) {
+ if (buffer.getSpanStart(FROM_BELOW) >= 0) {
+ selStart = selEnd = buffer.length();
+ }
+ }
+
+ if (selStart > last)
+ selStart = selEnd = Integer.MAX_VALUE;
+ if (selEnd < first)
+ selStart = selEnd = -1;
+
+ switch (what) {
+ case CLICK:
+ if (selStart == selEnd) {
+ return false;
+ }
+
+ ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
+
+ if (link.length != 1)
+ return false;
+
+ link[0].onClick(widget);
+ break;
+
+ case UP:
+ int beststart, bestend;
+
+ beststart = -1;
+ bestend = -1;
+
+ for (int i = 0; i < candidates.length; i++) {
+ int end = buffer.getSpanEnd(candidates[i]);
+
+ if (end < selEnd || selStart == selEnd) {
+ if (end > bestend) {
+ beststart = buffer.getSpanStart(candidates[i]);
+ bestend = end;
+ }
+ }
+ }
+
+ if (beststart >= 0) {
+ Selection.setSelection(buffer, bestend, beststart);
+ return true;
+ }
+
+ break;
+
+ case DOWN:
+ beststart = Integer.MAX_VALUE;
+ bestend = Integer.MAX_VALUE;
+
+ for (int i = 0; i < candidates.length; i++) {
+ int start = buffer.getSpanStart(candidates[i]);
+
+ if (start > selStart || selStart == selEnd) {
+ if (start < beststart) {
+ beststart = start;
+ bestend = buffer.getSpanEnd(candidates[i]);
+ }
+ }
+ }
+
+ if (bestend < Integer.MAX_VALUE) {
+ Selection.setSelection(buffer, beststart, bestend);
+ return true;
+ }
+
+ break;
+ }
+
+ return false;
+ }
+
+ public boolean onKeyUp(TextView widget, Spannable buffer,
+ int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(TextView widget, Spannable buffer,
+ MotionEvent event) {
+ int action = event.getAction();
+
+ if (action == MotionEvent.ACTION_UP ||
+ action == MotionEvent.ACTION_DOWN) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= widget.getTotalPaddingLeft();
+ y -= widget.getTotalPaddingTop();
+
+ x += widget.getScrollX();
+ y += widget.getScrollY();
+
+ Layout layout = widget.getLayout();
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
+
+ if (link.length != 0) {
+ if (action == MotionEvent.ACTION_UP) {
+ link[0].onClick(widget);
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ Selection.setSelection(buffer,
+ buffer.getSpanStart(link[0]),
+ buffer.getSpanEnd(link[0]));
+ }
+
+ return true;
+ } else {
+ Selection.removeSelection(buffer);
+ }
+ }
+
+ return super.onTouchEvent(widget, buffer, event);
+ }
+
+ public void initialize(TextView widget, Spannable text) {
+ Selection.removeSelection(text);
+ text.removeSpan(FROM_BELOW);
+ }
+
+ public void onTakeFocus(TextView view, Spannable text, int dir) {
+ Selection.removeSelection(text);
+
+ if ((dir & View.FOCUS_BACKWARD) != 0) {
+ text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
+ } else {
+ text.removeSpan(FROM_BELOW);
+ }
+ }
+
+ public static MovementMethod getInstance() {
+ if (sInstance == null)
+ sInstance = new LinkMovementMethod();
+
+ return sInstance;
+ }
+
+ private static LinkMovementMethod sInstance;
+ private static Object FROM_BELOW = new Object();
+}
diff --git a/core/java/android/text/method/MetaKeyKeyListener.java b/core/java/android/text/method/MetaKeyKeyListener.java
new file mode 100644
index 0000000..2d75b87
--- /dev/null
+++ b/core/java/android/text/method/MetaKeyKeyListener.java
@@ -0,0 +1,250 @@
+/*
+ * 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.method;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.text.*;
+
+/**
+ * This base class encapsulates the behavior for handling the meta keys
+ * (caps, fn, sym). Key listener that care about meta state should
+ * inherit from it; you should not instantiate this class directly in a client.
+ */
+
+public abstract class MetaKeyKeyListener {
+ public static final int META_SHIFT_ON = KeyEvent.META_SHIFT_ON;
+ public static final int META_ALT_ON = KeyEvent.META_ALT_ON;
+ public static final int META_SYM_ON = KeyEvent.META_SYM_ON;
+
+ public static final int META_CAP_LOCKED = KeyEvent.META_SHIFT_ON << 8;
+ public static final int META_ALT_LOCKED = KeyEvent.META_ALT_ON << 8;
+ public static final int META_SYM_LOCKED = KeyEvent.META_SYM_ON << 8;
+
+ private static final Object CAP = new Object();
+ private static final Object ALT = new Object();
+ private static final Object SYM = new Object();
+
+ /**
+ * Resets all meta state to inactive.
+ */
+ public static void resetMetaState(Spannable text) {
+ text.removeSpan(CAP);
+ text.removeSpan(ALT);
+ text.removeSpan(SYM);
+ }
+
+ /**
+ * Gets the state of the meta keys.
+ *
+ * @param text the buffer in which the meta key would have been pressed.
+ *
+ * @return an integer in which each bit set to one represents a pressed
+ * or locked meta key.
+ */
+ public static final int getMetaState(CharSequence text) {
+ return getActive(text, CAP, META_SHIFT_ON, META_CAP_LOCKED) |
+ getActive(text, ALT, META_ALT_ON, META_ALT_LOCKED) |
+ getActive(text, SYM, META_SYM_ON, META_SYM_LOCKED);
+ }
+
+ /**
+ * Gets the state of a particular meta key.
+ *
+ * @param meta META_SHIFT_ON, META_ALT_ON, or META_SYM_ON
+ * @param text the buffer in which the meta key would have been pressed.
+ *
+ * @return 0 if inactive, 1 if active, 2 if locked.
+ */
+ public static final int getMetaState(CharSequence text, int meta) {
+ switch (meta) {
+ case META_SHIFT_ON:
+ return getActive(text, CAP, 1, 2);
+
+ case META_ALT_ON:
+ return getActive(text, ALT, 1, 2);
+
+ case META_SYM_ON:
+ return getActive(text, SYM, 1, 2);
+
+ default:
+ return 0;
+ }
+ }
+
+ private static int getActive(CharSequence text, Object meta,
+ int on, int lock) {
+ if (!(text instanceof Spanned)) {
+ return 0;
+ }
+
+ Spanned sp = (Spanned) text;
+ int flag = sp.getSpanFlags(meta);
+
+ if (flag == LOCKED) {
+ return lock;
+ } else if (flag != 0) {
+ return on;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Call this method after you handle a keypress so that the meta
+ * state will be reset to unshifted (if it is not still down)
+ * or primed to be reset to unshifted (once it is released).
+ */
+ public static void adjustMetaAfterKeypress(Spannable content) {
+ adjust(content, CAP);
+ adjust(content, ALT);
+ adjust(content, SYM);
+ }
+
+ /**
+ * Returns true if this object is one that this class would use to
+ * keep track of meta state in the specified text.
+ */
+ public static boolean isMetaTracker(CharSequence text, Object what) {
+ return what == CAP || what == ALT || what == SYM;
+ }
+
+ private static void adjust(Spannable content, Object what) {
+ int current = content.getSpanFlags(what);
+
+ if (current == PRESSED)
+ content.setSpan(what, 0, 0, USED);
+ else if (current == RELEASED)
+ content.removeSpan(what);
+ }
+
+ /**
+ * Call this if you are a method that ignores the locked meta state
+ * (arrow keys, for example) and you handle a key.
+ */
+ protected static void resetLockedMeta(Spannable content) {
+ resetLock(content, CAP);
+ resetLock(content, ALT);
+ resetLock(content, SYM);
+ }
+
+ private static void resetLock(Spannable content, Object what) {
+ int current = content.getSpanFlags(what);
+
+ if (current == LOCKED)
+ content.removeSpan(what);
+ }
+
+ /**
+ * Handles presses of the meta keys.
+ */
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
+ press(content, CAP);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ || keyCode == KeyEvent.KEYCODE_NUM) {
+ press(content, ALT);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_SYM) {
+ press(content, SYM);
+ return true;
+ }
+
+ return false; // no super to call through to
+ }
+
+ private void press(Editable content, Object what) {
+ int state = content.getSpanFlags(what);
+
+ if (state == PRESSED)
+ ; // repeat before use
+ else if (state == RELEASED)
+ content.setSpan(what, 0, 0, LOCKED);
+ else if (state == USED)
+ ; // repeat after use
+ else if (state == LOCKED)
+ content.removeSpan(what);
+ else
+ content.setSpan(what, 0, 0, PRESSED);
+ }
+
+ /**
+ * Handles release of the meta keys.
+ */
+ public boolean onKeyUp(View view, Editable content, int keyCode,
+ KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
+ release(content, CAP);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT
+ || keyCode == KeyEvent.KEYCODE_NUM) {
+ release(content, ALT);
+ return true;
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_SYM) {
+ release(content, SYM);
+ return true;
+ }
+
+ return false; // no super to call through to
+ }
+
+ private void release(Editable content, Object what) {
+ int current = content.getSpanFlags(what);
+
+ if (current == USED)
+ content.removeSpan(what);
+ else if (current == PRESSED)
+ content.setSpan(what, 0, 0, RELEASED);
+ }
+
+ /**
+ * The meta key has been pressed but has not yet been used.
+ */
+ private static final int PRESSED =
+ Spannable.SPAN_MARK_MARK | (1 << Spannable.SPAN_USER_SHIFT);
+
+ /**
+ * The meta key has been pressed and released but has still
+ * not yet been used.
+ */
+ private static final int RELEASED =
+ Spannable.SPAN_MARK_MARK | (2 << Spannable.SPAN_USER_SHIFT);
+
+ /**
+ * The meta key has been pressed and used but has not yet been released.
+ */
+ private static final int USED =
+ Spannable.SPAN_MARK_MARK | (3 << Spannable.SPAN_USER_SHIFT);
+
+ /**
+ * The meta key has been pressed and released without use, and then
+ * pressed again; it may also have been released again.
+ */
+ private static final int LOCKED =
+ Spannable.SPAN_MARK_MARK | (4 << Spannable.SPAN_USER_SHIFT);
+}
+
diff --git a/core/java/android/text/method/MovementMethod.java b/core/java/android/text/method/MovementMethod.java
new file mode 100644
index 0000000..9e37e59
--- /dev/null
+++ b/core/java/android/text/method/MovementMethod.java
@@ -0,0 +1,43 @@
+/*
+ * 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.method;
+
+import android.widget.TextView;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.text.*;
+
+public interface MovementMethod
+{
+ public void initialize(TextView widget, Spannable text);
+ public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event);
+ public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event);
+ public void onTakeFocus(TextView widget, Spannable text, int direction);
+ public boolean onTrackballEvent(TextView widget, Spannable text,
+ MotionEvent event);
+ public boolean onTouchEvent(TextView widget, Spannable text,
+ MotionEvent event);
+
+ /**
+ * Returns true if this movement method allows arbitrary selection
+ * of any text; false if it has no selection (like a movement method
+ * that only scrolls) or a constrained selection (for example
+ * limited to links. The "Select All" menu item is disabled
+ * if arbitrary selection is not allowed.
+ */
+ public boolean canSelectArbitrarily();
+}
diff --git a/core/java/android/text/method/MultiTapKeyListener.java b/core/java/android/text/method/MultiTapKeyListener.java
new file mode 100644
index 0000000..7137d40
--- /dev/null
+++ b/core/java/android/text/method/MultiTapKeyListener.java
@@ -0,0 +1,285 @@
+/*
+ * 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.method;
+
+import android.view.KeyEvent;
+import android.view.View;
+import android.os.Message;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.text.*;
+import android.text.method.TextKeyListener.Capitalize;
+import android.widget.TextView;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+/**
+ * This is the standard key listener for alphabetic input on 12-key
+ * keyboards. You should generally not need to instantiate this yourself;
+ * TextKeyListener will do it for you.
+ */
+public class MultiTapKeyListener extends BaseKeyListener
+ implements SpanWatcher {
+ private static MultiTapKeyListener[] sInstance =
+ new MultiTapKeyListener[Capitalize.values().length * 2];
+
+ private static final SparseArraycontent
as having
+ * contained original
prior to AutoText replacement.
+ * Call this method when you have done or are about to do an
+ * AutoText-style replacement on a region of text and want to let
+ * the same mechanism (the user pressing DEL immediately after the
+ * change) undo the replacement.
+ *
+ * @param content the Editable text where the replacement was made
+ * @param start the start of the replaced region
+ * @param end the end of the replaced region; the location of the cursor
+ * @param original the text to be restored if the user presses DEL
+ */
+ public static void markAsReplaced(Spannable content, int start, int end,
+ String original) {
+ Replaced[] repl = content.getSpans(0, content.length(), Replaced.class);
+ for (int a = 0; a < repl.length; a++) {
+ content.removeSpan(repl[a]);
+ }
+
+ int len = original.length();
+ char[] orig = new char[len];
+ original.getChars(0, len, orig, 0);
+
+ content.setSpan(new Replaced(orig), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static SparseArray
+// Set the text to password display style:
+EditText txtView = (EditText)findViewById(R.id.text);
+txtView.setTransformationMethod(PasswordTransformationMethod.getInstance());
+
+//Set the input style to numbers, rather than qwerty keyboard style.
+txtView.setInputMethod(DigitsInputMethod.getInstance());
+
+// Find out whether the caps lock is on.
+// 0 is no, 1 is yes, 2 is caps lock on.
+int active = MultiTapInputMethod.getCapsActive(txtView.getText());
+
+
+
diff --git a/core/java/android/text/package.html b/core/java/android/text/package.html
new file mode 100644
index 0000000..162dcd8
--- /dev/null
+++ b/core/java/android/text/package.html
@@ -0,0 +1,13 @@
+
+
+
+appearance
should be, for example,
+ * android.R.style.TextAppearance_Small
.
+ */
+ public TextAppearanceSpan(Context context, int appearance) {
+ this(context, appearance, -1);
+ }
+
+ /**
+ * Uses the specified TextAppearance resource to determine the
+ * text appearance, and the specified text color resource
+ * to determine the color. The appearance
should be,
+ * for example, android.R.style.TextAppearance_Small
,
+ * and the colorList
should be, for example,
+ * android.R.styleable.Theme_textColorDim
.
+ */
+ public TextAppearanceSpan(Context context, int appearance,
+ int colorList) {
+ TypedArray a =
+ context.obtainStyledAttributes(appearance,
+ com.android.internal.R.styleable.TextAppearance);
+
+ mTextColor = a.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColor);
+ mTextColorLink = a.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColorLink);
+ mTextSize = a.getDimensionPixelSize(com.android.internal.R.styleable.
+ TextAppearance_textSize, -1);
+
+ mStyle = a.getInt(com.android.internal.R.styleable.TextAppearance_textStyle, 0);
+ int tf = a.getInt(com.android.internal.R.styleable.TextAppearance_typeface, 0);
+
+ switch (tf) {
+ case 1:
+ mTypeface = "sans";
+ break;
+
+ case 2:
+ mTypeface = "serif";
+ break;
+
+ case 3:
+ mTypeface = "monospace";
+ break;
+ }
+
+ a.recycle();
+
+ if (colorList >= 0) {
+ a = context.obtainStyledAttributes(com.android.internal.R.style.Theme,
+ com.android.internal.R.styleable.Theme);
+
+ mTextColor = a.getColorStateList(colorList);
+ a.recycle();
+ }
+ }
+
+ /**
+ * Makes text be drawn with the specified typeface, size, style,
+ * and colors.
+ */
+ public TextAppearanceSpan(String family, int style, int size,
+ ColorStateList color, ColorStateList linkColor) {
+ mTypeface = family;
+ mStyle = style;
+ mTextSize = size;
+ mTextColor = color;
+ mTextColorLink = linkColor;
+ }
+
+ /**
+ * Returns the typeface family specified by this span, or null
+ * if it does not specify one.
+ */
+ public String getFamily() {
+ return mTypeface;
+ }
+
+ /**
+ * Returns the text color specified by this span, or null
+ * if it does not specify one.
+ */
+ public ColorStateList getTextColor() {
+ return mTextColor;
+ }
+
+ /**
+ * Returns the link color specified by this span, or null
+ * if it does not specify one.
+ */
+ public ColorStateList getLinkTextColor() {
+ return mTextColorLink;
+ }
+
+ /**
+ * Returns the text size specified by this span, or -1
+ * if it does not specify one.
+ */
+ public int getTextSize() {
+ return mTextSize;
+ }
+
+ /**
+ * Returns the text style specified by this span, or 0
+ * if it does not specify one.
+ */
+ public int getTextStyle() {
+ return mStyle;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ updateMeasureState(ds);
+
+ if (mTextColor != null) {
+ ds.setColor(mTextColor.getColorForState(ds.drawableState, 0));
+ }
+
+ if (mTextColorLink != null) {
+ ds.linkColor = mTextColor.getColorForState(ds.drawableState, 0);
+ }
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint ds) {
+ if (mTypeface != null || mStyle != 0) {
+ Typeface tf = ds.getTypeface();
+ int style = 0;
+
+ if (tf != null) {
+ style = tf.getStyle();
+ }
+
+ style |= mStyle;
+
+ if (mTypeface != null) {
+ tf = Typeface.create(mTypeface, style);
+ } else if (tf == null) {
+ tf = Typeface.defaultFromStyle(style);
+ } else {
+ tf = Typeface.create(tf, style);
+ }
+
+ int fake = style & ~tf.getStyle();
+
+ if ((fake & Typeface.BOLD) != 0) {
+ ds.setFakeBoldText(true);
+ }
+
+ if ((fake & Typeface.ITALIC) != 0) {
+ ds.setTextSkewX(-0.25f);
+ }
+
+ ds.setTypeface(tf);
+ }
+
+ if (mTextSize > 0) {
+ ds.setTextSize(mTextSize);
+ }
+ }
+}
diff --git a/core/java/android/text/style/TypefaceSpan.java b/core/java/android/text/style/TypefaceSpan.java
new file mode 100644
index 0000000..7519ac2
--- /dev/null
+++ b/core/java/android/text/style/TypefaceSpan.java
@@ -0,0 +1,77 @@
+/*
+ * 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.style;
+
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+
+/**
+ * Changes the typeface family of the text to which the span is attached.
+ */
+public class TypefaceSpan extends MetricAffectingSpan {
+ private String mFamily;
+
+ /**
+ * @param family The font family for this typeface. Examples include
+ * "monospace", "serif", and "sans-serif".
+ */
+ public TypefaceSpan(String family) {
+ mFamily = family;
+ }
+
+ /**
+ * Returns the font family name.
+ */
+ public String getFamily() {
+ return mFamily;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ apply(ds, mFamily);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint paint) {
+ apply(paint, mFamily);
+ }
+
+ private static void apply(Paint paint, String family) {
+ int oldStyle;
+
+ Typeface old = paint.getTypeface();
+ if (old == null) {
+ oldStyle = 0;
+ } else {
+ oldStyle = old.getStyle();
+ }
+
+ Typeface tf = Typeface.create(family, oldStyle);
+ int fake = oldStyle & ~tf.getStyle();
+
+ if ((fake & Typeface.BOLD) != 0) {
+ paint.setFakeBoldText(true);
+ }
+
+ if ((fake & Typeface.ITALIC) != 0) {
+ paint.setTextSkewX(-0.25f);
+ }
+
+ paint.setTypeface(tf);
+ }
+}
diff --git a/core/java/android/text/style/URLSpan.java b/core/java/android/text/style/URLSpan.java
new file mode 100644
index 0000000..79809b5
--- /dev/null
+++ b/core/java/android/text/style/URLSpan.java
@@ -0,0 +1,43 @@
+/*
+ * 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.style;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.text.TextPaint;
+import android.view.View;
+
+public class URLSpan extends ClickableSpan {
+
+ private String mURL;
+
+ public URLSpan(String url) {
+ mURL = url;
+ }
+
+ public String getURL() {
+ return mURL;
+ }
+
+ @Override
+ public void onClick(View widget) {
+ Uri uri = Uri.parse(getURL());
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.addCategory(Intent.CATEGORY_BROWSABLE);
+ widget.getContext().startActivity(intent);
+ }
+}
diff --git a/core/java/android/text/style/UnderlineSpan.java b/core/java/android/text/style/UnderlineSpan.java
new file mode 100644
index 0000000..5dce0f2
--- /dev/null
+++ b/core/java/android/text/style/UnderlineSpan.java
@@ -0,0 +1,27 @@
+/*
+ * 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.style;
+
+import android.text.TextPaint;
+
+public class UnderlineSpan extends CharacterStyle {
+
+ @Override
+ public void updateDrawState(TextPaint ds) {
+ ds.setUnderlineText(true);
+ }
+}
diff --git a/core/java/android/text/style/UpdateLayout.java b/core/java/android/text/style/UpdateLayout.java
new file mode 100644
index 0000000..211685a
--- /dev/null
+++ b/core/java/android/text/style/UpdateLayout.java
@@ -0,0 +1,24 @@
+/*
+ * 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.style;
+
+/**
+ * The classes that affect character-level text formatting in a way that
+ * triggers a text layout update when one is added or remove must implement
+ * this interface.
+ */
+public interface UpdateLayout { }
diff --git a/core/java/android/text/style/WrapTogetherSpan.java b/core/java/android/text/style/WrapTogetherSpan.java
new file mode 100644
index 0000000..11721a8
--- /dev/null
+++ b/core/java/android/text/style/WrapTogetherSpan.java
@@ -0,0 +1,23 @@
+/*
+ * 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.style;
+
+public interface WrapTogetherSpan
+extends ParagraphStyle
+{
+
+}
diff --git a/core/java/android/text/style/package.html b/core/java/android/text/style/package.html
new file mode 100644
index 0000000..0a8520c
--- /dev/null
+++ b/core/java/android/text/style/package.html
@@ -0,0 +1,10 @@
+
+http://
. If the pattern matches example.com, which
+ * does not have a url scheme prefix, the supplied scheme will be prepended to
+ * create http://example.com
when the clickable url link is
+ * created.
+ */
+
+public class Linkify {
+ /**
+ * Bit field indicating that web URLs should be matched in methods that
+ * take an options mask
+ */
+ public static final int WEB_URLS = 0x01;
+
+ /**
+ * Bit field indicating that email addresses should be matched in methods
+ * that take an options mask
+ */
+ public static final int EMAIL_ADDRESSES = 0x02;
+
+ /**
+ * Bit field indicating that phone numbers should be matched in methods that
+ * take an options mask
+ */
+ public static final int PHONE_NUMBERS = 0x04;
+
+ /**
+ * Bit field indicating that phone numbers should be matched in methods that
+ * take an options mask
+ */
+ public static final int MAP_ADDRESSES = 0x08;
+
+ /**
+ * Bit mask indicating that all available patterns should be matched in
+ * methods that take an options mask
+ */
+ public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS
+ | MAP_ADDRESSES;
+
+ /**
+ * Don't treat anything with fewer than this many digits as a
+ * phone number.
+ */
+ private static final int PHONE_NUMBER_MINIMUM_DIGITS = 5;
+
+ /**
+ * Filters out web URL matches that occur after an at-sign (@). This is
+ * to prevent turning the domain name in an email address into a web link.
+ */
+ public static final MatchFilter sUrlMatchFilter = new MatchFilter() {
+ public final boolean acceptMatch(CharSequence s, int start, int end) {
+ if (start == 0) {
+ return true;
+ }
+
+ if (s.charAt(start - 1) == '@') {
+ return false;
+ }
+
+ return true;
+ }
+ };
+
+ /**
+ * Filters out URL matches that don't have enough digits to be a
+ * phone number.
+ */
+ public static final MatchFilter sPhoneNumberMatchFilter =
+ new MatchFilter() {
+ public final boolean acceptMatch(CharSequence s, int start, int end) {
+ int digitCount = 0;
+
+ for (int i = start; i < end; i++) {
+ if (Character.isDigit(s.charAt(i))) {
+ digitCount++;
+ if (digitCount >= PHONE_NUMBER_MINIMUM_DIGITS) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ };
+
+ /**
+ * Transforms matched phone number text into something suitable
+ * to be used in a tel: URL. It does this by removing everything
+ * but the digits and plus signs. For instance:
+ * '+1 (919) 555-1212'
+ * becomes '+19195551212'
+ */
+ public static final TransformFilter sPhoneNumberTransformFilter =
+ new TransformFilter() {
+ public final String transformUrl(final Matcher match, String url) {
+ return Regex.digitsAndPlusOnly(match);
+ }
+ };
+
+ /**
+ * MatchFilter enables client code to have more control over
+ * what is allowed to match and become a link, and what is not.
+ *
+ * For example: when matching web urls you would like things like
+ * http://www.example.com to match, as well as just example.com itelf.
+ * However, you would not want to match against the domain in
+ * support@example.com. So, when matching against a web url pattern you
+ * might also include a MatchFilter that disallows the match if it is
+ * immediately preceded by an at-sign (@).
+ */
+ public interface MatchFilter {
+ /**
+ * Examines the character span matched by the pattern and determines
+ * if the match should be turned into an actionable link.
+ *
+ * @param s The body of text against which the pattern
+ * was matched
+ * @param start The index of the first character in s that was
+ * matched by the pattern - inclusive
+ * @param end The index of the last character in s that was
+ * matched - exclusive
+ *
+ * @return Whether this match should be turned into a link
+ */
+ boolean acceptMatch(CharSequence s, int start, int end);
+ }
+
+ /**
+ * TransformFilter enables client code to have more control over
+ * how matched patterns are represented as URLs.
+ *
+ * For example: when converting a phone number such as (919) 555-1212
+ * into a tel: URL the parentheses, white space, and hyphen need to be
+ * removed to produce tel:9195551212.
+ */
+ public interface TransformFilter {
+ /**
+ * Examines the matched text and either passes it through or uses the
+ * data in the Matcher state to produce a replacement.
+ *
+ * @param match The regex matcher state that found this URL text
+ * @param url The text that was matched
+ *
+ * @return The transformed form of the URL
+ */
+ String transformUrl(final Matcher match, String url);
+ }
+
+ /**
+ * Scans the text of the provided Spannable and turns all occurrences
+ * of the link types indicated in the mask into clickable links.
+ * If the mask is nonzero, it also removes any existing URLSpans
+ * attached to the Spannable, to avoid problems if you call it
+ * repeatedly on the same text.
+ */
+ public static final boolean addLinks(Spannable text, int mask) {
+ if (mask == 0) {
+ return false;
+ }
+
+ URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
+
+ for (int i = old.length - 1; i >= 0; i--) {
+ text.removeSpan(old[i]);
+ }
+
+ ArrayListhttp://
to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ */
+ public static final void addLinks(TextView text, Pattern pattern,
+ String scheme) {
+ addLinks(text, pattern, scheme, null, null);
+ }
+
+ /**
+ * Applies a regex to the text of a TextView turning the matches into
+ * links. If links are found then UrlSpans are applied to the link
+ * text match areas, and the movement method for the text is changed
+ * to LinkMovementMethod.
+ *
+ * @param text TextView whose text is to be marked-up with links
+ * @param p Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg http://
to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ * @param matchFilter The filter that is used to allow the client code
+ * additional control over which pattern matches are
+ * to be converted into links.
+ */
+ public static final void addLinks(TextView text, Pattern p, String scheme,
+ MatchFilter matchFilter, TransformFilter transformFilter) {
+ SpannableString s = SpannableString.valueOf(text.getText());
+
+ if (addLinks(s, p, scheme, matchFilter, transformFilter)) {
+ text.setText(s);
+ addLinkMovementMethod(text);
+ }
+ }
+
+ /**
+ * Applies a regex to a Spannable turning the matches into
+ * links.
+ *
+ * @param text Spannable whose text is to be marked-up with
+ * links
+ * @param pattern Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg http://
to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ */
+ public static final boolean addLinks(Spannable text, Pattern pattern,
+ String scheme) {
+ return addLinks(text, pattern, scheme, null, null);
+ }
+
+ /**
+ * Applies a regex to a Spannable turning the matches into
+ * links.
+ *
+ * @param s Spannable whose text is to be marked-up with
+ * links
+ * @param p Regex pattern to be used for finding links
+ * @param scheme Url scheme string (eg http://
to be
+ * prepended to the url of links that do not have
+ * a scheme specified in the link text
+ * @param matchFilter The filter that is used to allow the client code
+ * additional control over which pattern matches are
+ * to be converted into links.
+ */
+ public static final boolean addLinks(Spannable s, Pattern p,
+ String scheme, MatchFilter matchFilter,
+ TransformFilter transformFilter) {
+ boolean hasMatches = false;
+ String prefix = (scheme == null) ? "" : scheme.toLowerCase();
+ Matcher m = p.matcher(s);
+
+ while (m.find()) {
+ int start = m.start();
+ int end = m.end();
+ boolean allowed = true;
+
+ if (matchFilter != null) {
+ allowed = matchFilter.acceptMatch(s, start, end);
+ }
+
+ if (allowed) {
+ String url = makeUrl(m.group(0), new String[] { prefix },
+ m, transformFilter);
+
+ applyLink(url, start, end, s);
+ hasMatches = true;
+ }
+ }
+
+ return hasMatches;
+ }
+
+ private static final void applyLink(String url, int start, int end,
+ Spannable text) {
+ URLSpan span = new URLSpan(url);
+
+ text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static final String makeUrl(String url, String[] prefixes,
+ Matcher m, TransformFilter filter) {
+ if (filter != null) {
+ url = filter.transformUrl(m, url);
+ }
+
+ boolean hasPrefix = false;
+ for (int i = 0; i < prefixes.length; i++) {
+ if (url.regionMatches(true, 0, prefixes[i], 0,
+ prefixes[i].length())) {
+ hasPrefix = true;
+ break;
+ }
+ }
+ if (!hasPrefix) {
+ url = prefixes[0] + url;
+ }
+
+ return url;
+ }
+
+ private static final void gatherLinks(ArrayList