在您的 Android 应用中创建搜索字段






4.09/5 (3投票s)
Android SDK 提供了一套 API 来在您的应用中实现此模式,在本文中,我们将回顾将其实现到您的应用程序中所需的第一步。
英特尔® 开发者专区提供用于跨平台应用开发的工具和操作指南信息、平台和技术信息、代码示例以及同行专业知识,以帮助开发者创新和成功。加入我们的安卓、物联网、英特尔® 实感™ 技术和Windows社区,下载工具、访问开发工具包、与志同道合的开发者分享想法,并参加黑客马拉松、竞赛、路演和本地活动。
搜索是大量 Android 应用程序中都存在的一项功能。在您的应用内提供一种搜索内容的方式非常重要,因为它能帮助用户找到他们想要的内容。然而,这种搜索必须快速高效,因为获取信息可能是用户打开您应用的主要原因。Android SDK 提供了一套 API 来在您的应用中实现此模式,在本文中,我们将回顾将其实现到您的应用程序中所需的第一步。
开始动手!
我们开始吧!在 Android Studio 中使用“Empty activity”模板创建一个新项目。项目创建后,添加一个新的菜单文件,并将其命名为 res/menu/menu_search.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/search"
            android:title="@string/hint_search"
            android:icon="@android:drawable/ic_menu_search"
            app:showAsAction="collapseActionView|ifRoom"
            app:actionViewClass="android.support.v7.widget.SearchView" />
</menu>
我们的菜单文件只包含一个负责显示搜索按钮的项目。当用户按下此按钮时,它会展开并显示一个文本字段,允许用户输入要搜索的词条。负责此任务的控件是 SearchView,正如我们在 app:actionViewClass 属性中看到的那样。将 app:showAsAction 定义为 collapseActionView 值,可以让 SearchView 在按钮被点击时自行展开。
请注意,我们在这里使用了支持库,以保持在不同 Android 版本间的兼容性。
一旦我们创建了菜单文件,我们将在 MainActivity 中加载它,如下所述。
public class MainActivity extends AppCompatActivity 
        implements SearchView.OnQueryTextListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_search, menu);
        MenuItem searchItem = menu.findItem(R.id.search);
        SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
        searchView.setOnQueryTextListener(this);
        return true;
    }
    @Override
    public boolean onQueryTextSubmit(String query) {
        // User pressed the search button
        return false;
    }
    @Override
    public boolean onQueryTextChange(String newText) {
        // User changed the text
        return false;
    }
}
在 onCreateOptionsMenu() 内部,我们使用 inflate() 加载菜单资源文件。通过 Menu 对象,我们找到包含 SearchView 控件的 MenuItem。通过为 SearchView 设置一个 OnQueryTextListener 对象,我们的应用可以检测到两个事件。
- 当用户在文本字段中输入每个字符时,会调用 onQueryTextChange;
- 当按下搜索按钮时,会触发 onQueryTextSubmit。
因此,您可以在这些方法中实现任何您想要的功能来执行搜索(例如,访问 SQLite 数据库或 Web 服务)。就是这样!这是在您的应用中实现搜索功能的最简单方法。
改善搜索体验
前面的方法效果不错,但通常需要搜索操作来帮助用户找到他们想要的东西。一个很好的方法是在用户输入时显示一些建议。为了演示这种方法,我们将在用户在 SearchView 中输入城市名称时显示一个城市列表。这个列表存储在服务器上。我们要做的第一件事是创建一个可搜索的配置。因此,在项目中添加一个新文件 res/xml/searchable.xml
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
            android:hint="@string/hint_search"
            android:label="@string/app_name"
            android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
            android:searchSuggestAuthority="ngvl.android.demosearch.citysuggestion"
            android:searchSuggestIntentAction="android.intent.action.VIEW"
            android:searchSuggestIntentData="content://ngvl.android.demosearch.city"/>
