2013年2月14日木曜日

Android AutoCompleteTextView で候補に入力履歴を表示する

AutoCompleteTextView はドロップダウンで入力候補を表示してくれる EditText です。
(EditText を extends してるのに AutoCompleteEditText じゃないのはなぜなんだ)

こんな感じで候補の一覧を Adapter として用意して AutoCompleteTextView の setAdapter() でセットします。 public class CountriesActivity extends Activity { protected void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.countries); AutoCompleteTextView textView = (AutoCompleteTextView) findViewById(R.id.countries_list); ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, COUNTRIES); textView.setAdapter(adapter); } private static final String[] COUNTRIES = new String[] { "Belgium", "France", "Italy", "Germany", "Spain" }; } 過去の入力を候補をして表示したい場合、候補が随時かわるので上記の方法は使えません。

そこで、SearchManager の出番です。
SearchManager については以前のエントリ「Y.A.M の雑記帳 - Android SearchManager 検索ボックスを使うぜ!」あたりをみてください。

簡単にいうと、SearchManager から候補一覧の Cursor(候補は ContentProvider として提供される)を取得して、それを Adapter にセットすればいいわけです。

SearchManager から候補一覧を取得するには SearchManager の getSuggestions() を呼べばいいのですが、残念ながら @hide です。 ただ、SearchableInfo があれば同じことはできます。

