当我们打算向APP中添加搜索功能的时候,Android会帮助我们实现用户接口,它提供了两种方式,一种是出现在activity页面顶部的”search dialog”,还有一种是可以嵌入在我们的layout中的”search widget”.这两种方式都可以放在我们APP的任何activity中.用户可以从任何可以启动”search dialog”和”search widget”的activity中初始化搜索操作,Android会启动合适的activity来处理搜索并显示结果.其它的搜索功能可以支持的特性有:
声音搜索
根据最近搜索提供搜索建议
根据APP内的数据提供搜索建议
下面将做详细介绍.
在开始实现搜索功能之前我们首先应该确定自己使用”search dialog”还是使用”search widget”.它俩都可以实现搜索功能,但是实现稍微有所区别:
Searchdialog是由Android控制的UI组件,当被用户激活的时候,它就会显示出来,效果如下图.Android将会控制其所有的事件,当用户提交一个查询的时候,Android会将该查询提交给我们指定的处理搜索的activity.这个dialog还可以处理搜索建议.
Search widget则是一个我们可以放在layout中任何位置的SearchView实例.默认情况下这个控件的行为就像一个标准的EditText控件,在那里默默的待着.但是我们可以对其进行配置让Android处理它所有的输入事件,将查询交付给合适的activity,然后像search dialog一样提供搜索建议.但是Search widget只能使用在Android3.0及以上版本中.如果有必要的话我们可以通过各种监听器处理所有的用户输入事件,但是在这里我们只聚焦于借助Android提供的帮助来实现搜索功能.
当用户通过search dialog或者search widget来实施搜索操作的时候,Android将会创建一个intent并且将用户的查询保存在里面.然后启动一个我们指定的activity来处理搜索(这个activity叫”searchable activity”)并将intent传递给它.要实现这一流程我们必须这样做:
Searchable configuration: 一个为searchdialog或者search widget配置一些设置的XML文件.包括一些设置项,比如语音搜索,搜索建议,和搜索框的提示文本等.
Searchable activity: 用于接收搜索查询的Activity,用于搜索数据和显示搜索结果.
Searchable interface: 由searchdialog或者SearchView提供.默认情况下Search dialog是隐藏的,当我们调用onSearchRequested()方法的时候(当用户点击搜索按钮的时候)显示在屏幕顶端.而使用searchView可以让我们将搜索框放在activity的任何位置,但是我们应该将它放在app bar中作为一个action view来处理.
剩下的部分将会对上述三个步骤进行详细介绍.
首先需要的是一个叫做”searchable configuration”的XML文件.它对搜索功能进行了一些配置,比如搜索建议和语音搜索的行为等.该文件一般命名为searchable.xml,并且必须位于res/xml/目录下.
Android使用该文件来实例化一个SearchableInfo对象,但是我们不能再运行时手动创建该对象,必须声明searchable configuration文件来由Android生成它.
下面来介绍该文件的规则,这个文件必须有一个<searchable>
标签作为根节点,然后指定一个或者多个属性,栗子:
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/app_label"
android:hint="@string/search_hint" >
</searchable>
其中android:label是唯一必须的属性,它指向一个string资源,这个string资源应该是APP的名字.在我们使Quick Search Box可用之前,这个label是不会被用户看到的,设置为可用之后,用户将会在系统设置中的Searchable条目中看到这个值.虽然android:hint不是必须的,但是官方还是建议我们实现这个属性,该属性会让用户在输入之前就看到搜索框中的提示文本.这东西很重要,因为它可以提示用户他们可以在这里做什么样的搜索.
为了保证Android下搜索操作的一致性,我们应该为android:hint指定统一的格式
”Search <content-or-product>”
,比如”Search songs andartists”或者”Search YouTube”.
<searchable>
标签还有其它的属性,然而如果不使用搜索建议或者语音搜索的话用不到它们,更多信息将在后文介绍.
Searchable Activity是APP中用来根据查询字符串处理搜索和展示搜索结果的Activity.当用户在search dialog或者search widget中执行一次搜索操作的时候,Android会启动我们的Searchable Activity,并传给它一个带有ACTION_SEARCH的Intent.然后Searchable Activity可以从intent中名为QUERY的extra中获取到查询的内容,然后就可以搜索数据并显示结果了.但是在此之前Android必须知道哪个Activity是用来做Searchable Activity的,我们应该在manifest文件中声明它.
目前我们首先需要一个处理搜索和显示结果的Activity,还不需要实现真正的搜索功能,只在manifest中声明一个Activity即可,在这个<activity>
中,我们需要:
通过<intent-filter>
声明Activity可以接受action为ACTION_SEARCH的intent.
通过<meta-data>
指定要使用哪个searchable configuration.
栗子:
<application ...>
<activity android:name=".SearchableActivity" >
<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>
标签必须包含android:name属性,并且它的值也是固定的”android.app.searchable”,同时android:resource属性也是必须的,它用来指定一个相关的searchable configuration文件,在这个栗子里是”res/xml/searchable.xml”.在这里<intent-filter>
并不需要一个带有默认值的<category>
,因为Android使用显示intent启动我们的Activity.
如果我们已经在manifest文件中声明了Searchable Activity,那么就可以通过以下三个步骤来处理一个搜索操作:
接收查询.
搜索我们的数据.
展示结果.
通常我们的搜索结果应该展示在一个ListView中,所以我们可以让我们的Searchable Activity继承自ListActivity.它会自带一个带有ListView的默认的layout并提供一些方便的操作ListView的方法.现在分别来介绍上面的三个步骤:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.search);
// Get the intent,verify the action and get the query
Intent intent = getIntent();
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
doMySearch(query);
}
}
QUERY总是会在action为ACTION_SEARCH的intent中,上面的代码里,在获取到查询内容之后,它被传给了doMySearch()方法.
搜索我们的数据:
我们可以用很多种方法保存和搜索自己的数据,保存和搜索数据很重要是我们应该着重考虑的部分,这里并不做详细介绍但是有两个需要注意的tips:
如果我们的数据是保存在设备的SQLite数据库中,执行全文搜索(使用FTS3而不是LIKE查询)可以提供更加健壮的搜索结果,并且可以执行的更加迅速.
如果我们的数据是在线存储的,那么搜索的性能及体验可能会被用户的网络状态影响.这时候应该在数据返回之前显示一个进度条,以保证用户体验
不论我们的数据存在什么地方,官方建议我们通过一个Adapter返回搜索的结果给searchable activity,这样我们就可以很轻松的在ListView中显示搜索结果了.如果数据是存在SQLite中我们可以使用CursorAdapter.如果数据从别的地方来,我们可以考虑使用BaseAdapter.关于Adapter:一个Adapter可以将一系列数据的每个条目绑定到View对象中.当Adapter被应用到ListView上,每个数据片都会作为一个单独的view被插入到list中.Adapter只是接口,所以我们需要它们的实现,比如CursorAdapter(用于从Cursor绑定数据),如果没有为我们的数据实现的现成的Adapter,我们可以使用BaseAdapter并扩展为我们自己的Adapter.
Search dialog在屏幕顶端提供了一个悬浮搜索框,搜索框的左边还有APP的图标.Searchdialog可以提供搜索建议,并且当用户执行搜索的时候Android将会将搜索的查询发送给search activity.但是如果我们的APP需要运行在Android3.0的设备上,我们应该考虑使用search widget.默认情况下search dialog总是隐藏的,直到用户激活它.我们的APP可以通过onSearchRequested()方法来激活search dialog,然而这之前我们应该指定search dialog为特定的activity可用,该方法才会生效.想要让search dialog相对于某个activity可用,我们必须为其指定searchable activity.比如前文代码段中声明的SearchableActivity.如果我们想要指定一个activity来显示search dialog(比如叫OtherActivity),并且将查询交给SearchableActivity来处理,那么我们必须在manifest中声明SearchableActivity和OtherActivity之间的关系.为了指定这样的关系需要在OtherActivity中使用<meta-data>
标签,并为其指定android:name属性和android:value属性,其中android:value属性表示要指定的searchable activity的类名,而android:name则需要使用固定的”android.app.default_searchable”.栗子:
<application ...>
<!-- this is the searchable activity; it performs searches -->
<activity android:name=".SearchableActivity" >
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
<!-- this activity enables the search dialog to initiate searches
in the SearchableActivity -->
<activity android:name=".OtherActivity" ...>
<!-- enable the search dialog to send searches to SearchableActivity -->
<meta-data android:name="android.app.default_searchable"
android:value=".SearchableActivity" />
</activity>
...
</application>
这样OtherActivity就有了一个可用的search dialog.当用户出在这个activity中,onSearchRequested()方法就可以激活search dialog了.当用户执行搜索操作的时候,Android启动SearchableActivity并传给它intent.
Searchable activity默认情况下本身自带searchdialog,所以我们不用为SearchableActivity声明search dialog.
如果我们希望在APP中的每个activity都提供search dialog,可以为<application>
标签添加<meta-data>
子标签,而不是为每个<activity>
添加.这样每个<activity>
都继承了该标签,也就都有了search dialog,并提供相同的searchable activity.如果希望为某个activity指定单独的searchable activity,那么我们可以为其<activity>
单独指定<meta-data>
.现在activity已经有可用的search dialog,那么就可以处理搜索操作了.
尽管有些设备提供了专用的搜索按钮,但是每种设备的搜索按钮的点击事件可能有所不同,并且很多设备并不提供这样的按钮,所以当我们使用search dialog的时候我们必须在UI中提供一个搜索按钮用来调用onSearchRequested()方法激活search dialog.
为了统一Android APP的用户体验,我们应该使用Android风格的搜索按钮图标,并放在Action bar上.
如果我们使用了一个app bar,那么我们应该使用search widget而不是search dialog.
我们也可以使用”type-to-search”功能,让用户在键盘上点击搜索按钮,我们可以在activity的onCreate()方法中通过setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL)方法来实现.
Search dialog是一个悬浮在屏幕顶端的对话框,它不会对activity的堆栈产生任何的影响,所以当searchdialog出现的时候,不会有activity的生命周期类的方法被调用(比如onPause()).所在的activity只是会失去输入焦点而已,输入焦点会在search dialog上.
如果我们想要在search dialog被激活的时候收到提醒,那么需要重写onSearchRequested()方法.当activity失去焦点而search dialog获得焦点的时候,Android将会调用这个方法.这时候我们可以做一些合适的操作(比如暂停一个游戏).除非我们正在传输search context data(下面讨论),否则应该调用该方法的父方法.栗子:
@Override
public boolean onSearchRequested() {
pauseSomeStuff();
return super.onSearchRequested();
}
如果用户通过返回键取消了搜索,search dialog会关闭并返回输入焦点给activity.我们可以监听search dialog的关闭消息,使用的方法是setOnDismissListener()和/或setOnCancelListener().我们应该只注册OnDismissListener,因为每次search dialog被关闭的时候都会调用它.但是OnCancelListener只有在用户明确退出的时候才会执行,所以在用户执行搜索的时候它不会被调用(就是用户点击搜索的时候,这时候search dialog会自然消失).
如果当前的activity不是searchable activity,那么当用户执行搜索操作的时候将会执行普通的生命周期事件.如果当前的activity是searchable activity,那么将会发生如下两件事:
默认情况下,searchable activity会在onCreate()中收到带有ACTION_SEARCH的intent并且一个新的activity实例会被添加到堆栈顶部.这样的话就会有两个searchable activity的实例在堆栈中了,所以点击返回键就会回到前一个activity而不是完全退出.
如果我们将启动模式设置为”singleTop”,那么searchable activity会通过onNewIntent(Intent)方法收到带有ACTION_SEARCH的intent.下面的一小段代码描述了在”singleTop”模式下如何处理新的Intent:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.search);
handleIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
doMySearch(query);
}
}
对比之前的代码,区别在于所有的处理intent的操作都在handleIntent,而onCreate()和onNewIntent()方法都可以调用它.Android在调用onNewIntent()的时候,activity并没有被重新启动,所以使用getIntent()方法还会返回以前的那个旧的intent,所以在onNewIntent()方法中调用setIntent()方法充值了新的intent,以防止我们在未来调用getIntent()方法的时候获得的intent是旧的intent.
很明显在”singleTop”模式下用户拥有更好的用户体验,因为他们不用按两次返回键才能退出searchable activity.所以官方建议我们使用”singleTop”模式来启动searchable activity.
代码栗子:
<activity android:name=".SearchableActivity"
android:launchMode="singleTop" >
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
有时候我们可能希望提供给searchable activity更多的数据来改进我们的搜索操作,这时候我们可以为带有ACTION_SEARCH的intent添加额外的数据,数据保存在一个叫APP_DATA的Bundle中.想要传递这些数据给searchable activity,需要重写onSearchRequested()方法,并创建一个Bundle对象,然后通过startSearch()方法来激活search dialog,并将Bundle对象给它.
代码栗子:
@Override
public boolean onSearchRequested() {
Bundle appData = new Bundle();
appData.putBoolean(SearchableActivity.JARGON,true);
startSearch(null,false,appData,false);
return true;
}
返回true表示我们已经成功的处理了这个事件,并调用了startSearch()方法来激活search dialog.当用户提交了一个查询,这些附带的信息就会被传递给searchable activity.我们可以从APP_DATA的Bundle中取出这些数据.栗子:
Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
if (appData != null) {
boolean jargon = appData.getBoolean(SearchableActivity.JARGON);
}
这里有一件要注意的事情,就是永远都不要在onSearchRequested()方法之外调用startSearch()方法.
SearchView widget在Android3.0及以上版本中可用,如果我们使用的版本在3.0以上并且决定使用searchwidget,那么官方建议我们将search widget作为一个action view置于app bar中,而不是使用search dialog或者将search widget置于我们的activity layout中.比如,这是一个在app bar中的search widget:
Search widget提供了跟search dialog同样的功能,当用户执行搜索操作的时候,它会启动一个合适的activity,也同样可以支持搜索建议和语音搜索.当我们使用search widget的时候,可能还需要提供对search dialog的支持,比如某些不能支持search widget的情况.
在我们已经创建了searchable configuration和一个searchableactivity之后,我们应该对每个SearchView的搜索辅助功能置为可用.我们可以调用setSearchableInfo()方法并传入SearchableInfo对象来实现这一功能.我们可以通过SearchManager的getSearchableInfo()方法来获取SearchableInfo对象.比如如果我们使用一个SearchView作为action view,我们应该在onCreateOptionsMenu()方法中将widget置为可用:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the options menu from XML
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.options_menu,menu);
// Get the SearchView and set the searchable configuration
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
// Assumes current activity is the searchable activity
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setIconifiedByDefault(false); // Do not iconify the widget; expand it by default
return true;
}
上面的代码就是所有我们所需要做的,search widget已经配置完成,Android将会把查询信息发送给我们的searchable activity.
如果我们想要自己处理更多的输入事件,我们可以在这里(SearchView)找到.
Search widget还提供了一些我们可能会需要的功能:
一个summit按钮:
默认情况下并没有任何的按钮用来提供提交操作,所以用户必须点击键盘上的”return”键,我们可以通过setSubmitButtonEnabled(true)来增加一个”submit”键.
搜索建议详情:
当我们使用搜索建议的时候,我们经常希望用户简单的选择某一选项,但是用户可能想要了解更加详细的信息,这时候我们可以为每个建议增加一个按钮,为用户提供详细信息.相关方法: setQueryRefinementEnabled(true).
改变搜索框的可见性:
默认情况下search widget是”iconified”(图表化)的,意思是它平时只显示为一个搜索图标(一个放大镜样子的图标),当用户点击它的时候,它则会扩展成一个搜索框.我们可以通过setIconifiedByDefault(false)设置为默认情况下显示搜索框.还可以通过setIconified()方法来切换搜索框是不是该显示.
更多的功能可以到SearchView的文档中查找.
当我们在Action Bar上使用search widget的时候,有时候可能APP会运行在一些较小屏幕的设备上,这时候搜索键就显示不出来了,而会被放在悬浮的菜单中.为了处理这种情况我们应该在用户点击悬浮框中的搜索键的时候,显示出一个search dialog.如果想要这样做,我们必须实现onOptionsItemSelected()方法来处理Search菜单项,然后通过onSearchRequested()打开searchdialog.
这个问题的答案最可能由我们使用的版本是不是高于Android3.0来决定.因为SearchView需要Android3.0及以上的版本才能支持,所以如果我们需要在Android3.0及以下版本中开发,我们应该使用search dialog.
如果开发版本是Android3.0及以上,那么则由我们的需求决定.大多数情况下我们应该使用search widget作为一个”action view”放在Action Bar上.然而有些情况这样的做法可能不使用,比如没空间给我们使用Action Bar.所以这种情况下我们应该考虑将search widget放在activity的layout中.如果这些都不能实现,我们则应该使用search dialog.
事实上我们在某些情况下更应该结合使用search widget和search dialog.
要完成一个基本的搜索功能,需要三个要素,包括搜索的配置(searchable configuration),展示结果的activity(searchableactivity)还有搜索接口(search dialog或者search widget).
用户在搜索的接口里输入要搜索的信息,Android根据配置将其发送给searchable activity,并由其处理然后通常在一个ListView中显示出来.