名著阅读 > Android程序设计:第2版 > 搜索界面 >

搜索界面

搜索框架使得应用变得可搜索。要注意,搜索框架只是一个UI框架,并没有提供对真正搜索逻辑的支撑。相反地,它提供了UI部分,支持用户搜索查询并执行它。这又可以调用指定的搜索逻辑,并返回相关结果。为了说明构建搜索逻辑和搜索界面的基础方面,我们将探索一个搜索应用实例,它支持用户通过莎士比亚的“sonnet(十四行诗)”进行搜索。

搜索基础

搜索对应用有些前提要求。首先,它需要真正的逻辑,能够返回搜索结果。它还需要可搜索的配置,建立关于当初始化搜索UI时显示哪些以及如何执行的一些规范。最后,启动可搜索的活动,接收查询,调用搜索逻辑后,显示结果。

搜索逻辑

有很多种方式可以为生成搜索结果创建真正的搜索逻辑。以下我们将探索两个选项:基础的基于索引的搜索以及SQLite数据库支持的搜索,即android.database.sqlite搜索。在任何一种情况下,我们将从基础的构建分块开始:数据对象和SearchLogic接口。

对于数据对象,该实例包含一个主类对象以及一个子类对象。因为用过sonnet进行搜索,我们先定义了一个Sonnet类,它包含标题、sonnet的编号及其行数。当需要在sonnet内检索特定行而不是整个sonnet时,需要用到子对象Sonnet Fragment。该子对象主要用于显示查找结果。


public class Sonnet {
    public int num;
    public String title;
    public String lines;
}
public class SonnetFragment {
    public int num;
    public String line;
}
  

当真正通过UI查找或检索sonnet时,需要为SearchLogic逻辑实现两个方法并调用它们:一是search方法,它接收查询字符串,返回SonnetFragment对象的有序数组;二是getSonnet方法,它根据序号返回特定的Sonnet对象。


public interface SearchLogicInterface {
    SonnetFragment search(String query);
    Sonnet getSonnet(int i);
}
  

基于索引的搜索逻辑。在准备工作中,Sonnet会被写到原始文件中,并对每行进行解析。每构成sonnet的几行会作为一个Sonnet对象进行处理,每行的每个词会作为key整合到大索引库中。对Sonnet值进行设置,每个SonnetRef对象都包含sonnet ID,它包含sonnet所包含的单词以及这些单词的序号列表。当然,也可以使用其他更高级的技术,比如基于单词的定位跟踪(sonnet中每行单词的位置作为一个值),它涉及在sonnet内对单词的含义或上下文进行引用的元数据,计算该sonnet内该单词的权重,为每个sonnet创建单词权重排名系统,以及其他方法处理更准确的搜索查询或生成更具体的查询结果。然而,在本节中,我们将简要介绍最基础的索引搜索系统。

当索引构建完成并且应用可以使用时,当查询指定了特定的搜索单词,会很快返回结果,因为需要处理的逻辑仅仅是在索引和引用的sonnet列表和行列表中查找单词,该单词即索引中的值。在以下实例中,我们通过HashMap存储索引,单词作为关键字以及多个包含sonnet号和行号作为值的SonnetRef对象。


// the index
private HashMap<String, HashSet<SonnetRef>> termindex;
...
// adding the term to the index
HashSet<SonnetRef> set = null;
if(index.containsKey(word)) {
    set = index.get(word);
} else set = new HashSet<SonnetRef>;
set.add(new SonnetRef(sons.size - 1, i));
...
  

为了处理多个单词项,这段逻辑获取所有的SonnetRef对象,通过多个对象的交集查找sonnet编号。交集即最终的返回值。

基于数据库的搜索逻辑。在这段实现中,搜索的核心是一个SQL查询,它查找存储在数据库中的sonnet。当读取sonnet数据时,每个sonnet会添加到数据库中。数据库的列包含引用的sonnet编号、sonnet标题、行号以及行本身。

搜索查询可以通过LIKE语句执行。

可搜索的配置文件

一旦建立了搜索逻辑,就需要理解搜索框架。首先,创建可搜索的配置文件(searchable configuration),它是在res/xml目录下的XML文件,通常命名成searchable.xml。可搜索的配置文件包含特定的属性,这些属性最终构成SearchableInfo对象的配置,系统启动时会对该对象进行初始化。

可搜索的XML配置文件必须包含可搜索的元素作为根节点,而且必须包含android:label属性。


<?xml version=\"1.0\" encoding=\"utf-8\"?>
<searchable xmlns:android=\"http://schemas.android.com/apk/res/android\"
    android:label=\"@string/app_label\"
    >
</searchable>
 

完整的可搜索配置文件语法如下:

