安卓专家级编程(五)
十、Android 搜索简介
Abstract
Android 中的搜索功能扩展了人们熟悉的基于网络的谷歌搜索栏,可以搜索基于设备的本地内容和基于互联网的外部内容。您可以进一步使用这种搜索机制,直接从主页上的搜索结果中发现和调用应用。Android 通过提供一个允许所有应用参与搜索的搜索框架,使这些功能成为可能。
Android 中的搜索功能扩展了人们熟悉的基于网络的谷歌搜索栏,可以搜索基于设备的本地内容和基于互联网的外部内容。您可以进一步使用这种搜索机制,直接从主页上的搜索结果中发现和调用应用。Android 通过提供一个允许所有应用参与搜索的搜索框架,使这些功能成为可能。
Android 搜索包含一个搜索框,让用户输入搜索数据。无论您是使用主页上的全局搜索框还是通过自己的应用进行搜索,都是如此:您使用相同的搜索框。
这个搜索框可以采用三种形式之一:搜索小部件、搜索对话框或搜索视图。搜索小部件是一个 Android 小部件,可以拖放到主屏幕上。应用可以调用搜索对话框来帮助用户输入搜索文本。这个搜索对话框可以由主页上或应用内部的搜索小部件调用。搜索视图是一种特殊的搜索对话框,它嵌入在视图中,特别是应用的操作栏中,用于搜索特定于该应用的数据。
当用户在搜索视图或搜索对话框中输入文本时,Android 获取文本并将其传递给各种已注册响应搜索的应用。应用将通过返回一组响应进行响应。Android 汇总了来自多个应用的这些响应,并将其作为可能的建议列表呈现。当用户点击其中一个响应时,Android 会调用给出建议的应用,按照给出建议的应用的设计做出正确的响应。从这个意义上说,Android 搜索是一组参与应用之间的联合搜索。
在这一章中,你将学到三件事:(1)在 Android 平台上搜索的最终用户体验是什么,(2)活动如何与搜索框架交互以调用或响应搜索,以及(3)在编写可搜索的应用时如何解决设备差异。这也是一个关于 Android 搜索的介绍性章节,它构成了编写你自己的搜索提供者的基础,这将在接下来的两个章节中讨论。
探索 Android 全球搜索
你不能错过 Android 设备上的搜索框;通常显示在首页,如图 10-1 所示。此搜索框也称为快速搜索框(QSB)或搜索小部件。在 Android 的某些版本中,或者取决于设备制造商或运营商,默认情况下,您可能不会在主屏幕上看到这个搜索小部件。本章中的所有图形都是使用 Android 4.0 中的模拟器捕获的。
如果您在主窗格上没有看到搜索小部件,或者如果您之前已经删除了它,就像任何其他 Android 小部件一样,您可以通过转到小部件目录并选择将搜索小部件放置在您可能有的任何主屏幕上来找到它。您也可以通过将搜索小部件拖到垃圾桶来将其从主页中移除。当然,您可以再次从小部件选项卡/屏幕中重新绘制它。
图 10-1。
Android home page with a search widget
QSB 作为一个窗口小部件的一个副作用是,将焦点转移到主页上的搜索窗口小部件以便输入数据,这基本上会将你带入一个全局搜索对话框(见图 10-2 ),从而你离开主页上下文,进入搜索对话框的上下文。
图 10-2。
Global search dialog spawned from the home search widget
您也可以通过点击物理搜索键来调用图 10-2 的搜索对话框(如果您的设备上有)。当搜索键可用时,就像 Home 键一样,您可以随时点按搜索键,而不考虑可见的应用。
趋势是没有物理搜索键。搜索关键字可能是虚拟的,或者它甚至可能不是一个全局图标。在这种情况下,您将不得不依赖于搜索小部件。作为一个好的模式,建议应用使用自己的搜索菜单项,或者在应用的操作栏中放置一个搜索视图。简而言之,您可以可靠地假设搜索小部件将始终可用,并且您的应用应该通过提供菜单项(如果有操作栏,它可以变成搜索视图)来显式地提供搜索体验。
棘手的是,虚拟或物理的搜索关键字可能存在,但如果你想利用它,你必须为它编程。当这个键存在时,并且当一个应用处于焦点时,应用就有机会专门化搜索(我们将在后面讨论)。这种定制的搜索称为本地搜索。更一般、普通和非定制的搜索称为全局搜索。
Note
当应用处于焦点时按下搜索键,由应用决定是允许还是不允许本地和全局搜索。在 2.0 之前的版本中,默认操作是允许全局搜索。在 2.2 版和更高版本中,默认行为是禁用全局搜索。这意味着当一个活动成为焦点时,用户必须首先单击 Home 键,然后单击 search 键或 Search 小部件,如果他或她想要进行全局搜索的话。
在 2.2 版本之前,Android 全局搜索框不提供仅在单个应用中搜索数据的选择。所有启用搜索的应用都被视为全局搜索的上下文。
从 2.2 开始,Android 搜索允许用户选择特定的搜索上下文(或应用)。他们可以通过点击全局搜索活动的左侧图标来实现这一点,这将打开提供搜索的单个搜索应用的选择(参见图 10-3 )。
图 10-3。
Global search dialog with various applications’ search contexts
你在图 10-3 中看到的是 4.0 中默认的搜索应用。该列表可能会随后续版本而变化。搜索上下文“All”的行为很像以前版本的全局搜索。您还可以通过将您的应用注册为可能的可搜索应用之一来创建自己的搜索上下文。我们将在接下来的两章中更详细地讨论这一方面,这里主要关注用户的搜索体验。
让我们暂时回头参考图 10-2 。根据您过去对设备的使用情况,图 10-2 中显示的图像可能会有所不同,因为 Android 会根据您过去的操作来猜测您在搜索什么。当在 QSB 中没有输入文本时,这种搜索模式被称为零建议模式。这是因为可搜索的应用没有被给予任何输入以提供建议。
根据输入的搜索文本,Android 会向用户提供一些建议,如图 10-4 所示。这些建议以列表形式显示在 QSB 下方,称为搜索建议。当你在搜索框中输入每个字母时,Android 会动态替换搜索建议以反映新信息。当没有搜索文本时,Android 会显示所谓的零建议。在图 10-2 中,Android 已经确定没有一个搜索应用主动提出任何零建议。但是,当您开始使用该设备时,您可能会看到一些建议,因为默认的建议提供程序可能会调出您上次的搜索字符串作为可能的建议,供您再次搜索。图 10-4 显示了当你在 QSB 中输入一些文本时出现的搜索建议。
图 10-4。
Search suggestions
图 10-4 中显示了六个重点区域。这些是:
The left-side search context/application icon The search box The search arrow on the right The list of suggestions The pencil icon next to each suggestion The Go button on the keyboard
左侧的搜索上下文/应用图标表示您的搜索上下文。对于“全部”,它通常是设备制造商提供的图标。例如,它可以是谷歌,也可以是必应,或者只是一个标准的搜索图标。
搜索框或 QSB 是一个编辑控件,您可以在其中输入搜索文本。如果搜索上下文是“All”,并且存在一些搜索文本,您可以单击右侧的搜索箭头,它将启动由设备制造商控制的默认搜索,这通常是浏览器搜索。此搜索的输入将是您在搜索框中输入的搜索文本。如果搜索的上下文是一个特定的应用,那么该应用中的一个活动将使用搜索框提供的输入来调用。
在图 10-4 中,建议列表中的每一项也是一组文字。然而,情况并不总是如此。这些建议可以是可以直接调用的应用的名称(稍后如图 10-5 所示)。如图 10-4 所示,当一个建议是一个可在网上搜索的文本时,你可以点击右边的铅笔图标,将建议文本移动到搜索编辑框中,从而对其进行修改。
在图 10-4 中还可以看到,键盘上的 Enter 按钮已经变成了 Go 按钮。在“全部”上下文的全局对话框中,此行为取决于制造商。当搜索上下文是您的应用时,您可以使用相关图标。
让我们再看一遍建议清单。Android 获取到目前为止已经输入到搜索框中的搜索文本,并寻找由可搜索应用提供的所谓的建议提供者。Android 异步并并行地调用每个建议提供者,以一组行的形式检索一组匹配的建议。Android 期望这些行(称为搜索建议)符合一组预定义的列(建议列)。通过探索这些众所周知的专栏,Android 绘制了建议列表。
当搜索文本改变时,Android 重复这个过程。为搜索建议调用所有建议提供者和接收建议的这种交互对于“所有”搜索上下文是真实的。然而,如果您要选择一个特定的搜索应用上下文(如图 10-3 所示),则只有为该应用定义的建议提供程序将被调用来检索搜索建议。
Note
搜索建议集也称为建议光标。这是因为代表建议提供者的内容提供者返回了一个cursor对象。
图 10-5 显示了一个建议列表的例子,其中一些建议指向已经安装在设备上的应用。对于“se”的输入文本,有两个应用与此名称匹配:搜索和设置。各个应用的图标也显示在建议列表中,如左侧所示。如果您选择其中一个应用(搜索或设置),则会调用相应的应用。
图 10-5。
Search suggestions for applications
为全球搜索启用可搜索的应用
正如我们已经说过的,您可以编写指定搜索特定数据集的应用。这些应用然后需要在它们的清单文件中注册它们将被考虑用于搜索。即使在此之后,最终用户也必须选择这些应用的一个子集或全部,使其成为可搜索上下文的一部分。
要选择或取消选择这些搜索应用,您必须访问搜索设置。要访问搜索设置,请单击主页上的搜索小部件。这将把您带到全局搜索对话框,正如我们到目前为止所展示的那样。现在,单击菜单(虚拟或物理)图标。迄今为止,大多数手机似乎都带有硬件菜单按钮;在平板电脑上,您可能会在屏幕的右上角看到软件菜单图标。该屏幕将显示可用于全局搜索活动的单个菜单项,如图 10-6 所示。
图 10-6。
Global search activity/dialog menu
如果你点击搜索设置菜单,如图 10-6 所示,你会看到搜索设置选项可用;见图 10-7 。
图 10-7。
Global search settings
“清除快捷方式”选项会删除搜索历史。相反,选择“可搜索项目”来查看可用的搜索应用集,如图 10-8 所示。
图 10-8。
Available searchable applications
如图 10-8 所示,一组可搜索的应用可用或安装在设备上。您可以根据需要选中/取消选中这些应用。
这就结束了对 Android 中搜索功能的高级使用的讨论。您还可以在网上搜索每种设备的专用用户指南。这些具体的指南将进一步展示不同设备之间的搜索体验可能会有所不同。我们提供了一个 Android 4.0 Nexus 用户指南的 URL 参考。
我们现在研究活动如何与搜索框架交互。
活动和搜索关键字交互
当用户在一个活动处于焦点时点击搜索键会发生什么?答案取决于很多因素。假设有一个搜索关键字,我们可以探究它对以下类型活动的影响:
- 不知道当前搜索的常规活动
- 明确禁止搜索的活动
- 明确调用全局搜索的活动
- 使用本地搜索的活动
常规活动中搜索关键字的行为
如果有一个完全不知道正在进行搜索的常规活动,单击搜索键将调用回调函数onSearchRequested() callback。默认情况下,这将调用本地搜索,除非另有定义。因为这是一个没有定义本地搜索的活动,所以单击搜索关键字不会产生任何影响。
Note
在 4.0 模拟器中,默认情况下不启用搜索键。如果您想测试这种行为,请转到 AVD 定义并选择启用物理键盘和 DPAD。
这种行为是有含义的。如果您的意图是在按下搜索键时调用全局搜索,那么您需要覆盖onSearchRequested()并显式调用全局搜索。或者,如果没有物理搜索关键字,您需要在应用中提供一个搜索菜单项。
禁用搜索的活动的行为
通过从 activity 类的onSearchRequested()回调方法返回 false,activity 可以选择完全禁用搜索(全局和局部)。清单 10-1 显示了这种禁用的一个例子。
清单 10-1。以编程方式禁用搜索
//filename: NoSearchActivity.java
public class NoSearchActivity extends Activity
{
.....other code
@Override
public boolean onSearchRequested()
{
return false;
}
}
通过菜单显式调用搜索
除了能够响应搜索关键字,活动还可以选择通过搜索菜单项显式调用搜索。当不再有物理搜索关键字时,这很有用。相反,您需要提供一个显式的菜单项。如果有足够的空间,这个菜单项可以嵌入到动作栏中。当搜索是一个菜单项时,您需要像处理任何其他菜单项一样处理它,并通过自己调用onSearchRequested()显式地调用搜索。
清单 10-2 显示了当一个菜单项被按下时调用搜索的一个活动(SearchInvokerActivity)的源代码。
清单 10-2。通过菜单调用搜索
public class SearchInvokerActivity extends Activity
{
.....other stuff
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
if (item.getItemId() == R.id.mid_si_search)
{
this.onSearchRequested();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onSearchRequested()
{
this.startSearch("test",true,null,true);
return true;
}
}
源代码的关键部分以粗体突出显示。注意菜单 ID ( R.id.mid_si_search)是如何调用函数onSearchRequested()的。这个方法,onSearchRequested(),调用搜索。
基础方法"startSearch"具有以下参数:
initialQuery:要搜索的文本。- selectInitialQuery:一个布尔值,指示是否突出显示搜索文本。在这种情况下,我们使用“true”来突出显示文本,以便在需要时可以删除它以支持新的文本。
appSearchData:要传递给搜索活动的 bundle 对象。在这种情况下,我们不针对任何特定的搜索活动。在清单 10-2 中,我们为这个参数传递了 null。globalSearch:如果为真,则调用全局搜索。如果为假,则调用本地搜索(如果可用);否则,调用全局搜索。
SDK 文档建议调用基类onSearchRequested(),不像我们在清单 10-2 中显示的那样。然而,默认的onSearchRequested()使用“false”作为startSearch()的最后一个参数。根据文档,如果没有可用的本地搜索,这将调用全局搜索。但是,在最近的版本中(从 2.2 开始),不调用全局搜索。这可能是一个 bug,也可能是设计出来的,需要更新文档。
在本例中,我们通过向最后一个参数startSearch()传递“true”来强制进行全局搜索。
了解本地搜索
现在,让我们看看在什么情况下,应用中的搜索关键字或专门的搜索图标不会调用全局搜索,而是调用本地搜索。但是首先,我们必须进一步解释本地搜索。
本地搜索有四个组成部分:
A search dialog (with a QSB in it) or a search view A search results activity A searchable info XML file (search configuration) An invoker activity that starts the search
第一个组件是搜索对话框或搜索视图,其中包含一个搜索框,与全球搜索 QSB 非常相似(如果不是相同的话)。这个 QSB,无论是本地的还是全局的,都提供了一个用于输入文本的编辑文本控件和一个用于单击的搜索图标。当活动在清单文件中声明它需要本地搜索时,将调用本地 QSB 而不是全局。您可以通过查看图 10-10 中的图标和 QSB 中的提示(搜索框内的文本)来区分调用的本地 QSB 和全局。正如您将看到的,这两个值来自一个搜索配置元数据 XML 文件。
本地搜索的第二个组成部分是一个活动,它可以从 QSB(本地或全局)接收搜索字符串,并显示一组结果或与搜索文本相关的任何其他输出。这种活动通常被称为搜索活动或搜索结果活动。
第三个组件是一个名为SearchableInfo的 XML 搜索元数据文件,它定义了 QSB 应该如何表现,以及是否有任何建议提供者与该搜索相关联。
本地搜索的第四个组件是允许调用刚才描述的搜索结果活动的活动(第二个组件)。这个调用活动通常被称为搜索调用者或搜索调用活动。这个搜索调用程序活动是可选的,因为可以通过建议让全局搜索直接调用本地搜索活动(第二个组件)。
在图 10-9 中,你可以看到这四个组件以及它们在上下文中是如何相互作用的。
图 10-9。
Local search activity interaction
在图 10-9 中,重要的交互显示为带注释的箭头(带圆圈的数字)。以下是更详细的解释:
SearchActivity(或搜索结果活动)是安卓搜索中的中枢。搜索元数据 XML 文件和建议提供者都挂起了这个活动。需要在清单文件中将SearchActivity定义为能够接收搜索请求的活动。SearchActivity还使用一个强制性的 XML 文件来声明本地 QSB 应该如何呈现(比如标题、提示等等),以及是否有相关的建议提供者(参见清单 10-6)。在图 10-9 中,你可以看到这是在SearchActivity和两个 XML 文件(清单文件和搜索元数据文件)之间的几个“定义”行。- 一旦在清单文件中定义了
SearchActivity(参见清单 10-5),清单文件中的Search InvokingActivity通过清单 10-8)中的元数据定义android.app.default_searchable(表明它与SearchActivity相关联。 - 有了这两个活动的定义,当
SearchInvokingActivity成为焦点时,按下搜索键将调用本地 QSB。你可以在图 10-9 中看到这一点——编号为 1 和 2 的圆圈。您可以通过查看 QSB 的标题和提示来判断所调用的 QSB 是本地 QSB。这两个值是在强制搜索元数据 XML 定义中设置的。一旦通过搜索关键字调用 QSB,您将能够在 QSB 中键入查询文本。这个本地的 QSB,类似于全球的 QSB,能够提出建议。 - 一旦输入了查询文本并点击了搜索图标,本地 QSB 就会将搜索转移给负责处理它的
SearchActivity,比如显示一组结果。
我们通过查看每个相关文件的代码片段来进一步研究这些交互。我们从清单 10-3 开始,SearchActivity的源代码(它也负责接收查询并显示搜索结果)。
清单 10-3。简单的搜索结果活动
//filename: SearchActivity.java
public class SearchActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.search_activity);
.....
//Use the invoking intent to receive the search text
//and do present what is needed
......
return;
}
}
这是一个非常简单的带有文本控件的活动。清单 10-4 显示了它的布局。
清单 10-4。简单搜索结果活动的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android
android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"
>
<TextView
android:id="@+id/text1"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="@string/search_activity_prompt"
/>
</LinearLayout>
我们使用了最简单的搜索活动。在下一章中,您将看到该活动如何检索搜索文本或搜索查询。现在,我们展示 QSB 是如何调用这个活动的。清单 10-5 显示了如何将SearchActivity定义为负责清单文件中搜索结果的搜索活动。
清单 10-5。用搜索元数据在清单文件中定义搜索结果活动
<activity android:name=".SearchActivity"
android:label="Activity/QSB Interaction::Search Results">
<intent-filter>
<action android:name="android.intent.action.SEARCH"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
Note
对于搜索活动,需要指定两件事情。活动需要表明它可以响应搜索操作。它还需要指定一个 XML 文件,描述与该搜索活动交互所需的元数据。
清单 10-6 显示了这个SearchActivity的搜索元数据 XML 文件。
清单 10-6。搜索结果活动的 SearchableInfo
<!-- /res/xml/searchable.xml -->
<searchable xmlns:android="http://schemas.android.com/apk/res/android
android:label="@string/search_label"
android:hint="@string/search_hint"
android:searchMode="showSearchLabelAsBadge"
/>
Tip
该 XML 中可用的各种选项记录在位于 http://developer.android.com/guide/topics/search/searchable-config.html 的 SDK 中。
我们将在接下来的两章中讨论更多这些可搜索的 info XML 属性。现在,属性android:label用于标记搜索框。属性android:hint用于在搜索框中放置初始文本(见图 10-10 )。清单 10-6 中的android:searchMode属性表示使用android:label属性来标记搜索框。这个选项在手机上看起来很好,标签在一行,搜索框在下面。但是,在 4.0 中,这个标签与搜索框一致;这看起来很糟糕,因为它占用了搜索框的空间。最好用showSearchIconAsBadge来代替。
现在让我们看看活动如何将这个SearchActivity指定为它的搜索。我们称之为调用激活LocalSearchEnabledActivity。清单 10-7 显示了这个LocalSearchEnbaledActivity的源代码。
清单 10-7。LocalSearchEnabledActivity 源代码
public class LocalSearchEnabledActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.local_search_enabled_activity);
return;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.search_invoker_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.mid_si_search)
{
onSearchRequested();
return true;
}
return super.onOptionsItemSelected(item);
}
}
注意,在清单 10-7 中,我们为搜索定义了一个名为R.id.mid_si_search的菜单项。这个菜单项是菜单文件的一部分,在清单 10-7 中用R.menu.search_invoker_menu表示。我们将让您创建一个满足R.id.mid_si_search和R.menu.search_invoker_menu的菜单文件。如果选择了菜单项R.id.mid_si_search,那么我们称之为onSearchRequested()。Android 此时如何知道调用本地搜索?清单文件中的LocalSearchEnabledActivity的定义阐明了 Android 与SearchActivity的关系,如清单 10-8 所示。
清单 10-8。通过元数据绑定搜索结果活动
<activity android:name=".LocalSearchEnabledActivity"
android:label="Activity/QSB Interaction::Local Search">
<meta-data android:name="android.app.default_searchable"
android:value=".SearchActivity" />
</activity>
注意这个LocalSearchEnabledActivity是如何指向中枢销SearchActivity的。SearchActivity反过来告诉本地 QSB 应该如何呈现,搜索文本被传递给那个SearchActivity。
Note
您也可以在应用级别使用这个元数据定义,这样所有活动都将继承这个搜索活动。如果需要,单个活动可以进一步覆盖应用级别的搜索活动。以前的版本在这里接受“”来表示全局搜索;该“”规范现在已被弃用。
当LocalSearchEnabledActivity处于焦点时,如果你点击设备搜索,或者使用清单 10-7 中的菜单项,两者都将调用本地搜索框(本地 QSB),如图 10-10 所示。
图 10-10。
Local search QSB
注意这个搜索框上的图标和这个搜索框的提示。看看它们与全局搜索有何不同(见图 10-2 )。该图标来自应用包的图标,如应用包的清单文件中所定义。该提示来自为SearchActivity指定的搜索元数据(searchable.xml,清单 10-6)。现在,如果您在 QSB 中输入文本并点击搜索图标,您将最终调用SearchActivity(参见清单 10-3)。图 10-11 显示了这个SearchActivity的样子。
图 10-11。
Search results in response to the local search QSB
虽然这个活动不使用任何查询搜索文本来获取结果,但是它演示了如何定义和调用搜索活动。在接下来的两章中,我们将展示这个SearchActivity如何利用搜索查询以及它需要响应的各种与搜索相关的动作。
启用键入搜索
当你在查看如图 10-10 所示的LocalSearchInvokerActivity活动时,有一种方法可以通过键入一个随机的字母(如“t”)来调用搜索。这种模式称为“键入搜索”,因为您键入的任何不受活动处理的键都将调用搜索。
键入搜索的意图是这样的:在任何 Android 活动上,你可以告诉 Android 任何按键都可以调用搜索——除了活动明确处理的按键。例如,如果一个活动处理“x”和“y”,但不关心任何其他键,该活动可以选择调用对任何其他键的搜索,如“z”或“a”。这种模式对于已经显示搜索结果的活动很有用;它可以将按键解释为再次开始搜索的提示。
下面是几行代码,您可以在活动的onCreate()方法中使用它们来实现这种行为(第一行用于调用全局搜索,第二行用于调用局部搜索):
this.setDefaultKeyMode(Activity.DEFAULT_KEYS_SEARCH_GLOBAL);
或者
this.setDefaultKeyMode(Activity.DEFAULT_KEYS_SEARCH_LOCAL);
在操作栏中使用 SearchView
到目前为止,我们已经在主页上看到了一个搜索小部件、一个全局搜索对话框和一个本地搜索对话框。还有另一种方法来利用搜索功能。这是通过一个通过动作栏暴露出来的SearchView来实现的,这是现在手机和平板电脑都推荐的模式。
图 10-12 显示了手机上的搜索视图,以及顶部标题/操作栏中的应用图标。
图 10-12。
Search view in the action bar of a phone
图 10-13 显示了带有搜索视图的操作栏在平板电脑上的外观。
图 10-13。
Search view in the action bar of a tablet
将搜索视图小部件定义为菜单项
要定义一个显示在活动操作栏中的搜索视图,您需要在一个菜单 XML 文件中定义一个菜单项,如清单 10-9 所示。
清单 10-9。搜索视图菜单项定义
<item android:id="@+id/menu_search"
android:title="Search"
android:showAsAction="ifRoom"
android:actionViewClass="android.widget.SearchView"
/>
清单 10-9 中的关键元素是指向android.widget.SearchView的actionViewClass属性。其他属性仅仅是通常的菜单项属性。
标识搜索视图小部件的搜索目标
到目前为止,您的操作栏中已经有了搜索视图,并且您已经有了可以响应搜索的活动(SearchActivity)。我们需要将这两部分结合在一起,这是用 Java 代码完成的。作为设置菜单的一部分,您需要在搜索调用活动的onCreateOptions()回调中这样做。清单 10-10 中的函数可以从onCreateOptions()中调用,以便将搜索视图小部件和搜索结果活动联系起来。
清单 10-10。将搜索视图小部件绑定到搜索结果活动
private void setupSearchView(Menu menu)
{
//Locate the search view widget
//as indicated by the menu item of listing 10-9
SearchView searchView =
(SearchView) menu.findItem(R.id.menu_search).getActionView();
if (searchView == null)
{
Log.d(tag, "Failed to get search view");
return;
}
//setup searchview
SearchManager searchManager =
(SearchManager) getSystemService(Context.SEARCH_SERVICE);
ComponentName cn =
new ComponentName(this,SearchActivity.class);
SearchableInfo info =
searchManager.getSearchableInfo(cn);
if (info == null)
{
Log.d(tag, "Failed to get search info");
return;
}
searchView.setSearchableInfo(info);
// Do not iconify the widget; expand it by default
searchView.setIconifiedByDefault(false);
}
为了练习这段代码,我们在本章的示例项目中包含了一个名为ActionBarSearchActivity的活动。这就是我们在图 10-12 中展示的活动。
清单 10-9 中的菜单项可能在活动的动作栏中没有空间。在这种情况下,它仅仅是一个菜单项,你必须显式地调用onSearchRequested()来调用搜索对话框,而不是搜索视图。你可以看到这个例子的代码清单 10-7。
由于篇幅限制,我们没有包括测试本章中介绍的概念所需的每个文件。你可以下载本章的专用项目;我们在参考资料中给出了该项目的 URL。
参考
以下是我们在撰写本章时发现的有价值的资源列表。
http://static.googleusercontent.com/external_content/untrusted_dlcp/www.google.com/en/us/help/hc/img/android/android_ug_42/Nexus-7-Guidebook.pdf:这既是一篇 Nexus 7 用户指南,也是讲 Android 4.0 中的搜索用户体验。你也可以在谷歌上搜索“Android Jellybean 用户指南”您可能会发现市场上每种设备都有专门的指南。http://developer.android.com/guide/topics/search/index.html:谷歌安卓搜索概述及词条文档。http://developer.android.com/guide/topics/search/searchable-config.html:Google 的这个 URL 是一个关键文档,它概括了 searchableinfo xml 文件中可用的属性。http://developer.android.com/reference/android/app/SearchManager.html:主要 Android 搜索工具的 API 引用,即SearchManager。http://www.androidbook.com/notes_on_search:在这个网址你可以在安卓搜索上找到作者的笔记。本书出版后,我们将继续更新内容。您将在这里找到代码片段、摘要、用于搜索的关键 URL,以及每个版本中的变化。- 在
www.androidbook.com/expertandroid/projects下载本章测试项目。ZIP 文件的名称是ExpertAndroid_Ch10_AndroidSearch.zip。
摘要
这是关于 Android 搜索的介绍性章节,涵盖了用户体验。我们已经展示了如何使用搜索关键字来调用搜索。我们已经展示了创建能够响应搜索的搜索结果活动的基础。我们解释了如何将应用或搜索调用活动与搜索结果活动联系起来。我们已经展示了搜索结果活动如何通过可搜索的 info XML 文件定义其搜索需求。我们已经演示了如何在动作栏中使用 searchview。我们还列出了在设备上实现搜索功能的各种方式,以及如何编写代码来涵盖所有这些情况。
接下来的两章进一步探讨了检索搜索文本和产生结果的搜索结果活动。我们还将介绍如何编写简单和定制的建议提供程序,在搜索对话框中提供搜索建议。
复习问题
以下问题是你在本章所学内容的里程碑。
What is QSB? What is a search widget, a search dialog, and a search view? What is searchable info? What is a global search and a local search? What is a search results activity? How do you attach a search results activity to an application or an activity? What is search metadata? What are search suggestions? What are suggestion columns? What is a suggestion cursor? What is a suggestions provider? What is zero suggestions mode? How do you craft a search so that it works well when the phyical search key is present and when it is not? How do you craft a search so that it works well on both tablets and phones? How does a search interact with the action bar? How do you attach searchable info to a search view in the action bar? How do you define the search icon for an action bar? What is the class name you would use for a search view in the menu.xml? How do you enable the Search key in the emulator if it is disabled by default?
十一、简单搜索建议供应器
Abstract
我们在第十章中探讨了 Android 搜索的用户体验。在那里,我们解释了搜索应用如何响应用户输入的搜索文本提供建议。搜索应用中响应建议的组件称为建议提供者。本章介绍了建议提供者,并探索了一个名为SearchRecentSuggestionsProvider.的预建建议提供者。在下一章中,我们将向您展示如何从头开始编写一个定制的搜索建议提供者。
我们在第十章中探讨了 Android 搜索的用户体验。在那里,我们解释了搜索应用如何响应用户输入的搜索文本提供建议。搜索应用中响应建议的组件称为建议提供者。本章介绍了建议提供者,并探索了一个名为SearchRecentSuggestionsProvider的预构建建议提供者。在下一章,我们将向你展示如何从头开始编写一个定制的搜索建议提供者。
当你写一个建议提供者的时候,有三个主要部分需要关注。第一个是建议提供者 Java 类,负责将这些建议返回给 Android 搜索。第二个是搜索结果活动(类似于第十章中讨论的活动),它接受一个查询或建议并将其转化为搜索结果。在本章中,我们交替使用搜索活动和搜索结果活动这两个术语。
编写建议提供者涉及的第三部分是元数据 XML 文件(也在第十章的中介绍),它是在搜索活动的上下文中定义的。这个搜索元数据 XML 文件也被称为searchableinfo XML 文件,因为在 Java 源代码中,这个文件通常被称为“在SearchActivity object上获取和设置searchableinfo”
我们描述了每个搜索应用的职责,并通过源代码片段展示了如何实现它们以形成一个简单的建议提供者。在本章末尾的参考资料中,我们还提供了一个下载完整工作应用的链接。
规划简单的建议提供者
所有搜索建议供应器的目标都是一样的:从 Android 搜索工具(快速搜索框,也称为 QSB,在第十章中介绍)接收部分或全部搜索文本,并以建议光标的形式回复一组建议(一组搜索建议行)。建议提供者是内容提供者。因此,实现一个建议提供者本质上与实现一个内容提供者是一样的,尽管有一组固定的输入和输出。
要演示一个建议提供者,最简单的方法是扩展预构建的SearchRecentSuggestionsProvider。SearchRecentSuggestionsProvider允许您重放(或建议)之前从 QSB 提交给搜索结果活动的查询。一旦这些查询被搜索结果活动保存,当用户开始在 QSB 中键入文本时,它们通过建议提供者被提示回 QSB。
SearchRecentSuggestionsProvider从一组空泛的建议开始。用户在搜索框中键入一些文本。该文本提供给查找结果的搜索活动。搜索活动还保存搜索文本作为下次可能的建议。这个保存操作本质上是保存到SearchRecentSuggestionsProvider;中,它允许建议提供者将那些先前的搜索表示为建议。但是,这是专门针对SearchRecentSuggestionsProvider的协议。在下一章中,当你学习编写一个定制的建议提供者时,你可以自由地定义你将如何用建议来响应。
虽然这个协议已经由SearchRecentSuggestionsProvider实现了,但是您需要初始化和专门化SearchRecentSuggestionsProvider来满足您特定的搜索需求。在派生的建议提供程序中,您通过指示搜索文本的哪些部分需要重放来初始化基本的SearchRecentSuggestionsProvider。
在我们简单的演示应用中,我们使用了一个最小的搜索结果活动,它只是一个文本视图,以显示搜索结果活动已经被调用。在搜索结果活动中,我们还将向您展示检索和保存搜索查询的方法,以便下次搜索建议提供者可以使用它们。
一旦应用完成,我们应该在本地和全局 QSB 中看到作为建议提示的前面的查询。在本章的最后一节,我们用图表展示了这种用户体验。
实现简单建议提供程序
因为简单的建议提供者是从SearchRecentSuggestionsProvider派生出来的,所以大部分职责都由这个基类来处理。因为建议提供者是内容提供者,所以您需要一个唯一的字符串来标识该内容提供者的权限。仔细阅读内容供应器,因为我们在本章中假设你已经知道内容供应器。内容供应器的权限是它的 web 类调用 URL。
base SearchRecentSuggestionsProvider需要用这个应用唯一的授权字符串进行初始化。Android search 然后基于唯一的内容供应器 URL 调用这个建议供应器。一旦使用这个对基本SearchRecentSuggestionsProvider类的简单调用实现了派生的建议提供者,就需要在清单文件中将其配置为具有权限的常规内容提供者。它(间接地通过可搜索的元数据 XML 文件)与搜索活动联系在一起。搜索活动定义引用可搜索的 XML 文件,该文件又指向建议提供者。
现在,我们将通过带注释的代码片段详细展示所有这些步骤。完整的源代码和工作应用可以从本章末尾参考资料中的 URL 下载。因为我们从SearchRecentSuggestionsProvider继承,简单建议提供者的源代码将会非常简单,如清单 11-1 所示。
清单 11-1 . SimpleSuggestionProvider.java
//SimpleSuggestionProvider.java
public class SimpleSuggestionProvider
extends SearchRecentSuggestionsProvider {
final static String AUTHORITY =
"com.androidbook.search.simplesp.SimpleSuggestionProvider";
final static int MODE =
DATABASE_MODE_QUERIES | DATABASE_MODE_2LINES;
public SimpleSuggestionProvider() {
super();
setupSuggestions(AUTHORITY, MODE);
}
}
清单 11-1 中有几件值得注意的事情:
The parent class is initialized. The base provider is set up with an authority and mode, indicating what portions of a search text need to be remembered.
建议内容供应器授权字符串必须是唯一的。清单 11-1 中的惟一授权字符串需要与清单文件中的内容提供者定义相匹配。这个SimpleSuggestionProvider(清单 11-1)和任何其他内容提供者一样在清单文件中注册。清单 11-2 显示了这个定义。
清单 11-2。清单文件中的 SimpleSuggestionProvider
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android=http://schemas.android.com/apk/res/android
<application...>
<provider android:name=".SimpleSuggestionProvider"
android:authorities
="com.androidbook.search.simplesp.SimpleSuggestionProvider" />
</application>
</manifest>
注意简单建议提供者的权限在源代码(清单 11-1)和清单文件(清单 11-2)中是如何匹配的。在这两种情况下,这种权威的价值是
com.androidbook.search.simplesp.SimpleSuggestionProvider
Android 提供的SearchRecentSuggestionsProvider工具的一个关键功能是存储/重放来自数据库的查询,以便它们可以作为未来的建议。一个建议有两个文本字符串(参见本章后面的图 11-6 )。一个字符串是查询,另一个字符串是显示在建议显示项中的描述行。只有第一个字符串是必需的。当您使用SearchRecentSuggestionsProvider来重放这些字符串时,您需要告诉它您是想要使用一个字符串还是两个字符串。
为了适应这种情况,基本建议提供程序支持两种模式(模式位)。
DATABASE_MODE_QUERIES(二进制值 1)DATABASE_MODE_2LINES(二进制值 2)
当搜索活动被调用来响应查询时,它负责保存这两个字符串值。搜索活动将调用清单 11-3 所示的方法来存储查询字符串。(我们将在讨论搜索活动时更详细地介绍这一点。).
清单 11-3。如何保存最近的查询
pulbic class SearchRecentSuggestions
{
...
public void saveRecentQuery (String queryString, String line2);
...
}
Note
类SearchRecentSuggestions是一个 SDK 类,当我们讨论搜索活动代码(清单 11-4)时,我们会涉及更多。
在清单 11-3 中,queryString是用户输入的字符串。该字符串显示为建议,如果用户单击建议,该字符串将被发送到搜索活动(作为新的搜索查询)。
以下是 Android 文档对line2论点的评论:
如果您已经用 DATABASE _ MODE _ 行配置了最近建议提供程序,则可以在此处传递第二行文本。它将以较小的字体显示在主要建议的下方。键入时,任何一行文本中的匹配项都将显示在列表中。如果您没有配置两行模式,或者如果给定的建议没有任何附加文本要显示,您可以在此处传递 null。
您可以在以下网址查看 Android 开发人员参考
http://developer.android.com/reference/android/provider/SearchRecentSuggestions.html
在我们的示例中,我们希望保存查询和在建议中与查询一起显示的有用文本。或者至少,我们希望在建议的底部显示有用的文本,如 SSSP(搜索简单建议供应器),以便当来自该供应器的建议出现在全局搜索中时,我们可以看到是什么应用负责搜索建议中的文本。
指定该模式以便保存建议和有用文本的方法是设置两个模式位,如SimpleSuggestionProvider的源代码所示(见清单 11-1)。如果您只是将模式位设置为保存两行,您将得到一个无效参数异常。模式位必须至少包括DATABASE_MODE_QUERIES位。本质上,你需要做一个按位或运算,这样模式在本质上是互补的,而不是排他的。
Tip
您可以通过 http://developer.android.com/reference/android/provider/SearchRecentSuggestions.html 了解更多关于这个预制建议供应器的信息。
搜索结果活动
Android search (QSB)通过查询字符串调用搜索结果活动。反过来,搜索活动需要从意图中读取这个查询字符串,执行必要的操作,并可能显示结果。因为搜索结果活动是一个活动,所以它可能被其他意图和其他动作调用。因此,检查调用它的意图动作是一个好的做法。在我们的例子中,当 Android search 调用搜索结果活动时,与调用意图一起传递的action属性是 ACTION_SEARCH。
在某些情况下,搜索结果活动可以调用自身。当这有可能发生时,您应该将搜索活动启动模式定义为一个singleTop。该活动还需要处理onNewIntent()的触发。(我们将在下一节“理解 onCreate()和 onNewIntent()”中介绍这一点。)对于查询字符串,我们只记录它。一旦查询被记录,我们需要将它保存在SearchRecentSuggestionsProvider中,这样它就可以作为未来搜索的建议。
现在让我们看看执行这些职责的搜索结果或搜索活动类的源代码。清单 11-4 显示了这个SearchActivity类的源代码。
清单 11-4。搜索活动的 Java 代码
public class SearchActivity extends Activity
{
private final static String tag ="SearchActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_search_activity);
//this.setDefaultKeyMode(Activity.DEFAULT_KEYS_SEARCH_GLOBAL);
this.setDefaultKeyMode(Activity.DEFAULT_KEYS_SEARCH_LOCAL);
// get and process search query here
final Intent queryIntent = getIntent();
final String queryAction = queryIntent.getAction();
if (Intent.ACTION_SEARCH.equals(queryAction))
{
Log.d(tag,"new intent for search");
this.doSearchQuery(queryIntent);
}
else {
Log.d(tag,"new intent NOT for search");
}
return;
}
@Override
public void onNewIntent(final Intent newIntent)
{
super.onNewIntent(newIntent);
Log.d(tag,"new intent calling me");
// get and process search query here
// Notice we are using the newIntent and not the one
// from the activity.
final Intent queryIntent = newIntent;
final String queryAction = queryIntent.getAction();
if (Intent.ACTION_SEARCH.equals(queryAction))
{
this.doSearchQuery(queryIntent);
Log.d(tag,"new intent for search");
}
else {
Log.d(tag,"new intent NOT for search");
}
}
private void doSearchQuery(final Intent queryIntent)
{
final String queryString =
queryIntent.getStringExtra(SearchManager.QUERY);
// Record the query string in the recent
// queries suggestions provider.
SearchRecentSuggestions suggestions =
new SearchRecentSuggestions(this,
SimpleSuggestionProvider.AUTHORITY,
SimpleSuggestionProvider.MODE);
String helpfullHint = "SSSP";
suggestions.saveRecentQuery(queryString, helpfullHint);
}
}
清单 11-5 快速记录了清单 11-4 中搜索结果活动的布局文件。
清单 11-5。搜索活动布局文件
<?xml version="1.0" encoding="utf-8"?>
<!-- /res/layout/layout_search_activity.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android
android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"
>
<TextView
android:id="@+id/text1"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:text="Test Search Activity view"
/>
</LinearLayout>
清单 11-6 显示了如何在清单文件中定义SearchActivity活动,并将其绑定到它的searchableinfo xml 文件。
清单 11-6。定义 SearchActivity 及其 SearchableInfo
<activity android:name=".SearchActivity"
android:label="SSSP: Search Activity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<meta-data android:name="android.app.default_searchable"
android:value=".SearchActivity" />
在第十章中,我们介绍了如何在清单文件中定义搜索活动。所有这些方面都存在于搜索活动清单文件中,如清单 11-6 所示。重申一下,我们将搜索活动定义为能够响应搜索动作。然后我们附加了一个可搜索的 XML 文件来定义搜索的性质。(我们将在本章后面的“搜索元数据”一节中详细介绍这个 XML 文件。)我们还指出,在应用级别,这(清单 11-4 的SearchActivity)是指定的搜索活动。这意味着在此应用的任何活动中进行搜索都会以此搜索活动为目标。
让我们回到清单 11-4 中搜索活动的实现源代码,看看搜索活动是如何检查动作和检索查询字符串的。
使用搜索查询字符串
搜索活动代码通过查看调用意图并将其与constant intent.ACTION_SEARCH进行比较来检查调用动作。如果动作匹配,那么它调用doSearchQuery()函数。
在doSearchQuery()函数中,搜索活动使用 intent extra 检索查询字符串。清单 11-7 显示了这段代码:
清单 11-7。如何从搜索意图中读取查询字符串
final String queryString =
queryIntent.getStringExtra(SearchManager.QUERY);
注意,额外意图被定义为SearchManager.QUERY。当您阅读本章时,您将会看到在SearchManager API 参考中定义的一些额外功能。(它的 URL 包含在本章末尾的参考资料中。)
了解 onCreate()和 onNewIntent()
当用户在搜索框中输入文本并点击建议或前进箭头时,搜索活动由 Android 启动。这导致创建搜索活动并调用其onCreate()方法。传递给这个onCreate()的意图将把动作设置为ACTION_SEARCH。
有时候,活动并没有被创建,而是通过onNewIntent()方法传递了新的搜索标准。这是怎么发生的?回调onNewIntent()与一个活动的发起方式密切相关。
当一个活动被设置为singleTop(见清单 11-6 中SearchActivity的清单文件定义)时,它指示 Android 不要创建一个新的活动,当那个活动已经在栈顶的时候。在这种情况下,Android 调用onNewIntent()而不是onCreate()。这就是为什么在清单 11-4 的SearchActivity源代码中,我们有两个地方来检查意图。注意,在onNewIntent()方法中,您应该使用新的意图来检索查询,而不是开始活动的意图。
测试 onNewIntent()
一旦你实现了onNewIntent(),你开始注意到它不会在正常的流程中被调用。这就提出了一个问题,什么时候搜索活动会在栈顶?这种情况一般不会发生。
原因如下:假设一个搜索调用活动 A 调用了一个搜索,这导致搜索活动 B 出现。然后,活动 B 显示结果,用户点击 Back 按钮返回,此时,作为用户搜索活动的活动 B 不再位于栈顶,活动 A 位于栈顶。或者用户可以点击主页键并使用主页屏幕上的全局搜索,在这种情况下,主页活动是顶部的活动。
搜索活动可以在上面的一种方式是这样的:假设由于搜索,活动 A 导致活动 B。如果活动 B 定义了搜索类型,那么当您关注活动 B 时,搜索将使用新的标准再次调用活动 B。清单 11-4 显示了我们是如何设置搜索类型来演示的。下面是代码:
this.setDefaultKeyMode(Activity.DEFAULT_KEYS_SEARCH_LOCAL);
保存搜索查询
我们已经讨论了搜索活动如何需要保存它遇到的查询,以便它们可以通过建议提供者作为建议被回放。清单 11-8 是保存这些查询的代码段:
清单 11-8。为将来的建议保存最近的搜索查询
final String queryString =
queryIntent.getStringExtra(SearchManager.QUERY);
// Record the query string in the
// recent queries suggestions provider.
SearchRecentSuggestions suggestions =
new SearchRecentSuggestions(this,
SimpleSuggestionProvider.AUTHORITY,
SimpleSuggestionProvider.MODE);
String helpfullHint = "SSSP";
suggestions.saveRecentQuery(queryString, helpfullHint);
从这段代码中,您可以看到 Android 将查询信息作为额外的(SearchManager。查询)通过意向。
一旦有了可用的查询,就可以使用 SDK 实用程序类SearchRecentSuggestions来保存查询和提示(“SSSP”),方法是实例化一个新的 suggestions 对象并要求它保存。因为我们已经选择使用两行模式和查询模式,所以saveRecentQuery的第二个参数是 SSSP(同样,这代表简单搜索建议提供者)。您将看到此帮助文本出现在该供应器的每个建议的底部。(参见本章后面的图 11-6 )。
现在,我们来看看搜索元数据定义(也称为searchableinfo),其中我们将搜索活动与搜索建议提供者联系起来。(另一方面,请参见清单文件清单 11-6,其中我们将搜索元数据与搜索活动联系在一起。)
探索搜索元数据
Android 中搜索的定义是从一个搜索活动开始的。首先在清单文件中定义一个搜索活动。作为这个定义的一部分,您告诉 Android 在哪里可以找到搜索元数据 XML 文件。参见清单 11-6,其中定义了我们的搜索活动以及搜索元数据 XML 文件的路径(searchable.xml)。清单 11-9 显示了相应的搜索元数据 XML 文件。
清单 11-9。SimpleSuggestionProvider 搜索元数据
<!-- filename: /res/xml/searchable.xml -->
<searchable
xmlns:android="http://schemas.android.com/apk/res/android
android:label="@string/search_label"
android:hint="@string/search_hint"
android:searchMode="showSearchLabelAsBadge"
android:queryAfterZeroResults="true"
android:includeInGlobalSearch="true"
android:searchSuggestAuthority=
"com.androidbook.search.simplesp.SimpleSuggestionProvider"
android:searchSuggestSelection=" ? "
/>
让我们研究一下清单 11–9 中的一些关键属性。
首先,在清单 11-9 中,属性searchSuggestAuthority指向建议提供者的权限,如该建议提供者的清单文件中所定义的那样(参见清单 11-2)。属性includeInGlobalSearch告诉 Android 使用这个建议提供者作为全局 QSB 的来源之一。
属性queryAfterZeroResults指示如果当前字母集没有返回任何结果,QSB 是否应该向建议提供者发送更多的字母。因为我们正在进行测试,所以我们不想遗漏任何细节,所以我们将该属性设置为 True,这样我们就给了提供者做出响应的所有机会。
当您从最近的搜索建议提供者派生时,属性searchSuggestSelection,总是由?表示的字符串,该字符串作为内容提供者query方法的selection字符串(where 子句)传递给建议提供者。通常,这表示任何内容提供者的 select 语句中的“where”子句。
具体到建议提供者,当有为searchSuggestSelection(作为一个协议)指定的值时,Android 将搜索查询字符串(在 QSB 中输入)作为内容提供者查询方法的 select arguments 数组中的第一个条目进行传递。
响应这些细微差别(提供者如何在内部使用这些字符串)的代码隐藏在最近的搜索建议提供者中;我们无法向您展示这些参数是如何在内容提供者的query方法中使用的。(我们将在下一章更详细地讨论这一点,在这一章中,你将看到该字符串的全貌。”?.")事实上,这个字符串不太可能用于缩小SearchRecentSuggestionsProvider的结果范围,因为它没有限定任何要查询的字段,比如“someid ==?”很可能它的存在促使 Android 将 QSB 字符串作为第一个参数传递给提供者。并且 SDK 搜索建议提供者仅仅依靠这个协议来接收由内容提供者query()方法的选择参数列表提供的便利数组中的 QSB 字符串。
现在让我们来讨论一个搜索调用者活动,我们将使用它作为这个应用的主要入口点。这个主活动允许我们测试本地搜索。
搜索调用者活动
到目前为止,我们已经开发了两个关键素材:一个搜索建议供应器和一个搜索活动。然后我们通过清单 11-6 中的searchableinfo.xml.将它们联系在一起,我们还指出SearchActivity是这个应用中搜索的目标活动。
Note
当您在检查SearchActivity上的结果时,您可以通过单击搜索关键字来利用应用级的搜索活动来调用onNewIntent()。如果您只为单个活动而不是整个应用定义默认搜索,情况就不一样了。
现在有了这些(搜索活动、建议提供者、searchableinfo xml和清单文件中的指定条目),您可以通过使用任何简单的活动作为搜索调用程序活动来测试搜索功能。在下一节中,我们将使用为本章开发的示例程序概述这种搜索体验。
简单建议供应器用户体验
示例应用包含一个非常简单的主活动,如图 11-1 所示。这个活动非常简单,不知道任何搜索上下文。
图 11-1。
A simple main activity (enabled for local search)
然而,因为我们已经将我们的搜索活动(见清单 11-6)定义为应用的全局活动,如果我们在图 11-1 中简单的主活动处于焦点时点击搜索键,我们将看到本地搜索被调用,如图 11-2 所示。
图 11-2。
Local search QSB shown on top of the main activity Note
本章中的所有数字都是使用 Android 2.3 捕获的。我们也在 4.0 中测试了该应用,我们没有发现明显的差异。我们还没有更新 4.0 的数据,但是本章的应用下载使用的是 Android 4.0。
如你所见,图 11-2 中没有任何建议,因为到目前为止我们还没有搜索任何建议。您还可以看到这是一个本地搜索—搜索的标签和提示与我们在搜索元数据 XML 文件中指定的一样。
让我们继续搜索字符串test1。这将带您进入搜索活动屏幕,如图 11-3 所示。
图 11-3。
Local search results activity
正如您在清单 11-4 的SearchActivity源代码中看到的,SearchActivity在屏幕上并没有做什么引人注目的事情,但是在幕后,它将查询字符串保存在数据库中。现在,如果您导航回主屏幕(通过按下返回按钮)并再次调用搜索,您将看到如图 11-4 所示的屏幕,其中搜索建议由之前的查询文本填充。你也可以在图 11-4 中看到建议“SSSP”的底部这在这里似乎无关紧要,因为这是一个本地搜索,并且清楚地表明它来自我们的应用。然而,当这个字符串“SSSP”作为全局搜索建议的一部分显示时,它将区分test1搜索字符串。
图 11-4。
Retrieved local suggestion
这是了解如何调用onNewIntent()的好时机。当你在进行搜索活动时(图 11-3 ,你可以键入一个类似 t 的字母,它会使用 type-to-search 再次调用搜索,你会在调试日志中看到onNewIntent()被调用。
让我们看看我们需要做些什么来让这些建议出现在全球 QSB 中。因为我们在searchable.xml中启用了includeInGlobalSearch,所以您应该也能在全球 QSB 中看到这些建议。但是,在此之前,您需要为全球 QSB 建议启用该应用,如图 11-5 所示。
图 11-5。
Enabling a Searchable application
在第十章中,我们向你展示了如何到达这个屏幕。我们编写的简单定制建议提供程序现在可以在可搜索应用列表中作为“SSSP:搜索活动”使用这个文本字符串来自于SearchActivity的活动名称(参见清单 11-6)。选择完成后,您可以看到如图 11-6 所示的全局搜索,与我们的建议供应器合作。
图 11-6。
Global suggestions from simple suggestion provider
在图 11-6 的全局搜索中,如果你键入一个类似 t 的文本,它会调出本节建议提供者的建议。但是,您可能希望用一个长词来测试它,比如 testsuperb,这样通用工具就不会有其他以 t 开头的建议,或者您必须一直向下滚动。或者,您可以筛选并明确选择该供应器进行搜索。在图 11-6 中,当您通过全局搜索导航到特定项目时,您将看到如图 11-3 所示的本地搜索活动。
参考
在您学习本章时,以下资源将会非常有用。
developer.android.com/guide/topics/search/index.htmldeveloper.android.com/guide/topics/search/searchable-config.htmlhttp://developer.android.com/reference/android/app/SearchManager.html:Android 主搜索工具的 API 引用,即SearchManager. You will see here some of the constants that you use while coding the suggestion provider.http://developer.android.com/reference/android/provider/SearchRecentSuggestions.html:允许保存最近搜索建议的类的 API 引用。http://www.androidbook.com/notes_on_search:在这个网址你可以在安卓搜索上找到作者的笔记。即使在这本书出版后,我们也会继续更新内容。您将在这里找到代码片段、摘要、用于搜索的关键 URL,以及每个版本中的变化。- 下载本章专用的测试项目:
www.androidbook.com/expertandroid/projects.ZIP 文件的名称是专家Android_ch11_SimpleSuggestionProvider.zip。
摘要
您已经了解了如何使用内置的RecentSearchSuggestionProvider来记住特定于您的应用的搜索。使用这种方法,用最少的代码,您应该能够进行本地搜索,并使它们作为建议甚至在全局上下文中可用。我们还解释了如何利用onNewIntent()回调来利用singleTop活动。
然而,这个简单的练习并没有向您展示如何从头开始编写建议提供者。更重要的是,对于建议提供者如何返回一组建议以及这个建议集中有哪些列可用,我们没有给你任何提示。为了理解这一点以及更多,我们需要从头实现一个定制的建议提供者。
那要等到下一章了。
复习问题
以下问题可以作为你在本章所学内容的里程碑:
What are suggestion providers? How do you code a very simple suggestion provider? What is SearchRecentSuggestionProvider? How do you save recent suggestions so that the SearchRecentSuggestionProvider could use them? What are the basic attributes of searchableinfo xml that are relevent to a SearchRecentSuggestionProvider? How do you test singleTop and onNewIntent()? How do you enable suggestion providers to show up in a global search?
十二、自定义搜索建议供应器
Abstract
安卓搜索太灵活,不能不定制。因为我们在上一章中使用了一个预构建的建议提供者,所以建议提供者的许多可定制特性都隐藏在SearchRecentSuggestionsProvider中,没有讨论。在本章中,我们将通过实现一个名为SuggestUrlProvider.的定制建议提供者来探究这些细节
安卓搜索太灵活,不能不定制。因为我们在上一章中使用了一个预构建的建议提供者,所以建议提供者的许多可定制特性都隐藏在SearchRecentSuggestionsProvider中,没有讨论。在本章中,我们将通过实现一个名为SuggestUrlProvider的定制建议提供者来探究这些细节。
我们从解释SuggestUrlProvider如何工作开始。我们将为您提供实现提供程序所需的所有代码片段,这些代码片段将提供如何构建定制建议提供程序的详细思路。我们还将通过在一个示例应用中展示它,向您展示如何使用这个定制建议提供程序。
规划定制建议提供者
我们将致电我们的定制建议供应器SuggestURLProvider。该提供程序的目标是监视输入到 QSB 中的内容。如果搜索查询具有类似“great . m”(后缀)的文本。m”被选择来表示含义),我们的定制建议提供者将把查询的第一部分解释为一个单词,并建议一个基于互联网的 URL,可以调用该 URL 来查找该单词的含义。
对于使用这种模式识别的每个单词,SuggestURLProvider提供了两个 URL。第一个 URL 允许用户使用 http://www.thefreedictionary.com 搜索单词,第二个 URL 使用 http://www.google.com 。选择这些建议中的一个会将用户直接带到这些站点中的一个。
实现自定义 SuggestUrlProvider
你已经在第十一章中看到,从本质上来说,建议提供者就是内容提供者。因此,实现定制建议提供者与实现内容提供者是一样的。我们将介绍实现内容提供者的步骤,并向您展示如何以不同的方式使其成为定制的建议提供者。如果你对内容供应器有一个大致的了解,这肯定会有所帮助,这样你就可以很快找到相似之处。但是,即使你没有这方面的知识,你也应该能够理解这里提出的论点和代码——在这个过程中,你也可以了解内容供应器!
实现定制建议提供者包括实现由内容提供者定义的虚拟方法,尤其是query()方法,因为我们扩展了基本提供者类。当您扩展基本的ContentProvider类方法时,以下细节很重要:
Understand the URIs honored by your suggestion provider. Understand how to implement the getType() method that is expected to return MIME types for the search results. This involves recognizing the incoming URIs from item 1 above through the URIMatcher class. Then based on the URI type use the SearchManager defined MIME types for identifying the MIME type of the search results. Understand how to implement the query() method that is expected to return the search results. This involves (a) recognizing the incoming URIs through the URIMatcher class, (b) understanding how to retrieve and use the query search text that was entered, and (c) returning a collection of rows with the required columns as defined by the SearchManager. Understand how to construct the necessary columns required by the SearchManager into a cursor as defined by the suggestion provider’s suggestion cursor contract.
我们现在按照与上面大致相同的顺序介绍这些细节,并在需要的地方提供源代码片段。最后,我们展示了计划中的SuggestUrlProvider的完整源代码。
了解建议提供者 URIs
内容供应器的核心是它的一组 URIs,用于读取和更新内容供应器。从这个意义上说,内容供应器是拥有内容或数据的网站。URI 是读取或更新内容的一种方式。
Android 搜索使用两种 URIs 来调用建议提供者。第一个叫做搜索 URI。这个 URI 被建议提供者用来提供建议集。对此搜索 URI 的响应需要一个或多个搜索建议行,每行包含一组包含搜索建议数据的众所周知的列。这个搜索 URI 可以采用清单 12-1 所示的两种形式中的任何一种。
清单 12-1。建议供应器搜索 URIs 的结构
content://com.androidbook.search.suggesturlprovider/search_suggest_query
content://com.androidbook.search.suggesturlprovider/search_suggest_query/<your-query>
在这个搜索 URI 中,第一部分,如清单 12-2 所示,是内容提供者权威,这是我们的建议提供者所独有的。
清单 12-2。我们客户建议提供者的基本权威
com.androidbook.search.custom.suggesturlprovider
第二个 URI 被称为建议捷径 URI。该 URI 用于更新先前缓存的建议。这些缓存的建议被称为 Android 建议快捷方式。对这种类型的 URI 的响应需要是包含相同的一组众所周知的搜索建议列的单行。这个建议的快捷方式 URI 可以采用清单 12-3 所示的两种形式中的任何一种。
清单 12-3。建议快捷方式 URIs 的结构
content://com.androidbook.search.suggesturlprovider/search_suggest_shortcut
content://com.androidbook.search.suggesturlprovider/search_suggest_shortcut/<shortcut-id>
这个建议快捷方式 URI 是由 Android 发出的,当它试图确定它缓存的快捷方式是否仍然有效时。如果提供程序返回单行,它将用新的快捷方式替换当前快捷方式。如果供应器发送一个 null,那么 Android 认为这个建议不再有效。
Android 中的SearchManager类定义了两个常数来表示这些区分它们的 URI 段(search_suggest_search和search_suggest_shortcut)。这些常量如清单 12-4 所示。
清单 12-4。SearchManager 建议 URI 定义
SearchManager.SUGGEST_URI_PATH_QUERY
SearchManager.SUGGEST_URI_PATH_SHORTCUT
提供者有责任在其query()方法中识别这些传入的 URIs。为了将传入的 URI 字符串与上面的常量之一进行比较,Android 提供了一个名为UriMatcher的实用程序类。清单 12-5 显示了如何初始化UriMatcher,以便它可以在以后用于识别输入的搜索 URI 结构。
清单 12-5。用 SearchManager 构建一个 UriMatcher 建议 URI 定义
private static UriMatcher buildUriMatcher(String AUTHORITY)
{
UriMatcher matcher =
new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI(AUTHORITY,
SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST);
matcher.addURI(AUTHORITY,
SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT,
SHORTCUT_REFRESH);
matcher.addURI(AUTHORITY,
SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
SHORTCUT_REFRESH);
return matcher;
}
在清单 12-5 中,我们创建了一个UriMatcher对象,它知道如何识别一个输入的字符串,如果它看起来像我们的建议提供者的搜索 URIs。要做到这一点,UriMatcher需要知道建议提供者的权限和遵循该权限的 URI 路径字符串模式,就像 web URI 和相应的路径段一样。一旦我们有了一个用权限和预期的 URIs 结构初始化的UriMatcher对象,我们就可以使用它,如清单 12-6 所示,根据权限来区分内容供应器 URIs。
清单 12-6。使用 UriMatcher 的 query()方法概述
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
{
...other stuff
switch (sURIMatcher.match(uri))
{
case SEARCH_SUGGEST:
//Return a series of suggestions
case SHORTCUT_REFRESH:
//Return the updated suggestion
}//eof-switch
....other stuff
}
在清单 12-6 中,query()方法是所有内容提供者都需要的签名。当针对该内容提供者发布任何 URI 时,将调用此方法。(稍后我们将介绍这个query()方法是如何完全实现的。)
实现 getType()来指定 MIME 类型
因为建议提供者最终是内容提供者,所以它有责任实现内容提供者契约,这包括为getType()方法定义一个实现,以返回相应 URIs 的 MIME 类型。清单 12-7 展示了getType()的实现。
清单 12-7。为建议的 UrlProvider 实现 getType()
....other stuff
//Initialize the declared object below first using code in listing 12-5
private static URIMatcher sURIMatcher;
....other stuff
public String getType(Uri uri) {
switch (sURIMatcher.match(uri)) {
case SEARCH_SUGGEST:
return SearchManager.SUGGEST_MIME_TYPE;
case SHORTCUT_REFRESH:
return SearchManager.SHORTCUT_MIME_TYPE;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
}
注意在清单 12-7 中我们是如何使用UriMatcher类的。通过它的SearchManager类,Android 搜索框架提供了一些常量来帮助这些 MIME 类型。MIME 类型常量如清单 12-8 所示。
清单 12-8。SearchManager MIME 类型常量
SearchManager.SUGGEST_MIME_TYPE
SearchManager.SHORTCUT_MIME_TYPE
清单 12-8 中的常量翻译成清单 12-9 中的字符串。
清单 12-9。SearchManager MIME 类型常数值
vnd.android.cursor.dir/vnd.android.search.suggest
vnd.android.cursor.item/vnd.android.search.suggest
MIME 类型对内容供应器很重要,就像它们对 web URLs 一样重要。它们指示 URI 返回的数据类型,以便调用者知道返回的是什么类型的数据。虽然我们没有直接使用清单 12-9 中给出的字符串值,但是它们有助于理解 MIME 类型在 Android 中是如何映射的,也有助于在查看日志文件时进行调试。
实现内容供应器查询方法
当它使用一个搜索 URIs 调用建议提供者时,Android 最终调用建议提供者(作为内容提供者)的query()方法来接收建议光标。清单 12-10 显示了我们的SuggestUrlProvider的query()方法的概要实现。
清单 12-10。为 SuggestUrlProvider 实现 query()方法
public Cursor query(Uri uri, String[] projection,
String selection,
String[] selectionArgs, String sortOrder)
{
Log.d(tag,"query called with uri:" + uri);
Log.d(tag,"selection:" + selection);
String query = selectionArgs[0];
Log.d(tag,"query:" + query);
switch (sURIMatcher.match(uri)) {
case SEARCH_SUGGEST:
Log.d(tag,"search suggest called");
return getSuggestions(query);
case SHORTCUT_REFRESH:
Log.d(tag,"shortcut refresh called");
return null;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
}
让我们看看清单 12-10 所示的query()方法中的参数。“uri”参数是我们已经讨论过的搜索 URIs 之一:它有一个类型Uri。类Uri只是 URI 字符串值的一层薄薄的包装。“projection”参数携带调用者有兴趣从内容提供者(比如我们的建议提供者)检索的列的列表。对于建议提供者,我们可以忽略这个参数,因为建议提供者所期望的列列表是固定的和已知的。
“selection”参数是一个字符串,表示一个带问号(?)在里面。问号应该被替换为selectionArgs数组中的值。在建议提供者的情况下,选择参数通常是“?”而selectionArgs数组包含一个携带输入到 QSB 中的查询字符串的元素。
尽管选择论的论点是?您可以修改它,因为searchableinfo XML 文件为它做了准备。清单 12-11 显示了如何通过searchableinfo XML 文件配置我们的定制建议提供者。
清单 12-11。CustomSuggestionProvider searchable info 元数据 XML 文件
//xml/searchable.xml
<searchable
xmlns:android="http://schemas.android.com/apk/res/android
android:label="@string/search_label"
android:hint="@string/search_hint"
android:searchMode="showSearchLabelAsBadge"
android:searchSettingsDescription="suggests urls"
android:includeInGlobalSearch="true"
android:queryAfterZeroResults="true"
android:searchSuggestAuthority=
"com.androidbook.search.custom.suggesturlprovider"
android:searchSuggestIntentAction=
"android.intent.action.VIEW"
android:searchSuggestSelection=" ? "
/>
Note
请注意searchSuggestAuthority字符串值。它应该与 Android 清单文件中相应的内容提供者 URL 定义相匹配。
注意清单 12-11 所示的搜索元数据定义文件中的searchSuggestSelection属性。它直接对应于清单 12-10 中内容提供者的query()方法的选择参数。
当您在searchableinfo XML 文件中指定searchSuggestSelection时,Android 假设您不想通过 URI 接收搜索文本,而是希望通过query()方法的选择参数接收文本。在这种情况下,Android search 将发送“?”(注意问号前后的空格)作为选择参数的值,并将查询文本作为选择参数(selectionArgs,在清单 12-10 中)数组的第一个元素。如果不指定searchSuggestSelection,那么它将把搜索文本作为 URI 的最后一个路径段传递。你可以选择其中之一。在我们的例子中,我们选择了选择方法,而不是 URI 方法。
现在,如果你注意到清单 12-10 中的query()方法的主体,你会看到我们首先决定哪种 URI 调用了query()方法。和以前一样,我们使用UriMatcher类来了解这一点。如果 URI 是一个建议 URI,那么我们调用getSuggestions()来返回一个光标。如果是快捷方式 URI,我们只需返回 null 来表示建议已经过期。当然,如果您想根据需要返回什么来专门化这个行为,您可以改变这个逻辑。
浏览建议光标列
在实现清单 12-10 中的query()方法时,我们使用了一个方法getSuggestions()来返回一个游标值的列。所以这个getSuggestions()方法需要返回一组具有明确定义名称的列。由getSuggestions()方法返回的光标称为建议光标。
建议光标毕竟是一个光标。它与 Android 定义的数据库游标没有什么不同。建议光标充当 Android 搜索工具和建议提供者之间的契约。这意味着游标返回的列的名称和类型是固定的,双方都知道。为了提供搜索的灵活性,Android 搜索在这个光标中提供了大量的列。这些列中的许多(如果不是大多数的话)是可选的。建议提供者不需要返回所有这些列;它可以忽略发送与建议提供者不相关的列。让我们看看建议提供者可以返回的列,每一列的含义,以及它如何影响搜索。
像所有游标一样,建议游标必须有一个_id列。这是强制性的。每隔一列以前缀SUGGEST_COLUMN_开始。这些常量被定义为SearchManager API 参考的一部分。这里,我们回顾一下最常用的色谱柱。(要获得完整的列表,请使用本章末尾参考资料中的 API 源代码。)在下面的列描述中,搜索活动一词指的是被调用来显示搜索文本的结果的活动。
text_1:显示的建议中的第一行文本(参见本章后面的图 12-1 中显示的基于键入内容的搜索建议列表)。text_2:提出的建议中的第二行文本(见本章后面的图 12-1 )。icon_1:建议左侧的图标,通常是资源 ID。icon_2:建议右侧的图标,通常是资源 ID。intent_action:当SearchActivity作为意图动作被调用时,传递给它什么。这将覆盖搜索元数据中可用的相应意图动作(参见清单 12-11)。intent_data:当SearchActivity作为意图数据被调用时,传递给它的是什么。这将覆盖搜索元数据中可用的相应意图动作(参见清单 12-11)。这是一个数据 URI。intent_data_id:追加到数据 URI。如果您想在元数据中一次性提到数据的根部分,然后对每个建议进行更改,这将非常有用。那样会更有效率一点。query:发送给搜索活动的查询字符串。shortcut_id:如前所述,Android search 缓存由建议供应器提供的建议。这些缓存的建议被称为快捷方式。如果这个栏目不存在,Android 会缓存建议,永远不会要求更新。如果这包含一个相当于SUGGEST_NEVER_MAKE_SHORTCUT的值,那么 Android 不会缓存这个建议。如果它包含任何其他值,此 ID 将作为快捷方式 URI 的最后一个路径段传递。(参见“了解建议提供者 URIs”一节)- 这个布尔值告诉 Android 当它在更新快捷键的过程中是否应该使用一个微调器。
有一组可变的附加列用于响应动作键。我们将在后面关于动作键的章节中讨论这个问题。现在,让我们看看我们的定制建议提供者如何选择返回哪些列,以及它们是如何返回的。
填充并返回建议光标
不要求每个定制建议提供者返回所有这些列。对于我们的建议提供者,我们将根据本章开始的“规划定制建议提供者”一节中指出的功能,只返回列的子集。
方法getSuggestions()依赖于我们定义的列名数组,如清单 12-12 所示。
清单 12-12。为 SuggestUrlProvider 定义建议光标列
private static final String[] COLUMNS = {
"_id", // must include this column
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_TEXT_2,
SearchManager.SUGGEST_COLUMN_INTENT_DATA,
SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
SearchManager.SUGGEST_COLUMN_SHORTCUT_ID
};
如您所见,列名不是硬编码的,而是取自于SearchManager API 中可用的列名定义。选择这些列是为了满足以下功能:
首先,用户输入一个带有提示的单词,比如 QSB 中的“great.m”。我们的建议提供者将不会回应,直到有一个。搜索文本中的“m”。一旦识别出来,建议提供者就从中提取单词(在本例中是“great”),然后提供两个建议。
接下来,第一个建议是用这个单词调用 thefreewebdictionary.com ,第二个建议是用模式define:great搜索 Google。为此,提供者加载列intent_action作为intent.action.view(由常量intent.ACTION_VIEW定义)和包含整个 URI 的意图数据。希望 Android 在看到以http://开头的数据 URI 时会启动浏览器。
然后,我们用search some-website with:填充 text 1 列,用单词本身填充 text 2 列(同样,在本例中是“棒极了”)。为了简化,我们还将快捷方式 ID 设置为SUGGEST_NEVER_MAKE_SHORTCUT。此设置禁用缓存,并防止触发建议快捷方式 URI。
一旦我们在类似清单 12-12 中的COLUMNS的数组中确定了这些列,我们就可以定义一个游标,如清单 12-13 所示。
清单 12-13。使用矩阵光标
MatrixCursor cursor = new MatrixCursor(COLUMNS);
String[] rowData;
//insert values for each column in rowData
cursor.addRow(rowData);
类MatrixCursor来自 Android API。一旦我们有了这个光标对象,我们就可以通过调用MatrixCursor对象上的addRow()来添加每个建议行。
SuggestUrlProvider 的全部源代码
我们已经介绍了所有必要的背景知识,现在向您展示我们定制的SuggestUrlProvider的完整代码。清单 12-14 显示了SuggestUrlProvider class.的完整源代码
清单 12-14。自定义建议提供程序完整源代码
public class SuggestUrlProvider extends ContentProvider
{
private static final String tag = "SuggestUrlProvider";
public static String AUTHORITY =
"com.androidbook.search.custom.suggesturlprovider";
private static final int SEARCH_SUGGEST = 0;
private static final int SHORTCUT_REFRESH = 1;
private static final UriMatcher sURIMatcher = buildUriMatcher();
private static final String[] COLUMNS = {
"_id", // must include this column
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_TEXT_2,
SearchManager.SUGGEST_COLUMN_INTENT_DATA,
SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
SearchManager.SUGGEST_COLUMN_SHORTCUT_ID
};
private static UriMatcher buildUriMatcher()
{
UriMatcher matcher =
new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI(AUTHORITY,
SearchManager.SUGGEST_URI_PATH_QUERY,
SEARCH_SUGGEST);
matcher.addURI(AUTHORITY,
SearchManager.SUGGEST_URI_PATH_QUERY +
"/*",
SEARCH_SUGGEST);
matcher.addURI(AUTHORITY,
SearchManager.SUGGEST_URI_PATH_SHORTCUT,
SHORTCUT_REFRESH);
matcher.addURI(AUTHORITY,
SearchManager.SUGGEST_URI_PATH_SHORTCUT +
"/*",
SHORTCUT_REFRESH);
return matcher;
}
@Override
public boolean onCreate() {
//lets not do anything in particular
Log.d(tag,"onCreate called");
return true;
}
@Override
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs,
String sortOrder)
{
Log.d(tag,"query called with uri:" + uri);
Log.d(tag,"selection:" + selection);
String query = selectionArgs[0];
Log.d(tag,"query:" + query);
switch (sURIMatcher.match(uri)) {
case SEARCH_SUGGEST:
Log.d(tag,"search suggest called");
return getSuggestions(query);
case SHORTCUT_REFRESH:
Log.d(tag,"shortcut refresh called");
return null;
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
}
private Cursor getSuggestions(String query)
{
if (query == null) return null;
String word = getWord(query);
if (word == null)
return null;
Log.d(tag,"query is longer than 3 letters");
MatrixCursor cursor = new MatrixCursor(COLUMNS);
cursor.addRow(createRow1(word));
cursor.addRow(createRow2(word));
return cursor;
}
private Object[] createRow1(String query)
{
return columnValuesOfQuery(query,
"android.intent.action.VIEW",
" http://www.thefreedictionary.com/ " + query,
"Look up in freedictionary.com for",
query);
}
private Object[] createRow2(String query)
{
return columnValuesOfQuery(query,
"android.intent.action.VIEW",
"http://www.google.com/search?hl=en&source=hp&q=define%3A/
+ query,
"Look up in google.com for",
query);
}
private Object[] columnValuesOfQuery(String query,
String intentAction,
String url,
String text1,
String text2)
{
return new String[] {
query, // _id
text1, // text1
text2, // text2
url,
// intent_data (included when clicking on item)
intentAction, //action
SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT
};
}
private Cursor refreshShortcut(String shortcutId,
String[] projection) {
return null;
}
public String getType(Uri uri) {
switch (sURIMatcher.match(uri)) {
case SEARCH_SUGGEST:
return SearchManager.SUGGEST_MIME_TYPE;
case SHORTCUT_REFRESH:
return SearchManager.SHORTCUT_MIME_TYPE;
default:
throw
new IllegalArgumentException("Unknown URL " + uri);
}
}
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException();
}
public int delete(Uri uri, String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException();
}
public int update(Uri uri, ContentValues values,
String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException();
}
private String getWord(String query)
{
int dotIndex = query.indexOf('.');
if (dotIndex < 0)
return null;
return query.substring(0,dotIndex);
}
}
总之,清单 12-14 汇集了所有的代码片段。在这里,您将看到我们是如何满足基类ContentProvider强加的完整契约的。
探索搜索元数据
在关于 Android 搜索的前两章中,我们已经介绍了搜索元数据 XML 文件的一些细节。我们还在清单 12-11 中介绍了这个搜索元数据 XML 文件的内容,在清单 12-11 中,我们展示了搜索查询是如何发送给我们的定制建议提供者的。我们现在可以介绍这个searchableinfo XML 文件的一些常用属性。有关该文件属性的完整列表,请参考 Google 的搜索配置文档,可从以下网址获得:
http://developer.android.com/guide/topics/search/searchable-config.html
为了帮助讨论这些额外的属性,让我们在这里复制清单 12-11,如清单 12-15,以便有一个快速的参考。
清单 12-15。Searchableinfo XML 文件
//xml/searchable.xml
<searchable
xmlns:android="http://schemas.android.com/apk/res/android
android:label="@string/search_label"
android:hint="@string/search_hint"
android:searchMode="showSearchLabelAsBadge"
android:searchSettingsDescription="suggests urls"
android:includeInGlobalSearch="true"
android:queryAfterZeroResults="true"
android:searchSuggestAuthority=
"com.androidbook.search.custom.suggesturlprovider"
android:searchSuggestIntentAction=
"android.intent.action.VIEW"
android:searchSuggestSelection=" ? "
/>
了解 SearchSuggestAction 属性
在清单 12-15 中,searchSuggestIntentAction属性用于在调用SearchActivity时传递或指定意图动作。这允许SearchActivity做一些默认搜索之外的事情。清单 12-16 显示了如何在响应搜索活动的onCreate()方法中使用意图动作:
清单 12-16。响应操作查看和操作搜索
//Body of onCreate
// get and process search query here
final Intent queryIntent = getIntent();
//query action
final String queryAction = queryIntent.getAction();
if (Intent.ACTION_SEARCH.equals(queryAction))
{
this.doSearchQuery(queryIntent);
}
else if (Intent.ACTION_VIEW.equals(queryAction))
{
this.doView(queryIntent);
}
else {
Log.d(tag,"Create intent NOT from search");
}
您将看到清单 12-16 中的代码在上下文中使用,因为清单 12-17 显示了SearchActivity如何通过检查意图的动作值来寻找查看动作或搜索动作。
了解 searchSuggestIntentData 属性
就像意图动作一样,您可以使用searchSuggestIntentData属性指定意图数据。这是一个数据 URI,在被调用时,它可以作为意图的一部分,沿着操作传递给搜索活动。
了解 searchSuggestPath 属性
我们在这里没有使用的另一个属性,但是建议提供者可以使用,叫做searchSuggestPath。如果指定的话,这个字符串值被附加到搜索建议 URI(调用建议提供者的那个)。它允许单个定制建议提供者针对两个不同的搜索活动提供建议。每个搜索活动使用相同的建议提供者权限,但是使用不同的searchSuggestPath。建议提供者可以使用这个路径后缀为每个目标搜索活动返回一组不同的建议。
了解 searchSuggestThreshold 属性
名为searchSuggestThreshold的属性表示在调用这个建议提供者之前必须输入到 QSB 中的字符数。默认阈值为零。
了解 queryAfterZeroResults 属性
属性queryAfterZeroResults(真或假)指示随着更多字符被键入,如果当前字符集返回零个结果集,是否应该联系提供者。在我们特定的SuggestUrlProvider中,打开这个标志很重要,这样我们每次都能看到整个查询文本。
实施搜索活动
既然我们已经对SuggestUrlProvider进行了完全编码,我们需要一个搜索活动来响应这个提供者提出的建议。在第十一章中讨论的简单建议提供者实现中,我们只讨论了搜索活动的部分职责。现在让我们看看我们忽略的方面。
Android search 调用一个搜索活动,以响应以两种方式之一出现的搜索动作。当从 QSB 点击一个搜索图标或者当用户直接点击一个建议时,就会发生这种情况。当被调用时,搜索活动需要检查它为什么被调用。此信息在意图操作中可用。也就是说,搜索活动检查意图动作以便做正确的事情。很多情况下,这个动作就是ACTION_SEARCH。但是,建议提供者可以选择通过搜索元数据 XML 文件或建议游标列指定操作来覆盖它。这种类型的行动可以是任何事情。在我们的例子中,我们也将使用一个视图动作。
正如我们在第十一章的中对简单建议提供者的讨论中所指出的,也可以将搜索活动的启动模式设置为singleTop。在这种情况下,搜索活动增加了响应onNewIntent()和onCreate()的责任。让我们看看这两个案例,看看它们有多么相似。
我们使用onNewIntent()和onCreate()来检查ACTION_SEARCH和ACTION_VIEW。在搜索动作ACTION_SEARCH的情况下,我们简单地向用户显示查询文本。(请看图 12-2 看看这段文字是什么样子的。).在查看动作的情况下,我们将控制转移到浏览器,并在当前活动上调用finish()方法,这样用户就有了通过直接点击建议来调用浏览器的印象。
Note
这个search activity不需要成为 Android 主应用菜单中的可启动活动。确保您不会像设置需要从设备的主应用屏幕调用的其他活动一样,无意中为此活动设置意图过滤器。
说完,我们来检查一下SearchActivity.java的源代码。
SearchActivity 源代码
既然我们已经概述了搜索活动的职责,我们可以看看这个搜索活动的源代码,如清单 12-17 所示。
清单 12-17。SearchActivity 的完整源代码
//file: SearchActivity.java
public class SearchActivity extends Activity
{
private final static String tag ="SearchActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(tag,"I am being created");
//See the downloadable project if you want the following layout file
//Or copy it from Listing 12-19.
setContentView(R.layout.layout_test_search_activity);
// get and process search query here
final Intent queryIntent = getIntent();
//query action
final String queryAction = queryIntent.getAction();
Log.d(tag,"Create Intent action:"+queryAction);
final String queryString =
queryIntent.getStringExtra(SearchManager.QUERY);
Log.d(tag,"Create Intent query:"+queryString);
if (Intent.ACTION_SEARCH.equals(queryAction))
{
this.doSearchQuery(queryIntent);
}
else if (Intent.ACTION_VIEW.equals(queryAction))
{
this.doView(queryIntent);
}
else {
Log.d(tag,"Create intent NOT from search");
}
return;
}
@Override
public void onNewIntent(final Intent newIntent)
{
super.onNewIntent(newIntent);
Log.d(tag,"new intent calling me");
// get and process search query here
final Intent queryIntent = newIntent;
//query action
final String queryAction = queryIntent.getAction();
Log.d(tag,"New Intent action:"+queryAction);
final String queryString =
queryIntent.getStringExtra(SearchManager.QUERY);
Log.d(tag,"New Intent query:"+queryString);
if (Intent.ACTION_SEARCH.equals(queryAction))
{
this.doSearchQuery(queryIntent);
}
else if (Intent.ACTION_VIEW.equals(queryAction))
{
this.doView(queryIntent);
}
else {
Log.d(tag,"New intent NOT from search");
}
return;
}
private void doSearchQuery(final Intent queryIntent)
{
final String queryString =
queryIntent.getStringExtra(SearchManager.QUERY);
appendText("You are searching for:" + queryString);
}
private void appendText(String msg)
{
TextView tv = (TextView)this.findViewById(R.id.text1);
tv.setText(tv.getText() + "\n" + msg);
}
private void doView(final Intent queryIntent)
{
Uri uri = queryIntent.getData();
String action = queryIntent.getAction();
Intent i = new Intent(action);
i.setData(uri);
startActivity(i);
this.finish();
}
}
我们从分析这个源代码(清单 12-17)开始,首先检查这个搜索活动是如何被调用的。
搜索活动调用的详细信息
像所有活动一样,我们知道搜索活动必须是通过意图调用的。然而,假设总是意图的行动对此负责是错误的。事实证明,搜索活动是通过其组件名称规范显式调用的。
你可能会问为什么这很重要。我们知道,在我们的建议提供者中,我们在建议行中明确地指定了一个意图动作。如果这个意图动作是 view,而意图数据是一个http URL,那么一个不知情的程序员会认为将会启动一个浏览器作为响应,而不是搜索活动。这当然是可取的。但是因为除了意图动作和数据之外,最终意图还加载了组件名SearchActivity,所以组件名将优先。
我们不知道为什么会有这种限制,也不知道如何克服这种限制。但事实是,不管您的建议提供者在建议中指定了什么意图动作,SearchActivity都是将要被调用的那个。在我们的例子中,我们只需从搜索活动启动浏览器并关闭搜索活动。
为了证明这一点,下面是当我们点击一个建议时,Android 启动调用我们的搜索活动的意图:
launching Intent {
act=android.intent.action.VIEW
flg=0x10000000
cmp=com.androidbook.search.custom/.SearchActivity (has extras)
}
请注意意图的组件规格。它直接指向搜索活动。所以无论你指示什么意图动作,Android 都会一直调用SearchActivity。因此,调用浏览器就成了搜索活动的责任。现在,让我们看看在搜索活动中我们是如何处理这些意图的。
响应操作 _ 搜索和操作 _ 查看
我们知道 Android search 通过名称显式调用搜索活动。然而,调用意图还带有指定的动作。当 QSB 通过搜索图标调用这个活动时,这个动作就是ACTION_SEARCH。如果搜索建议调用该操作,则该操作可能会有所不同。这取决于建议提供者如何设置建议。在我们的例子中,建议提供者将其设置为ACTION_VIEW。
因此,搜索活动需要检查动作的类型。清单 12-18 显示了我们如何检查这种类型的动作,看看是调用搜索查询方法还是视图方法。(这段代码摘自清单 12-17。)
清单 12-18。响应操作 _ 搜索和操作 _ 查看
if (Intent.ACTION_SEARCH.equals(queryAction))
{
this.doSearchQuery(queryIntent);
}
else if (Intent.ACTION_VIEW.equals(queryAction))
{
this.doView(queryIntent);
}
从清单 12-18 中的代码,你可以看到我们调用了查看动作的doView()和搜索动作的doSearchQuery()。
在doView()函数中,我们检索动作和数据 URI,并用它们填充新的意图,然后调用活动。这将调用浏览器。我们还在活动上调用了方法finish(),这样 Back 按钮就可以带您回到搜索调用它的地方。
在doSearchQuery()中,我们只是将搜索查询文本记录到视图中。让我们来看看用来支持doSearchQuery()的布局。
搜索活动布局
清单 12-19 是一个简单的布局,在doSearchQuery()的情况下被一个搜索活动使用。
清单 12-19。搜索活动布局 XML
<?xml version="1.0" encoding="utf-8"?>
<!-- file: layout/layout_test_search_activity.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:id="@+id/text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/search_activity_main_text"
/>
</LinearLayout>
在这一点上,提到负责这个应用的一些文本需求的strings.xml是合适的,如清单 12-19 所示。(参见android:text属性。)你会在图 12-2 ( SearchActivity视图)中看到我们用于清单 12-9 的字符串。您还可以在参考资料部分找到的相应的可下载应用中看到本章使用的所有文件。在这里列出长字符串值并没有什么好处。
正在响应 onCreate()和 onNewIntent()
在清单 12-17 中,你会看到onCreate()和onNewIntent()中的代码几乎相同。这种模式并不少见。调用搜索活动时,根据搜索活动的启动模式,调用onCreate()或onNewIntent()。这些方法在第十一章中有详细讨论。
完成搜索活动的注意事项
在本次讨论的早些时候,我们简要提到了如何回应doView()。清单 12-20 显示了这个doView()函数的代码摘录(摘自清单 12-17)。
清单 12-20。完成搜索活动
private void doView(final Intent queryIntent)
{
Uri uri = queryIntent.getData();
String action = queryIntent.getAction();
Intent i = new Intent(action);
i.setData(uri);
startActivity(i);
this.finish();
}
这个函数的目标是调用浏览器。如果我们没有在最后做finish()操作,用户在点击 back 按钮后会从浏览器返回到搜索活动,而不是像预期的那样回到他们来的搜索屏幕。理想情况下,为了提供最佳的用户体验,控件不应该通过搜索活动。完成这个活动就解决了这个问题。清单 12-20 还提供了一个机会来检查我们如何从原始意图(由建议提供者设置)中转移意图动作和意图数据,然后将它们传递给新的浏览器意图。
到目前为止,我们已经有了一个建议提供者、一个搜索活动和一个searchableinfo XML 文件。现在,我们将介绍这个应用的清单文件。
自定义建议提供程序清单文件
清单文件是您将应用的许多组件放在一起的地方。对于我们的定制建议提供者应用,与其他示例一样,这是您声明其组件的地方,比如搜索活动和建议提供者。您还可以使用 manifest 文件通过将Search Activity声明为默认搜索来声明该应用支持本地搜索。另外,请注意为搜索活动定义的意图过滤器。清单文件代码中用粗体突出显示了这些细节,如清单 12-21 所示。
清单 12-21。自定义建议提供程序清单文件
//file:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android
package="com.androidbook.search.custom"
android:versionCode="1"
android:versionName="1.0.0">
<application android:icon="@drawable/icon"
android:label="Custom Suggestions Provider">
<!--
****************************************************************
* Search related code: search activity
****************************************************************
-->
<activity android:name=".SearchActivity"
android:label="Search Activity Label"
android:launchMode="singleTop">
<intent-filter>
<action
android:name="android.intent.action.SEARCH" />
<category
android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<!--``Declare default search
<meta-data android:name="android.app.default_searchable"
android:value=".SearchActivity" />
<!--``Declare Suggestion Provider
<provider android:name="SuggestUrlProvider"
android:authorities=
"com.androidbook.search.custom.suggesturlprovider" />
</application>
<uses-sdk android:minSdkVersion="4" />
</manifest>
如你所见,我们在清单 12-21 中强调了三件事:
- 定义搜索活动及其搜索元数据 XML 文件
- 将
SearchActivity定义为应用的默认搜索 - 定义建议提供者及其权限
有了所有的源代码之后,是时候浏览一下应用,看看它在模拟器中是什么样子了。
自定义建议供应器用户体验
在这一节中,我们将向您展示如何使用我们开发的定制建议提供程序。如果您想在您的模拟器或设备上看到这个,您可以从本章末尾的参考资料中的 URL 下载本章的 Android Eclipse 项目。本节将快速介绍这一体验。
一旦您通过 ADT 构建并部署了下载的应用,您将不会看到任何弹出的活动,因为在这个应用中没有要启动的活动。自定义建议提供程序只是一个幕后引擎。因此,您将看到应用成功安装在 Eclipse 控制台中,而不是由安装程序启动任何活动。当然,该应用包含在模拟器或设备上的应用列表中。
成功安装意味着建议提供者准备好响应全局 QSB。但是在这之前,您需要使这个建议提供者能够参与全局搜索。我们已经在第十章和第十一章中展示了如何启用自定义搜索应用。
要开始此自定义搜索供应器的用户体验之旅,请打开全球 QSB,然后在 QSB 中输入“chiaroscur.m”。
图 12-1。
Suggestions from the custom SuggestUrlProvider
请注意图 12-1 中自定义建议提供者的搜索建议是如何呈现的。如果有太多来自其他搜索应用的建议,我们的建议可能在它们下面,在视图之外,你可能需要滚动。或者,您可以单击左上角的搜索图标,将搜索应用更改为“custom suggestion provider”应用。这将把搜索建议过滤到我们的建议列表中。
现在,如图 12-1 所示,导航到我们的定制建议供应器提供的建议之一,并单击 QSB 搜索图标。Android 会直接带你到搜索活动,不需要调用任何浏览器,如图 12-2 所示。
图 12-2。
SearchActivity responding to ACTION_SEARCH
这个例子演示了与ACTION_VIEW相对的ACTION_SEARCH。现在相反,如果你触摸图 12-1 中的免费字典建议,我们的搜索活动得到ACTION_VIEW,搜索活动调用浏览器,如图 12-3 所示。这演示了所讨论的两种意图动作:搜索活动和视图。
图 12-3。
SearchActivity transferring to free dictionary
同样,在图 12-1 中,如果你触摸谷歌建议项,你会看到如图 12-4 所示的浏览器变化。
图 12-4。
SearchActivity transferring to Google for the word definition
图 12-5 显示了如果不输入后缀会发生什么。m”进入全局搜索。
图 12-5。
Custom provider with no contributed suggestions
注意建议提供者没有提供任何反馈。
从头开始构建功能性定制建议提供程序的讨论到此结束。虽然我们已经讨论了搜索的许多方面,但是还有一些主题没有讨论。这些是动作键和特定于应用的搜索数据。
使用操作键和特定于应用的搜索数据
动作键和特定于应用的搜索数据增加了 Android 搜索的灵活性。操作键允许您使用专门的设备键来实现搜索相关的功能。特定于应用的搜索数据允许活动将附加数据传递给搜索活动。让我们从动作键开始。
在 Android 搜索中使用操作键
到目前为止,我们已经展示了许多调用搜索的方法:
- QSB 中可用的搜索图标
- 作为一组操作键的一部分的搜索键
- 由活动显示的显式图标或按钮
- 基于键入搜索声明的任何按键
在这一节中,我们将通过使用操作键来调用搜索。动作键是设备上与特定动作相关联的一组可用键。清单 12-22 显示了一些动作键的例子。
清单 12-22。动作键码列表
keycode_dpad_up
keycode_dpad_down
keycode_dpad_left
keycode_dpad_right
keycode_dpad_center
keycode_back
keycode_call
keycode_camera
keycode_clear
kecode_endcall
keycode_home
keycode_menu
keycode_mute
keycode_power
keycode_search
keycode_volume_up
keycode_volume_down
你可以看到这些动作键是在KeyEvent的 API 中定义的,在 http://developer.android.com/reference/android/view/KeyEvent.html 有。
Note
并非所有这些操作键都可以用于搜索,但有些可以,比如 keycode_call。你将不得不尝试每一个,看看哪一个适合你的需要。
一旦你知道你想要使用哪个动作键,你可以告诉 Android 你对这个键感兴趣,通过使用 XML 段把它放到元数据中,如清单 12-23 所示。
清单 12-23。动作键定义示例
<searchable xmlns:android="http://schemas.android.com/apk/res/android
android:label="@string/search_label"
android:hint="@string/search_hint"
android:searchMode="showSearchLabelAsBadge"
android:includeInGlobalSearch="true"
android:searchSuggestAuthority=
"com.androidbook.search.simplesp.SimpleSuggestionProvider"
android:searchSuggestSelection=" ? "
>
<actionkey
android:keycode="KEYCODE_CALL"
android:queryActionMsg="call"
android:suggestActionMsg="call"
android:suggestActionMsgColumn="call_column" />
<actionkey
android:keycode="KEYCODE_DPAD_CENTER"
android:queryActionMsg="doquery"
android:suggestActionMsg="dosuggest"
android:suggestActionMsgColumn="my_column" />
.....
</searchable>
同一搜索上下文也可以有多个操作键。下面是actionKey元素的每个属性代表什么,以及如何用它来响应动作键的按下。
keycode:这是在KeyEventAPI 类中定义的 keycode,应该用于调用搜索活动。由键码识别的该键可以被按下两次。第一种情况是,用户在 QSB 中输入查询文本,但没有导航到任何建议。通常,在没有动作键实现的情况下,用户会按下 QSB 的搜索图标。通过在搜索的元数据中指定一个操作键,Android 允许用户点击操作键,而不是 QSB 搜索 Go 图标。第二种是当用户导航到一个特定的建议,然后单击 action 键。在这两种情况下,搜索活动都是通过一个动作ACTION_SEARCH调用的。要知道这个动作是通过一个动作键调用的,可以查找一个名为SearchManager.ACTION_KEY的额外字符串。如果您在这里看到一个值,您知道您正在响应一个动作键的按下而被调用。queryActionMsg:您在这个元素中输入的任何文本都被传递给搜索活动,调用 intent 作为一个名为SearchManager.ACTION_MSG的额外字符串。这是在用户将文本输入到 QSB 中并按下动作键时完成的。suggestActionMsg:您在这个元素中输入的任何文本都被传递给搜索活动,调用 intent 作为一个名为SearchManager.ACTION_MSG的额外字符串。当一个建议被聚焦并且用户已经按下动作键时,这被完成。如你所见,在意图中,“额外”键和queryActionMsg键是一样的。如果您为这两个字段赋予相同的值,比如call,那么您将不会知道用户以何种方式调用了操作键。在许多情况下,这是不相关的,所以你可以给两者相同的值。但是如果你需要区分两者,你必须指定一个不同于queryActionMsg的值。suggestActionMsgColumn:值queryActionMsg和suggestActionMsg全局应用于这个搜索活动和建议提供者。没有办法根据建议改变动作的含义。如果您想这样做,那么您将需要告诉元数据,在每个建议的建议光标中有一个额外的列来携带这个消息。这将允许 Android 从额外的列中提取文本,并将其作为调用ACTION_SEARCH意图的一部分发送给活动。有趣的是,来自光标的这个额外列的值是通过 intent 中的同一个额外键发送的,即SearchManager.ACTION_MSG。
在这些属性中,keycode 是必需的。此外,要激发动作键,至少需要另外三个属性中的一个。
如果您要使用suggestActionMsgColumn,您需要在建议提供者类中填充这个列。在清单 12-23 的searchableinfo XML 文件中,如果你要使用所示的两个动作键,那么你需要在建议光标中定义指定的两个附加字符串列(见清单 12-12),即call_column和my_column。在这种情况下,清单 12-12 中的光标列数组将如清单 23-24 所示。
清单 12-24。建议光标中操作键列的示例
private static final String[] COLUMNS = {
"_id", // must include this column
SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_TEXT_2,
SearchManager.SUGGEST_COLUMN_INTENT_DATA,
SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
"call_column",
"my_column"
};
使用特定于应用的搜索上下文
Android search 允许一个活动在被调用时向搜索活动传递额外的搜索数据。让我们浏览一下细节。
正如我们在第十一章中所展示的,应用中的活动可以通过返回 false 来覆盖onSearchRequested()方法以禁用搜索。有趣的是,可以使用相同的方法将额外的特定于应用的数据传递给搜索活动。清单 12-25 给出了一个例子。
清单 12-25。将附加应用数据传递给搜索活动
public boolean onSearchRequested()
{
Bundle applicationData = new Bundle();
applicationData.putString("string_key","some string value");
applicationData.putLong("long_key",290904);
applicationData.putFloat("float_key",2.0f);
startSearch(null, // Initial Search search query string
false, // don't "select initial query"
applicationData, // extra data
false // don't force a global search
);
return true;
}
Note
可以使用下面的 Bundle API 参考来查看 Bundle 对象上可用的各种函数: http://developer.android.com/reference/android/os/Bundle.html 。
一旦搜索以这种方式开始,活动就可以使用名为SearchManager.APP_DATA的extra来检索应用数据包。清单 12-26 显示了如何从搜索意图中检索上述每个应用数据字段(在清单 12-27 中设置),在负责搜索结果的搜索活动的onCreate()或onNewIntent()方法中。
清单 12-26。检索附加应用数据上下文
Bundle applicationData =
queryIntent.getBundleExtra(SearchManager.APP_DATA);
if (applicationData != null)
{
String s = applicationData.getString("string_key");
long l = applicationData.getLong("long_key");
float f = applicationData.getFloat("float_key");
}
让我们回到清单 12-25 所示的startSearch()方法。我们在第十一章中介绍了这个startSearch()方法,你可以在下面的活动 API 中找到更多关于它的信息:
http://developer.android.com/reference/android/app/Activity.html
快速提供该方法的概述可能是有益的。它有以下四个参数:
initialQuery // a string argumentselectInitialQuery // booleanapplicationDataBundle //BundleglobalSearchOnly //boolean
第一个参数(如果可用)将填充 QSB 中的查询文本。如果为真,第二个布尔参数将突出显示文本。这样做将使用户能够用键入的内容替换所有选定的查询文本。如果为 false,则光标将位于查询文本的末尾。第三个理由当然是我们正在准备的捆绑包。第四个参数,如果为真,将总是调用全局搜索。如果为假,则首先调用本地搜索(如果可用);否则,它将使用全局搜索。
参考
以下是我们在撰写本章时发现的有价值的资源列表:
http://developer.android.com/guide/topics/search/index.html:谷歌安卓搜索的搜索概况和入口文档。http://developer.android.com/guide/topics/search/searchable-config.html:Google 的这个 URL 是一个关键文档,它概括了searchableinfoXML 文件中可用的属性。http://developer.android.com/reference/android/app/SearchManager.html:主要 Android 搜索工具的 API 参考,即SearchManager.您将在这里看到一些您在编写建议提供程序时使用的常量。http://developer.android.com/reference/android/os/Bundle.html:您可以使用这个 bundle API 引用来查看bundle对象上可用的各种函数。这对于特定于应用的搜索数据非常有用。http://www.androidbook.com/notes_on_search:在这个网址,你可以在安卓搜索上找到作者的笔记。即使在这本书出版后,我们也会继续更新内容。您将在这里找到代码片段、摘要、用于搜索的关键 URL,以及每个版本中的变化。http://developer.android.com/reference/android/view/KeyEvent.html:了解选择它们作为搜索关键字的常量是什么。- 在
www.androidbook.com/expertandroid/projects下载本章专用的测试项目。ZIP 文件的名称是ExpertAndroid_ch12_CustomSuggestionProvider.zip。
摘要
在这一章中,我们通过编写一个来自 scrach 的自定义建议提供程序,详细介绍了 Android 搜索的内部工作原理。在这个过程中,我们详细演示了建议光标及其列,探索了负责从建议提供者那里获取数据的 URIs,并给出了许多示例代码,这些代码可以使设计和实现您自己的创造性搜索策略变得容易。
复习问题
以下问题应作为你在本章中所学内容的里程碑:
How do you specialize a content provider to become a suggestion provider? How many types of search URIs are there? Why and how do you differentiate various search URIs? How do you know the names of the suggestion cursor columns? What Java class do you use to construct a cursor from the names of the cursor columns? How do you add rows to a MatrixCursor? How do you co-opt action keys to be search keys? How do you pass application-specific data to a search activity?