2013年2月14日木曜日

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

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

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

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

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

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

シンプルな Adapter だとだいたいこんな感じです。
  1. import android.app.SearchManager;  
  2. import android.app.SearchableInfo;  
  3. import android.content.ContentResolver;  
  4. import android.content.Context;  
  5. import android.database.Cursor;  
  6. import android.net.Uri;  
  7. import android.support.v4.widget.ResourceCursorAdapter;  
  8. import android.text.TextUtils;  
  9. import android.util.Log;  
  10. import android.view.View;  
  11. import android.view.ViewGroup;  
  12. import android.widget.TextView;  
  13.   
  14. class SearchRecentSuggestionsAdapter extends ResourceCursorAdapter {  
  15.   
  16.     private static final boolean DEBUG = false;  
  17.     private static final String LOG_TAG = "SearchRecentSuggestionsAdapter";  
  18.   
  19.     private static final int QUERY_LIMIT = 50;  
  20.   
  21.     private SearchableInfo mSearchable;  
  22.     private boolean mClosed = false;  
  23.   
  24.     static final int INVALID_INDEX = -1;  
  25.   
  26.     private int mText1Col = INVALID_INDEX;  
  27.   
  28.     public SearchRecentSuggestionsAdapter(Context context, SearchableInfo searchable) {  
  29.         super(context, R.layout.search_recent_row, nulltrue);  
  30.   
  31.         mSearchable = searchable;  
  32.     }  
  33.   
  34.     /** 
  35.      * Overridden to always return <code>false</code>, since we cannot be sure 
  36.      * that suggestion sources return stable IDs. 
  37.      */  
  38.     @Override  
  39.     public boolean hasStableIds() {  
  40.         return false;  
  41.     }  
  42.   
  43.     /** 
  44.      * Use the search suggestions provider to obtain a live cursor. This will be 
  45.      * called in a worker thread, so it's OK if the query is slow (e.g. round 
  46.      * trip for suggestions). The results will be processed in the UI thread and 
  47.      * changeCursor() will be called. 
  48.      */  
  49.     @Override  
  50.     public Cursor runQueryOnBackgroundThread(CharSequence constraint) {  
  51.         if (DEBUG)  
  52.             Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")");  
  53.   
  54.         String query = (constraint == null) ? "" : constraint.toString();  
  55.         Cursor cursor = null;  
  56.         try {  
  57.             cursor = getSuggestions(mSearchable, query, QUERY_LIMIT);  
  58.             return cursor;  
  59.         } catch (RuntimeException e) {  
  60.             Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);  
  61.         }  
  62.         return null;  
  63.     }  
  64.   
  65.     public Cursor getSuggestions(SearchableInfo searchable, String query, int limit) {  
  66.         if (searchable == null) {  
  67.             return null;  
  68.         }  
  69.   
  70.         String authority = searchable.getSuggestAuthority();  
  71.         if (authority == null) {  
  72.             return null;  
  73.         }  
  74.   
  75.         Uri.Builder uriBuilder = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(authority);  
  76.   
  77.         // if content path provided, insert it now  
  78.         final String contentPath = searchable.getSuggestPath();  
  79.         if (contentPath != null) {  
  80.             uriBuilder.appendEncodedPath(contentPath);  
  81.         }  
  82.   
  83.         // append standard suggestion query path  
  84.         uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);  
  85.   
  86.         // get the query selection, may be null  
  87.         String selection = searchable.getSuggestSelection();  
  88.         // inject query, either as selection args or inline  
  89.         String[] selArgs = null;  
  90.         if (selection != null) {  
  91.             // use selection if provided  
  92.             selArgs = new String[] { query };  
  93.         } else {  
  94.             // no selection, use REST pattern  
  95.             uriBuilder.appendPath(query);  
  96.         }  
  97.   
  98.         if (limit > 0) {  
  99.             uriBuilder.appendQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT, String.valueOf(limit));  
  100.         }  
  101.   
  102.         Uri uri = uriBuilder.build();  
  103.   
  104.         return mContext.getContentResolver().query(uri, null, selection, selArgs, null);  
  105.     }  
  106.   
  107.     public void close() {  
  108.         if (DEBUG)  
  109.             Log.d(LOG_TAG, "close()");  
  110.         changeCursor(null);  
  111.         mClosed = true;  
  112.     }  
  113.   
  114.     /** 
  115.      * Cache columns. 
  116.      */  
  117.     @Override  
  118.     public void changeCursor(Cursor c) {  
  119.         if (DEBUG)  
  120.             Log.d(LOG_TAG, "changeCursor(" + c + ")");  
  121.   
  122.         if (mClosed) {  
  123.             Log.w(LOG_TAG, "Tried to change cursor after adapter was closed.");  
  124.             if (c != null)  
  125.                 c.close();  
  126.             return;  
  127.         }  
  128.   
  129.         try {  
  130.             super.changeCursor(c);  
  131.   
  132.             if (c != null) {  
  133.                 mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);  
  134.             }  
  135.         } catch (Exception e) {  
  136.             Log.e(LOG_TAG, "error changing cursor and caching columns", e);  
  137.         }  
  138.     }  
  139.   
  140.     /** 
  141.      * Tags the view with cached child view look-ups. 
  142.      */  
  143.     @Override  
  144.     public View newView(Context context, Cursor cursor, ViewGroup parent) {  
  145.         View v = super.newView(context, cursor, parent);  
  146.         v.setTag(new ViewHolder(v));  
  147.         return v;  
  148.     }  
  149.   
  150.     /** 
  151.      * Cache of the child views of drop-drown list items, to avoid looking up 
  152.      * the children each time the contents of a list item are changed. 
  153.      */  
  154.     private final static class ViewHolder {  
  155.         public final TextView mText1;  
  156.   
  157.         public ViewHolder(View v) {  
  158.             mText1 = (TextView) v.findViewById(android.R.id.text1);  
  159.         }  
  160.     }  
  161.   
  162.     @Override  
  163.     public void bindView(View view, Context context, Cursor cursor) {  
  164.         ViewHolder views = (ViewHolder) view.getTag();  
  165.   
  166.         if (views.mText1 != null) {  
  167.             String text1 = getStringOrNull(cursor, mText1Col);  
  168.             views.mText1.setText(text1);  
  169.             views.mText1.setVisibility(TextUtils.isEmpty(text1) ? View.GONE : View.VISIBLE);  
  170.         }  
  171.     }  
  172.   
  173.     /** 
  174.      * Gets the text to show in the query field when a suggestion is selected. 
  175.      *  
  176.      * @param cursor 
  177.      *            The Cursor to read the suggestion data from. The Cursor should 
  178.      *            already be moved to the suggestion that is to be read from. 
  179.      * @return The text to show, or <code>null</code> if the query should not be 
  180.      *         changed when selecting this suggestion. 
  181.      */  
  182.     @Override  
  183.     public CharSequence convertToString(Cursor cursor) {  
  184.         if (cursor == null) {  
  185.             return null;  
  186.         }  
  187.   
  188.         String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY);  
  189.         if (query != null) {  
  190.             return query;  
  191.         }  
  192.   
  193.         if (mSearchable.shouldRewriteQueryFromData()) {  
  194.             String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA);  
  195.             if (data != null) {  
  196.                 return data;  
  197.             }  
  198.         }  
  199.   
  200.         if (mSearchable.shouldRewriteQueryFromText()) {  
  201.             String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1);  
  202.             if (text1 != null) {  
  203.                 return text1;  
  204.             }  
  205.         }  
  206.   
  207.         return null;  
  208.     }  
  209.   
  210.     /** 
  211.      * This method is overridden purely to provide a bit of protection against 
  212.      * flaky content providers. 
  213.      *  
  214.      * @see android.widget.ListAdapter#getView(int, View, ViewGroup) 
  215.      */  
  216.     @Override  
  217.     public View getView(int position, View convertView, ViewGroup parent) {  
  218.         try {  
  219.             return super.getView(position, convertView, parent);  
  220.         } catch (RuntimeException e) {  
  221.             Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e);  
  222.             // Put exception string in item title  
  223.             View v = newView(mContext, mCursor, parent);  
  224.             if (v != null) {  
  225.                 ViewHolder views = (ViewHolder) v.getTag();  
  226.                 TextView tv = views.mText1;  
  227.                 tv.setText(e.toString());  
  228.             }  
  229.             return v;  
  230.         }  
  231.     }  
  232.   
  233.     public static String getColumnString(Cursor cursor, String columnName) {  
  234.         int col = cursor.getColumnIndex(columnName);  
  235.         return getStringOrNull(cursor, col);  
  236.     }  
  237.   
  238.     private static String getStringOrNull(Cursor cursor, int col) {  
  239.         if (col == INVALID_INDEX) {  
  240.             return null;  
  241.         }  
  242.         try {  
  243.             return cursor.getString(col);  
  244.         } catch (Exception e) {  
  245.             Log.e(LOG_TAG, "unexpected error retrieving valid column from cursor, " + "did the remote process die?", e);  
  246.             return null;  
  247.         }  
  248.     }  
  249. }  