シンプルな Adapter だとだいたいこんな感じです。 import android.app.SearchManager; import android.app.SearchableInfo; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.support.v4.widget.ResourceCursorAdapter; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; class SearchRecentSuggestionsAdapter extends ResourceCursorAdapter { private static final boolean DEBUG = false; private static final String LOG_TAG = "SearchRecentSuggestionsAdapter"; private static final int QUERY_LIMIT = 50; private SearchableInfo mSearchable; private boolean mClosed = false; static final int INVALID_INDEX = -1; private int mText1Col = INVALID_INDEX; public SearchRecentSuggestionsAdapter(Context context, SearchableInfo searchable) { super(context, R.layout.search_recent_row, null, true); mSearchable = searchable; } /** * Overridden to always return false, since we cannot be sure * that suggestion sources return stable IDs. */ @Override public boolean hasStableIds() { return false; } /** * Use the search suggestions provider to obtain a live cursor. This will be * called in a worker thread, so it's OK if the query is slow (e.g. round * trip for suggestions). The results will be processed in the UI thread and * changeCursor() will be called. */ @Override public Cursor runQueryOnBackgroundThread(CharSequence constraint) { if (DEBUG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")"); String query = (constraint == null) ? "" : constraint.toString(); Cursor cursor = null; try { cursor = getSuggestions(mSearchable, query, QUERY_LIMIT); return cursor; } catch (RuntimeException e) { Log.w(LOG_TAG, "Search suggestions query threw an exception.", e); } return null; } public Cursor getSuggestions(SearchableInfo searchable, String query, int limit) { if (searchable == null) { return null; } String authority = searchable.getSuggestAuthority(); if (authority == null) { return null; } Uri.Builder uriBuilder = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority); // if content path provided, insert it now final String contentPath = searchable.getSuggestPath(); if (contentPath != null) { uriBuilder.appendEncodedPath(contentPath); } // append standard suggestion query path uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY); // get the query selection, may be null String selection = searchable.getSuggestSelection(); // inject query, either as selection args or inline String[] selArgs = null; if (selection != null) { // use selection if provided selArgs = new String[] { query }; } else { // no selection, use REST pattern uriBuilder.appendPath(query); } if (limit > 0) { uriBuilder.appendQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT, String.valueOf(limit)); } Uri uri = uriBuilder.build(); return mContext.getContentResolver().query(uri, null, selection, selArgs, null); } public void close() { if (DEBUG) Log.d(LOG_TAG, "close()"); changeCursor(null); mClosed = true; } /** * Cache columns. */ @Override public void changeCursor(Cursor c) { if (DEBUG) Log.d(LOG_TAG, "changeCursor(" + c + ")"); if (mClosed) { Log.w(LOG_TAG, "Tried to change cursor after adapter was closed."); if (c != null) c.close(); return; } try { super.changeCursor(c); if (c != null) { mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1); } } catch (Exception e) { Log.e(LOG_TAG, "error changing cursor and caching columns", e); } } /** * Tags the view with cached child view look-ups. */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { View v = super.newView(context, cursor, parent); v.setTag(new ViewHolder(v)); return v; } /** * Cache of the child views of drop-drown list items, to avoid looking up * the children each time the contents of a list item are changed. */ private final static class ViewHolder { public final TextView mText1; public ViewHolder(View v) { mText1 = (TextView) v.findViewById(android.R.id.text1); } } @Override public void bindView(View view, Context context, Cursor cursor) { ViewHolder views = (ViewHolder) view.getTag(); if (views.mText1 != null) { String text1 = getStringOrNull(cursor, mText1Col); views.mText1.setText(text1); views.mText1.setVisibility(TextUtils.isEmpty(text1) ? View.GONE : View.VISIBLE); } } /** * Gets the text to show in the query field when a suggestion is selected. * * @param cursor * The Cursor to read the suggestion data from. The Cursor should * already be moved to the suggestion that is to be read from. * @return The text to show, or null if the query should not be * changed when selecting this suggestion. */ @Override public CharSequence convertToString(Cursor cursor) { if (cursor == null) { return null; } String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY); if (query != null) { return query; } if (mSearchable.shouldRewriteQueryFromData()) { String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA); if (data != null) { return data; } } if (mSearchable.shouldRewriteQueryFromText()) { String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1); if (text1 != null) { return text1; } } return null; } /** * This method is overridden purely to provide a bit of protection against * flaky content providers. * * @see android.widget.ListAdapter#getView(int, View, ViewGroup) */ @Override public View getView(int position, View convertView, ViewGroup parent) { try { return super.getView(position, convertView, parent); } catch (RuntimeException e) { Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e); // Put exception string in item title View v = newView(mContext, mCursor, parent); if (v != null) { ViewHolder views = (ViewHolder) v.getTag(); TextView tv = views.mText1; tv.setText(e.toString()); } return v; } } public static String getColumnString(Cursor cursor, String columnName) { int col = cursor.getColumnIndex(columnName); return getStringOrNull(cursor, col); } private static String getStringOrNull(Cursor cursor, int col) { if (col == INVALID_INDEX) { return null; } try { return cursor.getString(col); } catch (Exception e) { Log.e(LOG_TAG, "unexpected error retrieving valid column from cursor, " + "did the remote process die?", e); return null; } } } search_recent_row.xml <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:padding="8dip" android:layout_width="match_parent" android:layout_height="48dip" > <TextView android:id="@android:id/text1" style="?android:attr/dropDownItemStyle" android:textAppearance="?android:attr/textAppearanceSearchResultTitle" android:singleLine="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" /> </RelativeLayout> この Adapter を AutoCompleteTextView にセットすれば OK です。 public class InputActivity extends Activity { protected void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); final AutoCompleteTextView textView = (AutoCompleteTextView) findViewById(R.id.editText1); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); final SearchableInfo info = searchManager.getSearchableInfo(getComponentName()); textView.setThreshold(info.getSuggestThreshold()); textView.setImeOptions(info.getImeOptions()); int inputType = info.getInputType(); if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) { inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; if (info.getSuggestAuthority() != null) { inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE; } } textView.setInputType(inputType); if (info.getSuggestAuthority() != null) { // 候補の CursorAdapter をセット SearchRecentSuggestionsAdapter adapter = new SearchRecentSuggestionsAdapter(this, info); textView.setAdapter(adapter); } findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 検索文字列を候補に追加 String query = textView.getText().toString(); SearchRecentSuggestions suggestions = new SearchRecentSuggestions(InputActivity.this, info.getSuggestAuthority(), SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES); suggestions.saveRecentQuery(query, null); } }); } } この Activity にはもちろん AndroidManifest.xml で searchable をセットしておくことが必要です。




2 件のコメント:

  1. Using EasyHits4U you can earn free advertising credits by visiting other ads from a account base of over 1,200,000 accounts. Earn credits fast with a 1:1 exchange ratio.

    返信削除
  2. If you need your ex-girlfriend or ex-boyfriend to come crawling back to you on their knees (no matter why you broke up) you need to watch this video
    right away...

    (VIDEO) Get your ex back with TEXT messages?

    返信削除