自定义SearchView的搜索提示

  SearchView除了提供输入和提交搜索的界面,还能够根据输入显示对应的搜索提示。本文以搜索历史记录为例,介绍自定义Android搜索提示的方法,如图1所示。

图1

图1

1. SearchRecentSuggestionsProvider

  首先要创建自己的SearchRecentSuggestionsProvider,用于保存和查询搜索记录。

1.1 创建SearchRecentSuggestionsProvider

  SearchRecentSuggestionsProvider基本实现了进行读写搜索历史所需的所有功能,只需继承它并定义ContentProvider的AUTHORITY和Mode。Mode必须包含DATABASE_MODE_QUERIES ,可选包含DATABASE_MODE_2LINES 来让历史记录具有两行内容。具体内容如下:

public class SimpleSearchSuggestionsProvider extends SearchRecentSuggestionsProvider {
private static final String LOG_TAG = SimpleSearchSuggestionsProvider.class.getSimpleName();
public final static String AUTHORITY = "com.nex3z.examples.customsearchsuggestionitem.SimpleSearchSuggestionsProvider";
public final static int MODE = DATABASE_MODE_QUERIES;
public SimpleSearchSuggestionsProvider() {
setupSuggestions(AUTHORITY, MODE);
}
}
public class SimpleSearchSuggestionsProvider extends SearchRecentSuggestionsProvider { private static final String LOG_TAG = SimpleSearchSuggestionsProvider.class.getSimpleName(); public final static String AUTHORITY = "com.nex3z.examples.customsearchsuggestionitem.SimpleSearchSuggestionsProvider"; public final static int MODE = DATABASE_MODE_QUERIES; public SimpleSearchSuggestionsProvider() { setupSuggestions(AUTHORITY, MODE); } }
public class SimpleSearchSuggestionsProvider extends SearchRecentSuggestionsProvider {
    private static final String LOG_TAG = SimpleSearchSuggestionsProvider.class.getSimpleName();

    public final static String AUTHORITY = "com.nex3z.examples.customsearchsuggestionitem.SimpleSearchSuggestionsProvider";
    public final static int MODE = DATABASE_MODE_QUERIES;

    public SimpleSearchSuggestionsProvider() {
        setupSuggestions(AUTHORITY, MODE);
    }
}

【完整代码】

  然后要在AndroidManifest.xml中声明这个ContentProvider:

<application
...
<provider
android:name=".SimpleSearchSuggestionsProvider"
android:authorities="com.nex3z.examples.customsearchsuggestionitem.SimpleSearchSuggestionsProvider" />
...
</application>
<application ... <provider android:name=".SimpleSearchSuggestionsProvider" android:authorities="com.nex3z.examples.customsearchsuggestionitem.SimpleSearchSuggestionsProvider" /> ... </application>
<application
...
    <provider
        android:name=".SimpleSearchSuggestionsProvider"
        android:authorities="com.nex3z.examples.customsearchsuggestionitem.SimpleSearchSuggestionsProvider" />
...
</application>

1.2. 保存搜索记录

  有了SimpleSearchSuggestionsProvider,就可以使用SearchRecentSuggestions可以很方便地保存搜索记录:

SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this,
SimpleSearchSuggestionsProvider.AUTHORITY, SimpleSearchSuggestionsProvider.MODE);
suggestions.saveRecentQuery(query, null);
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SimpleSearchSuggestionsProvider.AUTHORITY, SimpleSearchSuggestionsProvider.MODE); suggestions.saveRecentQuery(query, null);
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this,
        SimpleSearchSuggestionsProvider.AUTHORITY, SimpleSearchSuggestionsProvider.MODE);
suggestions.saveRecentQuery(query, null);

这里saveRecentQuery() 的第一个参数代表要保存的搜索记录,第二个参数为可选添加的内容(需要在SearchRecentSuggestionsProvider设置DATABASE_MODE_2LINES )。

1.3. 查询搜索记录

  可以按照使用一般ContentProvider的方法来查询搜索记录。这里使用了SearchManager来辅助建立uri:

public Cursor getRecentSuggestions(String query, int limit) {
Uri.Builder uriBuilder = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(SimpleSearchSuggestionsProvider.AUTHORITY);
uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
String selection = " ?";
String[] selArgs = new String[] { query };
if (limit > 0) {
uriBuilder.appendQueryParameter(
SearchManager.SUGGEST_PARAMETER_LIMIT, String.valueOf(limit));
}
Uri uri = uriBuilder.build();
return getContentResolver().query(uri, null, selection, selArgs, null);
}
public Cursor getRecentSuggestions(String query, int limit) { Uri.Builder uriBuilder = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(SimpleSearchSuggestionsProvider.AUTHORITY); uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY); String selection = " ?"; String[] selArgs = new String[] { query }; if (limit > 0) { uriBuilder.appendQueryParameter( SearchManager.SUGGEST_PARAMETER_LIMIT, String.valueOf(limit)); } Uri uri = uriBuilder.build(); return getContentResolver().query(uri, null, selection, selArgs, null); }
public Cursor getRecentSuggestions(String query, int limit) {
    Uri.Builder uriBuilder = new Uri.Builder()
            .scheme(ContentResolver.SCHEME_CONTENT)
            .authority(SimpleSearchSuggestionsProvider.AUTHORITY);

    uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);

    String selection = " ?";
    String[] selArgs = new String[] { query };

    if (limit > 0) {
        uriBuilder.appendQueryParameter(
                SearchManager.SUGGEST_PARAMETER_LIMIT, String.valueOf(limit));
    }

    Uri uri = uriBuilder.build();

    return getContentResolver().query(uri, null, selection, selArgs, null);
}