让我们来理解一下这个文件中定义的属性
- 使用 android:voiceSearchMode属性启用了语音搜索按钮。
- 当用户输入时,将会在响应 android:searchSuggestAuthority的内容提供程序中调用query()方法(我们稍后会看到这个提供程序)。
- 当用户在列表中选择一个建议时,将使用 android:searchSuggestIntentAction属性中描述的操作调用一个新的 Activity。
- android:searchSuggestIntentData对前一个属性进行了补充,因为它为用户点击建议时触发的 Intent 定义了一个- Uri模式。
正如我们所知,内容提供程序通常访问 SQLite 数据库来存储和检索数据,但我们也可以用它来访问网络,正如 Android 文档明确指出的那样
内容提供程序“可能同时被多个线程调用,并且必须是线程安全的”。
在这个例子中,建议将从存储在 Web 服务器上的 JSON 文件中检索。创建一个名为 CitySuggestionProvider 的新内容提供程序,只需实现 query() 方法(其他方法为空即可)。
public class CitySuggestionProvider extends ContentProvider {
    List<String> cities;
    @Override
    public boolean onCreate() {
        return false;
    }
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        if (cities == null || cities.isEmpty()){
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    .url("https://dl.dropboxusercontent.com/u/6802536/cidades.json")
                    .build();
            try {
                Response response = client.newCall(request).execute();
                String jsonString = response.body().string();
                JSONArray jsonArray = new JSONArray(jsonString);
                cities = new ArrayList<>();
                int lenght = jsonArray.length();
                for (int i = 0; i < lenght; i++) {
                    String city = jsonArray.getString(i);
                    cities.add(city);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        MatrixCursor cursor = new MatrixCursor(
                new String[] {
                        BaseColumns._ID,
                        SearchManager.SUGGEST_COLUMN_TEXT_1,
                        SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID
                }
        );
        if (cities != null) {
            String query = uri.getLastPathSegment().toUpperCase();
            int limit = Integer.parseInt(uri.getQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT));
            int lenght = cities.size();
            for (int i = 0; i < lenght && cursor.getCount() < limit; i++) {
                String city = cities.get(i);
                if (city.toUpperCase().contains(query)){
                    cursor.addRow(new Object[]{ i, city, i });
                }
            }
        }
        return cursor;
    }
    // 
      Other methods are empty...
}
当用户开始输入文本时,我们的提供程序会通过类似下面的 URI 被调用。content://ngvl.android.demosearch.citysuggestion/search_suggest_query/santos?limit=50
系统在 URI 的末尾添加 search_suggest_query/<text> 来调用我们的提供程序。默认情况下,结果限制设置为 50,并作为查询参数传递。值得注意的是,搜索操作是在 UI 线程之外完成的,因此我们可以像我们所做的那样执行 HTTP 请求。在这个例子中,我们将城市列表存储在内存中,但在实际应用中,我们可以使用 SQLite 数据库中的一个表。
我们使用 Uri 对象,通过 getLastPathSegment() 获取了文本参数,并通过 getQueryParameter() 获取了要返回的最大建议数。从网络读取内容后,我们构建了一个 MatrixCursor 来表示建议的城市列表。这里一个重要的细节是列名。列“_id”和“suggest_text_1”是必须的。您可以使用更多的列或创建 CursorAdapter 的子类来自定义您的建议列表。
正如我之前所说,我们的建议存储在 Web 服务器上的一个 JSON 文件中。因此,在这里,我们执行请求来检索城市列表,并使用 OkHttp 库将其作为 JSON 文件读取。所以,别忘了在 build.gradle 文件中添加这个依赖。
dependencies {
    ...
    compile 'com.squareup.okhttp3:okhttp:3.0.1'
}
请确保在 AndroidManifest.xml 中添加 INTERNET 权限,并且内容提供程序的声明如下所示。
<manifest ...
    <uses-permission android:name="android.permission.INTERNET"/>
    <application...
        <provider
            android:name=".CitySuggestionProvider"
            android:authorities="ngvl.android.demosearch.citysuggestion"
            android:enabled="true"
            android:exported="true"/>
请确保 android:authorities 与可搜索配置文件中声明的相同。
之后,我们还需要将 Activity 与可搜索配置连接起来。修改 AndroidManifest.xml 中的 Activity 声明。
<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.SEARCH"/>
    </intent-filter>
    <meta-data
        android:name="android.app.searchable"
        android:resource="@xml/searchable"/>
</activity>
通过为 <meta-data> 标签使用 android.app.searchable 值,我们定义了该 Activity 具有一个可搜索的配置。现在我们需要将此信息设置到 SearchView 控件中。将 onCreateOptionsMenu() 修改成如下所示。
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_search, menu);
    MenuItem searchItem = menu.findItem(R.id.search);
    SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
    searchView.setOnQueryTextListener(this);
    SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    searchView.setSearchableInfo(searchManager.getSearchableInfo(
            new ComponentName(this, MainActivity.class)));
    searchView.setIconifiedByDefault(false); 
    return true;
}
运行应用程序并在 SearchView 中开始输入一些文本。您会看到一些建议被显示出来。