search_recent_row.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:padding="8dip"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="48dip" >  
  6.   
  7.     <TextView android:id="@android:id/text1"  
  8.         style="?android:attr/dropDownItemStyle"  
  9.         android:textAppearance="?android:attr/textAppearanceSearchResultTitle"  
  10.         android:singleLine="true"  
  11.         android:layout_width="match_parent"  
  12.         android:layout_height="wrap_content"  
  13.         android:layout_centerVertical="true" />  
  14.   
  15. </RelativeLayout>  
この Adapter を AutoCompleteTextView にセットすれば OK です。
  1. public class InputActivity extends Activity {  
  2.   
  3.     protected void onCreate(Bundle icicle) {  
  4.         super.onCreate(icicle);  
  5.         setContentView(R.layout.main);  
  6.   
  7.         final AutoCompleteTextView textView = (AutoCompleteTextView) findViewById(R.id.editText1);  
  8.   
  9.         SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);  
  10.         final SearchableInfo info = searchManager.getSearchableInfo(getComponentName());  
  11.   
  12.         textView.setThreshold(info.getSuggestThreshold());  
  13.         textView.setImeOptions(info.getImeOptions());  
  14.   
  15.         int inputType = info.getInputType();  
  16.         if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {  
  17.             inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;  
  18.             if (info.getSuggestAuthority() != null) {  
  19.                 inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;  
  20.             }  
  21.         }  
  22.         textView.setInputType(inputType);  
  23.   
  24.         if (info.getSuggestAuthority() != null) {  
  25.             // 候補の CursorAdapter をセット  
  26.             SearchRecentSuggestionsAdapter adapter = new SearchRecentSuggestionsAdapter(this, info);  
  27.             textView.setAdapter(adapter);  
  28.         }  
  29.           
  30.         findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {  
  31.               
  32.             @Override  
  33.             public void onClick(View v) {  
  34.                 // 検索文字列を候補に追加  
  35.                 String query = textView.getText().toString();  
  36.                   
  37.                 SearchRecentSuggestions suggestions = new SearchRecentSuggestions(InputActivity.this, info.getSuggestAuthority(),  
  38.                         SearchRecentSuggestionsProvider.DATABASE_MODE_QUERIES);  
  39.                 suggestions.saveRecentQuery(query, null);  
  40.             }  
  41.         });  
  42.     }  
  43. }  
この Activity にはもちろん AndroidManifest.xml で searchable をセットしておくことが必要です。




0 件のコメント:

コメントを投稿