285 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
| /*
 | |
|  * 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.content.res.TypedArray;
 | |
| import android.graphics.Rect;
 | |
| import android.graphics.drawable.Drawable;
 | |
| import android.text.Editable;
 | |
| import android.text.Selection;
 | |
| import android.text.Spanned;
 | |
| import android.text.Spannable;
 | |
| import android.text.SpannableString;
 | |
| import android.text.TextUtils;
 | |
| import android.text.method.QwertyKeyListener;
 | |
| import android.util.AttributeSet;
 | |
| import android.util.Log;
 | |
| import android.view.KeyEvent;
 | |
| import android.view.LayoutInflater;
 | |
| import android.view.View;
 | |
| import android.view.ViewGroup;
 | |
| 
 | |
| import com.android.internal.R;
 | |
| 
 | |
| /**
 | |
|  * 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 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 defStyle) {
 | |
|         super(context, attrs, defStyle);
 | |
|     }
 | |
| 
 | |
|     /* 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));
 | |
|     }
 | |
| 
 | |
|     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 + ", ";
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | 