2. 创建Suggestion Item

2.1. Item布局

  接下来创建自定义的Suggestion Item,在res/layout下建立search_suggestion_item.xml,其中包含一个用于显示图标的ImageView和一个用于显示提示的TextView:

<LinearLayout
android:orientation="horizontal"
...>
<ImageView
android:id="@+id/iv_suggestion_item_icon"
.../>
<TextView
android:id="@+id/tv_suggestion_item_title"
.../>
</LinearLayout>
<LinearLayout android:orientation="horizontal" ...> <ImageView android:id="@+id/iv_suggestion_item_icon" .../> <TextView android:id="@+id/tv_suggestion_item_title" .../> </LinearLayout>
<LinearLayout
    android:orientation="horizontal"
    ...>

    <ImageView
        android:id="@+id/iv_suggestion_item_icon"
        .../>

    <TextView
        android:id="@+id/tv_suggestion_item_title"
        .../>

</LinearLayout>

【完整代码】

2.2. 创建Adapter

  Adapter把从ContentProvider中获取的提示数据填充到上面创建的Item里,这里的Adapter就是一个简单的CursorAdapter。前面使用SearchRecentSuggestions的saveRecentQuery() 方法来保存历史记录,对应地,这里要从SearchManager.SUGGEST_COLUMN_TEXT_1 获取所保存的搜索历史记录。

@Override
public void bindView(View view, Context context, Cursor cursor) {
ViewHolder viewHolder = (ViewHolder) view.getTag();
viewHolder.mTitle.setText(
cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)));
}
@Override public void bindView(View view, Context context, Cursor cursor) { ViewHolder viewHolder = (ViewHolder) view.getTag(); viewHolder.mTitle.setText( cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1))); }
@Override
public void bindView(View view, Context context, Cursor cursor) {
    ViewHolder viewHolder = (ViewHolder) view.getTag();
    viewHolder.mTitle.setText(
            cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)));
}

【完整代码】

3. 创建Searchable Configuration

  Searchable Configuration用于配置搜索对话框的UI。在res/xml下建立searchable.xml,其中必须包含label ,通常为app的名称,一般不可见。这里还添加了hint ,用于在用户未输入搜索内容时,在搜索框显示提示。由于这里使用了自定义的adapter来显示搜索提示,这里并不需要添加searchSuggestAuthority 。

<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/app_name"
android:hint="@string/search_hint" />
<searchable xmlns:android="http://schemas.android.com/apk/res/android" android:label="@string/app_name" android:hint="@string/search_hint" />
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/app_name"
    android:hint="@string/search_hint" />

【完整代码】

4. 配置Searchable Activity

  Searchable Activity用来处理搜索,当用户提交搜索后,系统会启动对应的Activity,搜索内容通过带有ACTION_SEARCH 的intent来传递。

4.1. 声明Searchable Activity

  这里以MainActivity做为Searchable Activity,首先要在AndroidManifest.xml中声明。

<application
...
<activity
android:name=".MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
...
</application>
<application ... <activity android:name=".MainActivity" android:launchMode="singleTop"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.SEARCH" /> </intent-filter> <meta-data android:name="android.app.searchable" android:resource="@xml/searchable" /> </activity> ... </application>
<application
...
    <activity
        android:name=".MainActivity"
        android:launchMode="singleTop">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.SEARCH" />
        </intent-filter>
        <meta-data
            android:name="android.app.searchable"
            android:resource="@xml/searchable" />
    </activity>

...
</application>

首先要添加meta-data,指向上一步中创建的Searchable Configuration(searchable.xml)。然后还要注册android.intent.action.SEARCH的intent-filter,用于接收用户提交搜索后由系统发出的intent。注意这里还设置了launchMode 为singleTop ,避免搜索时重复启动Activity。

4.2. 添加搜索菜单

  在res/menu/下添加menu_search.xml如下:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_search"
android:title="@string/action_search"
android:icon="@drawable/ic_search_24dp"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="android.support.v7.widget.SearchView" />
</menu>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/action_search" android:title="@string/action_search" android:icon="@drawable/ic_search_24dp" app:showAsAction="collapseActionView|ifRoom" app:actionViewClass="android.support.v7.widget.SearchView" /> </menu>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/action_search"
        android:title="@string/action_search"
        android:icon="@drawable/ic_search_24dp"
        app:showAsAction="collapseActionView|ifRoom"
        app:actionViewClass="android.support.v7.widget.SearchView" />
