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 + ", ";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|