AutoCompleteTextView

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)了以下功能:

  1. 實現(xiàn)自動補全的匹配模式的配置,有三種可選匹配模式:
MODE_CONTAINS / MODE_STARTSWITH(default) / MODE_SPLIT
  1. 實現(xiàn)匹配成功事件的回調(diào),用于根據(jù)匹配結(jié)果數(shù)來動態(tài)設(shè)置下拉提示框的高度
  2. 實現(xiàn)刪除匹配結(jié)果中子項的事件回調(diào),用于實時更新存儲在SharedPreferences的歷史記錄數(shù)據(jù)
  3. 支持使用@字符來預(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

源碼參考

AutoCompleteTextView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容