| /* |
| * 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.widget; |
| |
| import android.content.Context; |
| import android.text.Editable; |
| import android.text.SpannableString; |
| import android.text.Spanned; |
| import android.text.TextUtils; |
| import android.text.method.QwertyKeyListener; |
| import android.util.AttributeSet; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| |
| /** |
| * An editable text view, extending {@link AutoCompleteTextView}, that |
| * can show completion suggestions for the substring of the text where |
| * the user is typing instead of necessarily for the entire thing. |
| * <p> |
| * You must provide a {@link Tokenizer} to distinguish the |
| * various substrings. |
| * |
| * <p>The following code snippet shows how to create a text view which suggests |
| * various countries names while the user is typing:</p> |
| * |
| * <pre class="prettyprint"> |
| * public class CountriesActivity extends Activity { |
| * protected void onCreate(Bundle savedInstanceState) { |
| * super.onCreate(savedInstanceState); |
| * setContentView(R.layout.autocomplete_7); |
| * |
| * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, |
| * android.R.layout.simple_dropdown_item_1line, COUNTRIES); |
| * MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit); |
| * textView.setAdapter(adapter); |
| * textView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer()); |
| * } |
| * |
| * private static final String[] COUNTRIES = new String[] { |
| * "Belgium", "France", "Italy", "Germany", "Spain" |
| * }; |
| * }</pre> |
| */ |
| |
| public class MultiAutoCompleteTextView extends AutoCompleteTextView { |
| private Tokenizer mTokenizer; |
| |
| public MultiAutoCompleteTextView(Context context) { |
| this(context, null); |
| } |
| |
| public MultiAutoCompleteTextView(Context context, AttributeSet attrs) { |
| this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); |
| } |
| |
| public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { |
| this(context, attrs, defStyleAttr, 0); |
| } |
| |
| public MultiAutoCompleteTextView( |
| Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| } |
| |
| /* package */ void finishInit() { } |
| |
| /** |
| * Sets the Tokenizer that will be used to determine the relevant |
| * range of the text where the user is typing. |
| */ |
| public void setTokenizer(Tokenizer t) { |
| mTokenizer = t; |
| } |
| |
| /** |
| * Instead of filtering on the entire contents of the edit box, |
| * this subclass method filters on the range from |
| * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} |
| * if the length of that range meets or exceeds {@link #getThreshold}. |
| */ |
| @Override |
| protected void performFiltering(CharSequence text, int keyCode) { |
| if (enoughToFilter()) { |
| int end = getSelectionEnd(); |
| int start = mTokenizer.findTokenStart(text, end); |
| |
| performFiltering(text, start, end, keyCode); |
| } else { |
| dismissDropDown(); |
| |
| Filter f = getFilter(); |
| if (f != null) { |
| f.filter(null); |
| } |
| } |
| } |
| |
| /** |
| * Instead of filtering whenever the total length of the text |
| * exceeds the threshhold, this subclass filters only when the |
| * length of the range from |
| * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} |
| * meets or exceeds {@link #getThreshold}. |
| */ |
| @Override |
| public boolean enoughToFilter() { |
| Editable text = getText(); |
| |
| int end = getSelectionEnd(); |
| if (end < 0 || mTokenizer == null) { |
| return false; |
| } |
| |
| int start = mTokenizer.findTokenStart(text, end); |
| |
| if (end - start >= getThreshold()) { |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * Instead of validating the entire text, this subclass method validates |
| * each token of the text individually. Empty tokens are removed. |
| */ |
| @Override |
| public void performValidation() { |
| Validator v = getValidator(); |
| |
| if (v == null || mTokenizer == null) { |
| return; |
| } |
| |
| Editable e = getText(); |
| int i = getText().length(); |
| while (i > 0) { |
| int start = mTokenizer.findTokenStart(e, i); |
| int end = mTokenizer.findTokenEnd(e, start); |
| |
| CharSequence sub = e.subSequence(start, end); |
| if (TextUtils.isEmpty(sub)) { |
| e.replace(start, i, ""); |
| } else if (!v.isValid(sub)) { |
| e.replace(start, i, |
| mTokenizer.terminateToken(v.fixText(sub))); |
| } |
| |
| i = start; |
| } |
| } |
| |
| /** |
| * <p>Starts filtering the content of the drop down list. The filtering |
| * pattern is the specified range of text from the edit box. Subclasses may |
| * override this method to filter with a different pattern, for |
| * instance a smaller substring of <code>text</code>.</p> |
| */ |
| protected void performFiltering(CharSequence text, int start, int end, |
| int keyCode) { |
| getFilter().filter(text.subSequence(start, end), this); |
| } |
| |
| /** |
| * <p>Performs the text completion by replacing the range from |
| * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the |
| * the result of passing <code>text</code> through |
| * {@link Tokenizer#terminateToken}. |
| * In addition, the replaced region will be marked as an AutoText |
| * substition so that if the user immediately presses DEL, the |
| * completion will be undone. |
| * Subclasses may override this method to do some different |
| * insertion of the content into the edit box.</p> |
| * |
| * @param text the selected suggestion in the drop down list |
| */ |
| @Override |
| protected void replaceText(CharSequence text) { |
| clearComposingText(); |
| |
| int end = getSelectionEnd(); |
| int start = mTokenizer.findTokenStart(getText(), end); |
| |
| Editable editable = getText(); |
| String original = TextUtils.substring(editable, start, end); |
| |
| QwertyKeyListener.markAsReplaced(editable, start, end, original); |
| editable.replace(start, end, mTokenizer.terminateToken(text)); |
| } |
| |
| @Override |
| public void onInitializeAccessibilityEvent(AccessibilityEvent event) { |
| super.onInitializeAccessibilityEvent(event); |
| event.setClassName(MultiAutoCompleteTextView.class.getName()); |
| } |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(info); |
| info.setClassName(MultiAutoCompleteTextView.class.getName()); |
| } |
| |
| public static interface Tokenizer { |
| /** |
| * Returns the start of the token that ends at offset |
| * <code>cursor</code> within <code>text</code>. |
| */ |
| public int findTokenStart(CharSequence text, int cursor); |
| |
| /** |
| * Returns the end of the token (minus trailing punctuation) |
| * that begins at offset <code>cursor</code> within <code>text</code>. |
| */ |
| public int findTokenEnd(CharSequence text, int cursor); |
| |
| /** |
| * Returns <code>text</code>, modified, if necessary, to ensure that |
| * it ends with a token terminator (for example a space or comma). |
| */ |
| public CharSequence terminateToken(CharSequence text); |
| } |
| |
| /** |
| * This simple Tokenizer can be used for lists where the items are |
| * separated by a comma and one or more spaces. |
| */ |
| public static class CommaTokenizer implements Tokenizer { |
| public int findTokenStart(CharSequence text, int cursor) { |
| int i = cursor; |
| |
| while (i > 0 && text.charAt(i - 1) != ',') { |
| i--; |
| } |
| while (i < cursor && text.charAt(i) == ' ') { |
| i++; |
| } |
| |
| return i; |
| } |
| |
| public int findTokenEnd(CharSequence text, int cursor) { |
| int i = cursor; |
| int len = text.length(); |
| |
| while (i < len) { |
| if (text.charAt(i) == ',') { |
| return i; |
| } else { |
| i++; |
| } |
| } |
| |
| return len; |
| } |
| |
| public CharSequence terminateToken(CharSequence text) { |
| int i = text.length(); |
| |
| while (i > 0 && text.charAt(i - 1) == ' ') { |
| i--; |
| } |
| |
| if (i > 0 && text.charAt(i - 1) == ',') { |
| return text; |
| } else { |
| if (text instanceof Spanned) { |
| SpannableString sp = new SpannableString(text + ", "); |
| TextUtils.copySpansFrom((Spanned) text, 0, text.length(), |
| Object.class, sp, 0); |
| return sp; |
| } else { |
| return text + ", "; |
| } |
| } |
| } |
| } |
| } |