很好!我们的建议功能正在工作。但是当选择一个建议时会发生什么?或者如果我按下了键盘上的搜索按钮呢?
处理建议和搜索操作
目前,当用户按下搜索按钮或在列表中选择一个建议时,会创建并显示一个 MainActivity 的新实例。为了避免这种情况,我们可以在 AndroidManifest.xml 中将我们 Activity 的启动模式设置为 singleTop,并实现 onNewIntentMethod()。
<activity android:name=".MainActivity"
    android:launchMode="singleTop">
@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
        String query = intent.getStringExtra(SearchManager.QUERY);
        Toast.makeText(this, "Searching by: "+ query, Toast.LENGTH_SHORT).show();
    } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
        String uri = intent.getDataString();
        Toast.makeText(this, "Suggestion: "+ uri, Toast.LENGTH_SHORT).show();
    }
}
当用户点击搜索按钮时,Activity 会使用 ACTION_SEARCH 被调用,但当选择一个建议时,会使用 ACTION_VIEW(正如我们在可搜索配置文件中定义的那样)。如果您现在运行应用程序并执行搜索,将会使用 MainActivity 的同一个实例,而不是创建一个新的实例。
您可以毫无问题地使用这种方法,但您可能需要(或更喜欢)在另一个 Activity 中执行搜索。为此,我们将创建一个新的可搜索 Activity,当用户按下搜索按钮或选择一个建议时,该 Activity 将被调用。
它在 AndroidManifest.xml 中的声明必须是这样的。
<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>
记得从 MainActivity 的声明中移除这些相同的配置(<intent-filter> 和 <meta-data>)。所以现在,我们必须对 MainActivity 做一点小小的改动。
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    ...
    searchView.setSearchableInfo(searchManager.getSearchableInfo(
            new ComponentName(this, SearchableActivity.class)));
    ...
}
// Remove onNewIntent method too
通过这种实现,当用户按下回车/搜索按钮时,会使用 "android.intent.action.SEARCH" 操作和一个名为 "query" 的参数启动一个新的 Activity。这个参数代表用户在 SearchView 中输入的文本。下面的代码展示了如何在 SearchableActivity 中处理搜索。
public class SearchableActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_searchable);
        TextView txt = (TextView)findViewById(R.id.textView);
        Intent intent = getIntent();
        if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
            String query = intent.getStringExtra(SearchManager.QUERY);
            txt.setText("Searching by: "+ query);
        } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
            String uri = intent.getDataString();
            txt.setText("Suggestion: "+ uri);
        }
    }
}
一旦您有了查询参数,您就可以搜索任何您想要的内容。例如,在本地数据库/内容提供程序中或在 Web 服务中搜索。
结论
在本文中,我们在一个 Android 应用中实现了一个搜索功能,并看到了它是多么容易。这些只是第一步,更多详情请查看Android 搜索培训教程。
本示例的源代码可在我的 GitHub 上获取。