为了获取更多关于可搜索配置的信息,请查看Android开发者指南的Search Configuration一节(http://developer.android.com/guide/topics/search/searchable-config.html#searchable-element)。

可搜索的活动

定义了可搜索的配置后,必须创建可搜索的活动。该活动最终会调用搜索逻辑并显示结果。当在Search Dialog或Search Widget中执行搜索时,系统会启动ACTION_SEARCH操作的intent来启动该活动。查询是通过intent中的SearchManager.QUERY字符串值来表示的。活动可以通过查询字符串来调用搜索逻辑。该逻辑返回结果,并显示给用户。在前面给出的例子中,查询字符串会传递给搜索逻辑的search方法,并返回SonnetFragment对象数组。该活动后续的逻辑会负责把结果显示给用户。

第一步是在manifest文件中声明可搜索的活动,向系统指定搜索查找应该在该活动上执行。这是通过把android.intent.action.SEARCH操作添加到intent过滤器中,并执行可搜索的配置来完成的。


<application ... >
    <activity android:name=\".searchdemo.SearchActivity\" >
         <intent-filter>
             <action android:name=\"android.intent.action.SEARCH\" />
         </intent-filter>
         <meta-data android:name=\"android.app.searchable\"
                           android:resource=\"@xml/searchable\"/>
    </activity>
    ...
</application>
  

第二步是决定如何显示搜索结果。通常建议采用表格形式显示结果。在这个例子中,我们使用ListView显示搜索结果。为了简单,该活动扩展了ListActivity,因为ListActivity提供了默认的布局方式ListView,通过getListView方法和setListAdapter方法可以很方便地访问。

在这段代码中,处理系统传递给该活动的ACTION_SEARCH intent这段逻辑很重要。从intent中抽取了SearchManager.QUERY字符串的其他信息(通过getStringExtra方法)后,查询就作为搜索逻辑的输入并返回结果。在这个例子中,把SonnetFragment对象的数组放到ArrayAdapter,该ArrayAdapter被设置成List的适配器,对结果进行显示。

以上是最基础的UI工作。下面,我们将涵盖一些UI组件,用户访问这些组件来执行搜索:即Search Dialog和Search Widget。如果你的目标设备运行在Android 3.0(蜂巢Honeycomb/API 11)或更新的版本上,应该使用Search Widget。

Search Dialog

Search Dialog是个用户界面,它支持用户输入文本并执行搜索。当初始化后,该组件出现在屏幕的最上方。Android系统控制Search Dialog中的所有事件。当用户输入查询并提交时,系统会向manifest文件中指定的活动发送ACTION_SEARCH intent。

为了支持Search Dialog从特定的活动给声明的可搜索的活动发送查询,<meta-data>元素必须包含android:value属性,它指定可搜索的活动的类名称。它还必须包含manifest指定的该活动的Search Dialog和android:name属性,后者的值为“android.app.default_searchable”。


<application ... >
    <activity
          android:label=\"@string/app_name\"
          android:name=\".searchdemo.MainActivity\" >
          <meta-data android:name=\"android.app.default_searchable\"
                    android: />
       </activity>
    </activity>
    ...
</application>
  

有了元数据引用之后,当用户在前端按下设备的该活动的Search按钮(如果设备有该按钮),或者当活动调用onSearchRequested方法时,会激活Search Dialog。


public class MainActivity extends Activity {
      @Override
      public void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.main);
           findViewById(R.id.search).setOnClickListener(new OnClickListener {
                  @Override
                  public void onClick(View v) {
                        onSearchRequested; // activates the Search Dialog
                   }
         });
}
  

Search Dialog会悬浮在屏幕的上方。它不会给活动栈带来任何变化。因此,当出现Search Dialog时,不会调用生命周期方法onPause。活动会失去Search Dialog的输入光标。如果用户按下Back按钮取消搜索,Search Dialog会关闭,活动会重新获取到输入光标。

Search Widget

Search Widget(具体地说即SearchView类)只在Android 3.0(蜂巢Honeycomb/API 11)或更新的版本中提供。建议在Action Bar中使用SearchView作为action视图,可以对菜单项进行折叠,而不是把Search Widget放到活动布局中。要在ActionBar中使用它,首先创建一个定制的菜单XML文件(在这个例子中名为search_menu.xml),在其中一项上引用android:actionView Class=“android.widget.SearchView”,把该XML文件放到res/menu目录中。


<?xml version=\"1.0\" encoding=\"utf-8\"?>
<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">
      <item android:id=\"@+id/menu_search\"
            android:icon=\"@android:drawable/ic_menu_search\"
            android:title=\"@string/search\"
            android:showAsAction=\"ifRoom|withText\"
            android:actionViewClass=\"android.widget.SearchView\"
            />
</menu>
  

有了菜单XML文件后,可以在活动的onCreateOptionsMenu方法中设置。通过SearchableInfo引用SearchView并设置setSearchableInfo方法,表示可搜索的配置。


@Override
public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the options menu from XML
    MenuInflater inflater = getMenuInflater;
        // inflate the search_menu.xml
    inflater.inflate(R.menu.search_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;
    searchView.setSearchableInfo(
            searchManager.getSearchableInfo(getComponentName));
        // Do not iconify the widget; expand it by default
    searchView.setIconifiedByDefault(false);
        // turn on submit button
    searchView.setSubmitButtonEnabled(true);
        // enable query selection refinement
    searchView.setQueryRefinementEnabled(true);
    return true;
}