创建一个搜索接口

当我们打算向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

首先需要的是一个叫做”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

Searchable Activity是APP中用来根据查询字符串处理搜索和展示搜索结果的Activity.当用户在search dialog或者search widget中执行一次搜索操作的时候,Android会启动我们的Searchable Activity,并传给它一个带有ACTION_SEARCH的Intent.然后Searchable Activity可以从intent中名为QUERY的extra中获取到查询的内容,然后就可以搜索数据并显示结果了.但是在此之前Android必须知道哪个Activity是用来做Searchable Activity的,我们应该在manifest文件中声明它.

声明一个Searchable Activity

目前我们首先需要一个处理搜索和显示结果的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的方法.现在分别来介绍上面的三个步骤:

  • 接收查询:

    当用户执行一个搜索操作的时候,Android将会启动我们的Searchable Activity并发送一个带有ACTION_SEARCH的intent对象.这个intent的extra中带有查询字符串.我们必须在Activity启动的时候获取这些信息,栗子:
@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.

  • 展示结果:

    就好像上面提到的,用来展示搜索结果的比较合适的UI是ListView,所以我们可以扩展ListActivity来展示搜索结果,并调用setListAdapter()方法并传入一个Adapter.

使用Search Dialog

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

尽管有些设备提供了专用的搜索按钮,但是每种设备的搜索按钮的点击事件可能有所不同,并且很多设备并不提供这样的按钮,所以当我们使用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)方法来实现.

Activity的生命周期对search dialog的影响

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()方法.

使用SearchWidget

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的情况.

配置searchwidget

在我们已经创建了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)找到.

Searchwidget的其他功能

Search widget还提供了一些我们可能会需要的功能:

  • 一个summit按钮:
    默认情况下并没有任何的按钮用来提供提交操作,所以用户必须点击键盘上的”return”键,我们可以通过setSubmitButtonEnabled(true)来增加一个”submit”键.

  • 搜索建议详情:
    当我们使用搜索建议的时候,我们经常希望用户简单的选择某一选项,但是用户可能想要了解更加详细的信息,这时候我们可以为每个建议增加一个按钮,为用户提供详细信息.相关方法: setQueryRefinementEnabled(true).

  • 改变搜索框的可见性:
    默认情况下search widget是”iconified”(图表化)的,意思是它平时只显示为一个搜索图标(一个放大镜样子的图标),当用户点击它的时候,它则会扩展成一个搜索框.我们可以通过setIconifiedByDefault(false)设置为默认情况下显示搜索框.还可以通过setIconified()方法来切换搜索框是不是该显示.

更多的功能可以到SearchView的文档中查找.

结合使用searchwidget和search dialog

当我们在Action Bar上使用search widget的时候,有时候可能APP会运行在一些较小屏幕的设备上,这时候搜索键就显示不出来了,而会被放在悬浮的菜单中.为了处理这种情况我们应该在用户点击悬浮框中的搜索键的时候,显示出一个search dialog.如果想要这样做,我们必须实现onOptionsItemSelected()方法来处理Search菜单项,然后通过onSearchRequested()打开searchdialog.

应该使用Searchdialog还是使用Search widget

这个问题的答案最可能由我们使用的版本是不是高于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中显示出来.

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1