</menu>

【完整代码】

搜索菜单以放大镜图标按钮的形式显示,注意这里指定app:actionViewClass 为Support Library的SearchView。

4.3. 配置SearchView

  首先在onCreateOptionsMenu()找到搜索菜单对应的MenuItem:

@Override
public boolean onCreateOptionsMenu(Menu menu){
getMenuInflater().inflate(R.menu.menu_search, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
...
}
@Override public boolean onCreateOptionsMenu(Menu menu){ getMenuInflater().inflate(R.menu.menu_search, menu); MenuItem searchItem = menu.findItem(R.id.action_search); ... }
@Override
public boolean onCreateOptionsMenu(Menu menu){
    getMenuInflater().inflate(R.menu.menu_search, menu);
    MenuItem searchItem = menu.findItem(R.id.action_search);
    ...
}

  然后由searchItem获取SearchView,并通过SearchManager配置SearchableInfo,也就是之前的searchable.xml:
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
final SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); final SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
SearchManager searchManager =
        (SearchManager) getSystemService(Context.SEARCH_SERVICE);
final SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));

  接下来把SearchSuggestionAdapter配置到searchView(注意SearchView的setSuggestionsAdapter() 只支持CursorAdapter):
mSuggestionAdapter = new SearchSuggestionAdapter(this, null, 0);
searchView.setSuggestionsAdapter(mSuggestionAdapter);
mSuggestionAdapter = new SearchSuggestionAdapter(this, null, 0); searchView.setSuggestionsAdapter(mSuggestionAdapter);
mSuggestionAdapter = new SearchSuggestionAdapter(this, null, 0);
searchView.setSuggestionsAdapter(mSuggestionAdapter);

  当搜索框内容变化时,从ContentProvider查询对应的搜索记录,并传递给adapter:
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
Cursor cursor = getRecentSuggestions(newText, 10);
mSuggestionAdapter.swapCursor(cursor);
return false;
}
});
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return false; } @Override public boolean onQueryTextChange(String newText) { Cursor cursor = getRecentSuggestions(newText, 10); mSuggestionAdapter.swapCursor(cursor); return false; } });
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    @Override
    public boolean onQueryTextSubmit(String query) {
        return false;
    }

    @Override
    public boolean onQueryTextChange(String newText) {
        Cursor cursor = getRecentSuggestions(newText, 10);
        mSuggestionAdapter.swapCursor(cursor);
        return false;
    }
});

  当suggestion被选中时,把对应suggestion内容填充到搜索栏,并触发搜索:
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
@Override
public boolean onSuggestionSelect(int position) {
return false;
}
@Override
public boolean onSuggestionClick(int position) {
searchView.setQuery(mSuggestionAdapter.getSuggestionText(position), true);
return true;
}
});
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { @Override public boolean onSuggestionSelect(int position) { return false; } @Override public boolean onSuggestionClick(int position) { searchView.setQuery(mSuggestionAdapter.getSuggestionText(position), true); return true; } });
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
    @Override
    public boolean onSuggestionSelect(int position) {
        return false;
    }

    @Override
    public boolean onSuggestionClick(int position) {
        searchView.setQuery(mSuggestionAdapter.getSuggestionText(position), true);
        return true;
    }
});

这里setQuery() 的第二个参数指示是否触发搜索,这里设为true,当用户点击搜索历史提示时,会立即搜索指定关键词。

4.4. 处理搜索

  使用自定义搜索提示时,处理搜索并没有什么不同,这里顺带一提。当用户提交搜索后,系统会启动对应的Searchable Activity,发送ACTION_SEARCH intent,其中带有名为QUERY 的String extra保存了搜索内容。
首先在onCreate() 中调用handleIntent() 来处理ACTION_SEARCH ,另外Activity设置了launchMode 为“singleTop” ,还需要在onNewIntent() 中调用handleIntent() 。

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handleIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
handleIntent(intent);
}
private void handleIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this,
SimpleSearchSuggestionsProvider.AUTHORITY, SimpleSearchSuggestionsProvider.MODE);
suggestions.saveRecentQuery(query, null);
}
}
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); handleIntent(getIntent()); } @Override protected void onNewIntent(Intent intent) { handleIntent(intent); } private void handleIntent(Intent intent) { if (Intent.ACTION_SEARCH.equals(intent.getAction())) { String query = intent.getStringExtra(SearchManager.QUERY); SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SimpleSearchSuggestionsProvider.AUTHORITY, SimpleSearchSuggestionsProvider.MODE); suggestions.saveRecentQuery(query, null); } }
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    handleIntent(getIntent());
}

@Override
protected void onNewIntent(Intent intent) {
    handleIntent(intent);
}

private void handleIntent(Intent intent) {
    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
        String query = intent.getStringExtra(SearchManager.QUERY);
        SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this,
                SimpleSearchSuggestionsProvider.AUTHORITY, SimpleSearchSuggestionsProvider.MODE);
        suggestions.saveRecentQuery(query, null);
    }
}

【完整代码】

5. 完整代码

  完整代码可以参考这里