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 をセットしておくことが必要です。




0 件のコメント:

コメントを投稿