AutoCompleteTextView常用屬性
屬性 | 描述 |
---|---|
android:completionHint | 設(shè)置出現(xiàn)在下拉菜單底部的提示信息 |
android:completionThreshold | 設(shè)置觸發(fā)補全提示信息的字符個數(shù) |
android:dropDownHorizontalOffset | 設(shè)置下拉菜單于文本框之間的水平偏移量 |
android:dropDownHeight | 設(shè)置下拉菜單的高度 |
android:dropDownWidth | 設(shè)置下拉菜單的寬度 |
android:singleLine | 設(shè)置單行顯示文本內(nèi)容 |
android:dropDownVerticalOffset | 設(shè)置下拉菜單于文本框之間的垂直偏移量 |
使用ArrayAdapter來作為AutoCompleteTextView的數(shù)據(jù)適配器
- 簡單的xml布局
<AutoCompleteTextView
android:id="@+id/tv_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_type"
android:completionHint="@string/chint_recent"
android:completionThreshold="1" />
- 默認AutoCompleteTextView中的數(shù)據(jù)保存在SharedPreferences中,故將SharedPreferences做了簡單的API封裝以方便數(shù)據(jù)存取,詳細的SharedPreferences請參考這里:SharedPreferences
// 從SharedPreferences中獲取歷史記錄數(shù)據(jù)
private String getHistoryFromSharedPreferences(String key) {
SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_PRIVATE);
return sp.getString(key, SP_EMPTY_TAG);
}
// 將歷史記錄數(shù)據(jù)保存到SharedPreferences中
private void saveHistoryToSharedPreferences(String key, String history) {
SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString(key, history);
editor.apply();
}
// 清除保存在SharedPreferences中的歷史記錄數(shù)據(jù)
private void clearHistoryInSharedPreferences() {
SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.clear();
editor.apply();
}
- 使用默認適配器的AutoCompleteTextView相關(guān)初始化
private void initSearchView() {
mSearchTv = (AutoCompleteTextView) findViewById(R.id.tv_search);
String[] mSearchHistoryArray = getHistoryArray(SP_KEY_SEARCH);
mSearchAdapter = new ArrayAdapter<>(
this,
android.R.layout.simple_dropdown_item_1line,
mSearchHistoryArray
);
mSearchTv.setAdapter(mSearchAdapter); // 設(shè)置適配器
// 設(shè)置下拉提示框的高度為200dp
// mAutoCompleteTv.setDropDownHeight(); // 或XML中為android:dropDownHeight="200dp"
// 默認當(dāng)輸入2個字符以上才會提示, 現(xiàn)在當(dāng)設(shè)置輸入1個字符就自動提示
// mAutoCompleteTv.setThreshold(1); // 或XML中為android:completionThreshold="1"
// 設(shè)置下拉提示框中底部的提示
// mAutoCompleteTv.setCompletionHint("最近的5條記錄");
// 設(shè)置單行輸入限制
// mAutoCompleteTv.setSingleLine(true);
}
private String[] getHistoryArray(String key) {
String[] array = getHistoryFromSharedPreferences(key).split(SP_SEPARATOR);
if (array.length > MAX_HISTORY_COUNT) { // 最多只提示最近的50條歷史記錄
String[] newArray = new String[MAX_HISTORY_COUNT];
System.arraycopy(array, 0, newArray, 0, MAX_HISTORY_COUNT); // 實現(xiàn)數(shù)組間的內(nèi)容復(fù)制
}
return array;
}
- 保存AutoCompleteTextView中的歷史記錄數(shù)據(jù)到SharedPreferences中
private void saveSearchHistory() {
String text = mSearchTv.getText().toString().trim(); // 獲取搜索框文本信息
if (TextUtils.isEmpty(text)) { // null or ""
Toast.makeText(this, "Please type something again.", Toast.LENGTH_SHORT).show();
return;
}
String old_text = getHistoryFromSharedPreferences(SP_KEY_SEARCH);// 獲取SP中保存的歷史記錄
StringBuilder sb;
if (SP_EMPTY_TAG.equals(old_text)) {
sb = new StringBuilder();
} else {
sb = new StringBuilder(old_text);
}
sb.append(text + SP_SEPARATOR); // 使用逗號來分隔每條歷史記錄
// 判斷搜索內(nèi)容是否已存在于歷史文件中,已存在則不再添加
if (!old_text.contains(text + SP_SEPARATOR)) {
saveHistoryToSharedPreferences(SP_KEY_SEARCH, sb.toString()); // 實時保存歷史記錄
mSearchAdapter.add(text); // 實時更新下拉提示框中的歷史記錄
Toast.makeText(this, "Search saved: " + text, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Search existed: " + text, Toast.LENGTH_SHORT).show();
}
}
上面代碼中,為了能夠?qū)崟r更新下拉提示框中的歷史記錄,需要在保存數(shù)據(jù)后再調(diào)用ArrayAdapter.add()
方法,而不是調(diào)用ArrayAdapter.notifyDataSetChanged()
。
- 實時清除下拉提示框中的歷史記錄
clearHistoryInSharedPreferences(); // 試試清除歷史記錄
mSearchAdapter.clear(); // 實時清除下拉提示框中的歷史記錄
- 效果演示
arrayadapter_autocomplete_textview_320x512.gif
使用自定義AutoCompleteAdapter來作為AutoCompleteTextView的數(shù)據(jù)適配器
- 簡單的xml布局
使用RelativeLayout來容納AutoCompleteTextView和ImageView,其中ImageView位于右側(cè),用于點擊清除AutoCompleteTextView的內(nèi)容
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center_vertical">
<AutoCompleteTextView
android:id="@+id/tv_custom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="40dp"
android:hint="@string/hint_type"/>
<ImageView
android:id="@+id/iv_custom"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="10dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:scaleType="fitCenter"
android:src="@drawable/ic_action_name"
android:contentDescription="@null"/>
</RelativeLayout>
- 使用自定義適配器的AutoCompleteTextView相關(guān)初始化
private void initCustomView() {
mCustomTv = (AutoCompleteTextView) findViewById(R.id.tv_custom);
mDeleteIv = (ImageView) findViewById(R.id.iv_custom);
mDeleteIv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mCustomTv.setText(""); // 清空TextView的內(nèi)容
}
});
ArrayList<String> mOriginalValues = new ArrayList<>();
String[] mCustomHistoryArray = getHistoryArray(SP_KEY_CUSTOM);
mOriginalValues.addAll(Arrays.asList(mCustomHistoryArray)); // String[] => ArrayList<String>
mCustomAdapter = new AutoCompleteAdapter(this, mOriginalValues);
mCustomAdapter.setDefaultMode(AutoCompleteAdapter.MODE_STARTSWITH | AutoCompleteAdapter.MODE_SPLIT);// 設(shè)置匹配模式
mCustomAdapter.setSupportPreview(true); // 支持使用特殊符號進行預(yù)覽提示內(nèi)容,默認為'@'
simpleItemHeight = mCustomAdapter.getSimpleItemHeight();
Toast.makeText(this, "simpleItemHeight: " + simpleItemHeight, Toast.LENGTH_SHORT).show(); // 103
mCustomAdapter.setOnFilterResultsListener(new AutoCompleteAdapter.OnFilterResultsListener() {
@Override
public void onFilterResultsListener(int count) {
curCount = count;
if (count > MAX_ONCE_MATCHED_ITEM) { // 限制提示框最多要顯示的記錄行數(shù)
curCount = MAX_ONCE_MATCHED_ITEM;
}
if (curCount != prevCount) { // 僅當(dāng)目前的數(shù)目和之前的不同才重新設(shè)置下拉框高度,避免重復(fù)設(shè)置
prevCount = curCount;
mCustomTv.setDropDownHeight(simpleItemHeight * curCount);
}
}
});
mCustomAdapter.setOnSimpleItemDeletedListener(new AutoCompleteAdapter.OnSimpleItemDeletedListener() {
@Override
public void onSimpleItemDeletedListener(String value) {
String old_history = getHistoryFromSharedPreferences(SP_KEY_CUSTOM); // 獲取之前的記錄
String new_history = old_history.replace(value + SP_SEPARATOR, ""); // 用空字符串替換掉要刪除的記錄
saveHistoryToSharedPreferences(SP_KEY_CUSTOM, new_history); // 保存修改過的記錄
}
});
mCustomTv.setAdapter(mCustomAdapter); //
mCustomTv.setThreshold(1); //
// 設(shè)置下拉時顯示的提示行數(shù) (此處不設(shè)置也可以,因為在AutoCompleteAdapter中有專門的事件監(jiān)聽來實時設(shè)置提示框的高度)
// mCustomTv.setDropDownHeight(simpleItemHeight * MAX_ONCE_MATCHED_ITEM);
}
- 保存AutoCompleteTextView中的歷史記錄數(shù)據(jù)到SharedPreferences中
private void saveCustomHistory() {
String text = mCustomTv.getText().toString().trim(); // 獲取搜索框信息
if (TextUtils.isEmpty(text)) { // null or ""
Toast.makeText(this, "Please type something again.", Toast.LENGTH_SHORT).show();
return;
}
String old_text = getHistoryFromSharedPreferences(SP_KEY_CUSTOM); // 獲取SP中保存的歷史記錄
StringBuilder sb;
if (SP_EMPTY_TAG.equals(old_text)) {
sb = new StringBuilder();
} else {
sb = new StringBuilder(old_text);
}
sb.append(text + SP_SEPARATOR); // 使用逗號來分隔每條歷史記錄
// 判斷搜索內(nèi)容是否已存在于歷史文件中,已存在則不再添加
if (!old_text.contains(text + SP_SEPARATOR)) {
saveHistoryToSharedPreferences(SP_KEY_CUSTOM, sb.toString()); // 實時保存歷史記錄
mCustomAdapter.add(text); // 實時更新下拉提示框中的歷史記錄
Toast.makeText(this, "Custom saved: " + text, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Custom existed: " + text, Toast.LENGTH_SHORT).show();
}
}
- 實時清除下拉提示框中的歷史記錄
clearHistoryInSharedPreferences(); // 試試清除歷史記錄
mCustomAdapter.clear(); // 實時清除下拉提示框中的歷史記錄
- 自定義適配器AutoCompleteAdapter
AutoCompleteAdapter參考了ArrayAdapter的部分源代碼,繼承自BaseAdapter
并實現(xiàn)Filterable
接口,實現(xiàn)了以下功能:
- 實現(xiàn)自動補全的匹配模式的配置,有三種可選匹配模式:
MODE_CONTAINS / MODE_STARTSWITH(default) / MODE_SPLIT
- 實現(xiàn)匹配成功事件的回調(diào),用于根據(jù)匹配結(jié)果數(shù)來動態(tài)設(shè)置下拉提示框的高度
- 實現(xiàn)刪除匹配結(jié)果中子項的事件回調(diào),用于實時更新存儲在SharedPreferences的歷史記錄數(shù)據(jù)
- 支持使用
@
字符來預(yù)覽所有提示內(nèi)容
public class AutoCompleteAdapter extends BaseAdapter implements Filterable {
private static final int MODE_NONE = 0x000; // 0000b
public static final int MODE_CONTAINS = 0x001; // 0001b
public static final int MODE_STARTSWITH = 0x002; // 0010b
public static final int MODE_SPLIT = 0x004; // 0100b
private static final String SPLIT_SEPARATOR = "[,.\\s]+"; // 分隔符,默認為空白符、英文逗號、英文句號
private static boolean isFound = false; // 當(dāng)MODE_STARTSWITH模式匹配成功時,不再進行MODE_SPLIT模式的匹配
private int defaultMode = MODE_STARTSWITH; // 0110b
private LayoutInflater inflater;
private ArrayFilter mArrayFilter;
private ArrayList<String> mOriginalValues; // 所有的item
private List<String> mObjects; // 過濾后的item
private final Object mLock = new Object(); // 同步鎖
private int maxMatch = 10; // 最多顯示的item數(shù)目,負數(shù)表示全部
private int simpleItemHeight; // 單行item的高度值,故需要在XML中固定父布局的高度值
private char previewChar = '@'; // 默認字符
private boolean isSupportPreview = false; // 是否可以使用@符號進行預(yù)覽全部提示內(nèi)容
public AutoCompleteAdapter(Context context, ArrayList<String> mOriginalValues) {
this(context, mOriginalValues, -1);
}
public AutoCompleteAdapter(Context context, ArrayList<String> mOriginalValues, int maxMatch) {
this.mOriginalValues = mOriginalValues;
// 初始化時將其設(shè)置成mOriginalValues,避免在未進行數(shù)據(jù)保存時執(zhí)行刪除操作導(dǎo)致程序的崩潰
this.mObjects = mOriginalValues;
this.maxMatch = maxMatch;
inflater = LayoutInflater.from(context);
initViewHeight();
}
private void initViewHeight() {
View view = inflater.inflate(R.layout.simple_dropdown_item_1line, null);
LinearLayout linearLayout = (LinearLayout) view.findViewById(R.id.layout_item);
linearLayout.measure(0, 0);
// 其他方法獲取的高度值會因View尚未被繪制而獲取到0
simpleItemHeight = linearLayout.getMeasuredHeight();
}
public int getSimpleItemHeight() {
return simpleItemHeight; // 5 * 2 + 28(dp) => 103(px)
}
public void setSupportPreview(boolean isSupportPreview){
this.isSupportPreview = isSupportPreview;
}
public void setSupportPreview(boolean isSupportPreview, char previewChar){
this.isSupportPreview = isSupportPreview;
this.previewChar = previewChar;
}
@Override
public int getCount() {
return mObjects.size();
}
@Override
public Object getItem(int position) {
return mObjects.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
convertView = inflater.inflate(R.layout.simple_dropdown_item_1line, null);
holder.tv = (TextView) convertView.findViewById(R.id.tv_simple_item);
holder.iv = (ImageView) convertView.findViewById(R.id.iv_simple_item);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.tv.setText(mObjects.get(position));
holder.iv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String value = mObjects.remove(position);
if (mDeleteListener != null) {
mDeleteListener.onSimpleItemDeletedListener(value);
}
if (mFilterListener != null) {
mFilterListener.onFilterResultsListener(mObjects.size());
}
mOriginalValues.remove(value);
notifyDataSetChanged();
}
});
return convertView;
}
private static class ViewHolder {
TextView tv;
ImageView iv;
}
public void setDefaultMode(int defaultMode) {
this.defaultMode = defaultMode;
}
public void add(String item) {
mOriginalValues.add(item);
notifyDataSetChanged(); //
}
public void clear() {
if(mOriginalValues != null && !mOriginalValues.isEmpty()) {
mOriginalValues.clear();
notifyDataSetChanged(); //
}
}
// Interface
public interface OnSimpleItemDeletedListener {
void onSimpleItemDeletedListener(String value);
}
private OnSimpleItemDeletedListener mDeleteListener;
public void setOnSimpleItemDeletedListener(OnSimpleItemDeletedListener listener) {
this.mDeleteListener = listener;
}
// Interface
public interface OnFilterResultsListener {
void onFilterResultsListener(int count);
}
private OnFilterResultsListener mFilterListener;
public void setOnFilterResultsListener(OnFilterResultsListener listener) {
this.mFilterListener = listener;
}
@Override
public Filter getFilter() {
if (mArrayFilter == null) {
mArrayFilter = new ArrayFilter(mFilterListener);
}
return mArrayFilter;
}
private class ArrayFilter extends Filter {
private OnFilterResultsListener listener;
public ArrayFilter(OnFilterResultsListener listener) {
this.listener = listener;
}
@Override
protected FilterResults performFiltering(CharSequence prefix) {
FilterResults results = new FilterResults();
if (mOriginalValues == null) {
synchronized (mLock) {
mOriginalValues = new ArrayList<>(mObjects);
}
}
if (prefix == null || prefix.length() == 0) {
synchronized (mLock) {
ArrayList<String> list = new ArrayList<>(mOriginalValues);
results.values = list;
results.count = list.size();
}
} else {
if (isSupportPreview) {
int index = prefix.toString().indexOf(String.valueOf(previewChar));
if (index != -1) {
prefix = prefix.toString().substring(index + 1);
}
}
String prefixString = prefix.toString().toLowerCase(); // prefixString
final int count = mOriginalValues.size(); // count
final ArrayList<String> newValues = new ArrayList<>(count); // newValues
for (int i = 0; i < count; i++) {
final String value = mOriginalValues.get(i); // value
final String valueText = value.toLowerCase(); // valueText
// 1. 匹配所有
if ((defaultMode & MODE_CONTAINS) != MODE_NONE) {
if (valueText.contains(prefixString)) {
newValues.add(value);
}
} else { // support: defaultMode = MODE_STARTSWITH | MODE_SPLIT
// 2. 匹配開頭
if ((defaultMode & MODE_STARTSWITH) != MODE_NONE) {
if (valueText.startsWith(prefixString)) {
newValues.add(value);
isFound = true;
}
}
// 3. 分隔符匹配,效率低
if (!isFound && (defaultMode & MODE_SPLIT) != MODE_NONE) {
final String[] words = valueText.split(SPLIT_SEPARATOR);
for (String word : words) {
if (word.startsWith(prefixString)) {
newValues.add(value);
break;
}
}
}
if(isFound) { // 若在MODE_STARTSWITH模式中匹配,則再次復(fù)位進行下一次判斷
isFound = false;
}
}
if (maxMatch > 0) { // 限制顯示item的數(shù)目
if (newValues.size() > maxMatch - 1) {
break;
}
}
} // for (int i = 0; i < count; i++)
results.values = newValues;
results.count = newValues.size();
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
//noinspection unchecked
mObjects = (List<String>) results.values;
if (results.count > 0) {
// 由于當(dāng)刪除提示框中的記錄行時,而AutoCompleteTextView此時內(nèi)容又不改變,故不會觸發(fā)FilterResults事件
// 導(dǎo)致刪除記錄行時,提示框的高度不會發(fā)生相應(yīng)的改變
// 解決方法:需要在ImageView的點擊監(jiān)聽器中也調(diào)用OnFilterResultsListener.onFilterResultsListener()
// 來共同完成
if (listener != null) {
listener.onFilterResultsListener(results.count);
}
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
}
}
- 下拉提示框的item布局simple_dropdown_item_1line.xml
這里需要固定父類控件LinearLayout的高度,在AutoCompleteAdapter中會獲取其高度用于設(shè)置AutoCompleteTextView的下拉菜單的高度
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout_item"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="28dp"
android:padding="5dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_simple_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="5dp"
android:paddingEnd="0dp"
android:text="@string/text_nothing"
android:textAllCaps="false"
android:textSize="18sp"
android:textColor="#000"/>
<ImageView
android:id="@+id/iv_simple_item"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="5dp"
android:src="@drawable/ic_action_name"
android:contentDescription="@null"
android:scaleType="fitCenter" />
</LinearLayout>
- 效果演示
autocompleteadapter_autocomplete_textview_320x512.gif
支持使用@
字符來預(yù)覽所有提示內(nèi)容
autocomplete_textview_preview_support.gif
- PlantUML插件畫的類圖
PlantUML_UML_Class_AutoCompleteAdapter.PNG