为您的 Android* 应用添加本地搜索功能





0/5 (0投票)
在本文中,我将解释如何在保持 UI 连续性的同时,为我们现有的餐厅应用程序添加本地搜索功能。
Intel® Developer Zone 提供跨平台应用程序开发工具和操作指南、平台和技术信息、代码示例以及同行专业知识,帮助开发人员创新并取得成功。加入我们的社区,获取 Android、物联网、Intel® RealSense™ 技术 和 Windows 的相关信息,下载工具,访问开发套件,与志同道合的开发人员分享想法,并参与黑客马拉松、竞赛、路演和本地活动。
引言
搜索功能是各种应用程序的基本需求。在我们的案例中,我们有一个餐厅应用程序,需要用户能够轻松快速地搜索菜单以找到他们想要的东西。在本文中,我将解释如何在保持 UI 连续性的同时,为我们现有的餐厅应用程序添加本地搜索功能。我将详细介绍我所做的 UI 选择及其原因,并介绍如何为 activity 添加 GestureOverlayView。
搜索
对于搜索,在开始编写代码之前,我们需要考虑许多设计问题。您想搜索什么?我们想搜索标题和描述,以供用户获得最大化的搜索结果,因为标题并不总是能说明菜肴的实际内容。此外,您还可以为每道菜添加一些隐藏的元数据进行搜索。至于搜索 activity 的布局,您希望搜索结果如何显示?我最初使用列表视图显示结果,就像购物车视图 activity 一样。但是,这些菜肴看起来不太吸引人,因为图片很小,而且当我把图片放大时,页面上的结果空间就变少了。所以,我决定坚持使用主菜单使用的网格视图,但不是在一侧有一个较大的详细视图,而是让网格视图占据整个屏幕,以便很容易地区分于普通菜单。要查看商品的详细视图,用户现在点击商品,它会显示为一个对话框片段,悬停在页面上方(参见图 2)。这样,用户可以快速点击关闭它,然后点击另一个商品进行查看。搜索需要快速流畅地工作,因为用户希望尽快找到他们想要的东西,否则他们可能找不到该商品,或者在寻找时感到沮丧而离开。最后,我们将如何处理用户的隐私?我们可以设计一个提供基于最近查询建议的搜索,或者设计一个需要输入个人信息的搜索。这会引发关于其他人看到您正在搜索的内容以及您的个人数据去向的担忧。虽然在我们的例子中,这只是一个餐厅应用程序,所以您不应该太担心别人知道您喜欢派,但在某些应用程序中,您需要考虑隐私。对于我们的应用程序,它不需要任何个人信息,不记录任何搜索词,也没有搜索词的历史记录。
在我们的餐厅应用中实现它的第一步是查看我们的数据库类,并添加一个方法来构建一个新的搜索结果表以供显示。您可以在此处阅读更多关于餐厅数据库设置的信息:使用数据库与您的 Android* 应用。使用 SQLite 查询,我们可以用几行代码轻松搜索我们的数据库以查找我们的商品。在这里,我们正在搜索名称和描述中包含搜索词以及任何附加文本的内容。我们还返回所有列,因为稍后我们需要这些信息在详细视图中显示。请注意,如果您的数据库非常大,可能会有延迟,您需要向用户显示一个进度条或加载指示器。
/**
* Builds a table of items matching the searchTerm in their name or description
*/
public Cursor searchMenuItems(String searchTerm) {
SQLiteDatabase db = getReadableDatabase();
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(TABLES.MENU);
Cursor c = qb.query(db, null, "("+MenuColumns.NAME+" LIKE '%"+searchTerm+"%') " +
"OR ("+MenuColumns.DESCRIPTION+" LIKE '%" + searchTerm+"%')",
null, null, null, null);
return c;
}
接下来,我们需要在操作栏中设置主 activity 的搜索选项。有关如何设置操作栏的更多信息,请阅读本文:为 Android* 设备构建动态 UI。搜索功能将完全在我们的应用程序内部处理;我们不希望搜索列出设备上安装的应用程序,也不希望发送意图让另一个搜索应用程序处理。
将此字符串变量添加到 MainActivity
类。我们将使用它将查询字符串发送到搜索意图。
/* Search string label */
public final static String SEARCH_MESSAGE= "com.example.restaurant.MESSAGE";
更新 MainActivity
的 onCreateOptionsMenu
方法。
/**
* Initialize the action menu on action bar
*/
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.action_bar, menu);
//set up the search
MenuItem searchItem = menu.findItem(R.id.action_search);
SearchView mSearchView = (SearchView) searchItem.getActionView();
searchItem.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM
| MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
//set up the query listener
mSearchView.setOnQueryTextListener(new OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
//start the search intent
Intent searchIntent = new Intent(MainActivity.this, SearchResultsActivity.class);
searchIntent.putExtra(SEARCH_MESSAGE, query);
startActivity(searchIntent);
return false;
}
@Override
public boolean onQueryTextChange(String query) {
//do nothing in our case
return true;
}
});
return super.onCreateOptionsMenu(menu);
}
以及 SearchResultsActivity
类。
public class SearchResultsActivity extends Activity{
TextView mQueryText;
GridView searchListResults;
SearchAdapter adapter;
Vector<com.example.restaurant.MenuFactory.MenuItem> searchList;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.search_query_grid_results);
mQueryText = (TextView) findViewById(R.id.txt_query);
//setup the grid view
searchListResults = (GridView)findViewById(R.id.search_results);
searchList= new Vector<com.example.restaurant.MenuFactory.MenuItem>();
//get and process search query here
final Intent queryIntent = getIntent();
doSearchQuery(queryIntent);
adapter= new SearchAdapter(this,searchList);
searchListResults.setAdapter(adapter);
//Listener for grid view
searchListResults.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id){
FragmentTransaction ft = getFragmentManager().beginTransaction();
Fragment prev = getFragmentManager().findFragmentByTag("dialog");
if (prev != null) {
ft.remove(prev);
}
ft.addToBackStack(null);
DialogFragment newFragment = SearchResultsDialogFragment.newInstance(searchList.elementAt(position));
newFragment.show(ft, "dialog");
}
});
}
当我们构建列表时,我们还将处理找不到任何匹配结果的情况。如果没有匹配项,我们会向查看者显示一个对话框消息,让他们知道,并关闭搜索 activity,以免他们看到空白页面。
/**
* Builds the found item list.
*/
private void doSearchQuery(final Intent queryIntent) {
//Get the query text
String message= queryIntent.getStringExtra(MainActivity.SEARCH_MESSAGE);
//Set the UI field
mQueryText.setText(message);
RestaurantDatabase dB= new RestaurantDatabase(this);
MenuFactory mMF= MenuFactory.getInstance();
Cursor c= dB.searchMenuItems(message);
Set<String> categories = new HashSet<String>();
while (c.moveToNext()) {
String category = c.getString(c.getColumnIndexOrThrow(RestaurantDatabase.MenuColumns.CATEGORY));
categories.add(category);
//build a new menu item and add it to the list
MenuItem item= mMF.new MenuItem();
item.setCategory(category);
item.setName(c.getString(c.getColumnIndexOrThrow(RestaurantDatabase.MenuColumns.NAME)));
item.setDescription(c.getString(c.getColumnIndexOrThrow(RestaurantDatabase.MenuColumns.DESCRIPTION)));
item.setNutrition(c.getString(c.getColumnIndexOrThrow(RestaurantDatabase.MenuColumns.NUTRITION)));
item.setPrice(c.getString(c.getColumnIndexOrThrow(RestaurantDatabase.MenuColumns.PRICE)));
item.setImageName(c.getString(c.getColumnIndexOrThrow(RestaurantDatabase.MenuColumns.IMAGENAME)));
searchList.add(item);
}
c.close();
//Handle the case of not finding anything
if(searchList.size()==0){
Intent intent = new Intent(SearchResultsActivity.this, OrderViewDialogue.class);
intent.putExtra(OrderViewActivity.DIALOGUE_MESSAGE, "Sorry, no matching items found.");
startActivity(intent);
SearchResultsActivity.this.finish();
}
}
在类的这一部分是网格视图的适配器,我们能够重用主菜单代码本身,只需进行一些小的编辑。我们还能够改编布局文件,因此保持 UI 在视觉上一致也有一个好处,就是可以轻松地重复使用代码,而不必从头开始。您可能已经注意到上面,我也重用了 OrderViewDialogue
,一个我为购物车编写的类,但它在这里也能正常工作。
/**
* SearchAdapter to handle the grid view of found items. Each grid item contains
* a view_grid_item which includes a image, name, and price.
*/
class SearchAdapter extends BaseAdapter {
private Vector<com.example.restaurant.MenuFactory.MenuItem> mFoundList;
private LayoutInflater inflater;
public SearchAdapter(Context c, Vector<com.example.restaurant.MenuFactory.MenuItem> list) {
mFoundList= list;
inflater = LayoutInflater.from(c);
}
public int getCount() {
return mFoundList.size();
}
public Object getItem(int position) {
return mFoundList.get(position);
}
public long getItemId(int position) {
return 0;
}
// create a new ItemView for each item referenced by the Adapter
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
ImageView picture;
TextView name;
TextView price;
if(v == null) {
v = inflater.inflate(R.layout.view_grid_item, parent, false);
v.setTag(R.id.picture, v.findViewById(R.id.picture));
v.setTag(R.id.grid_name, v.findViewById(R.id.grid_name));
v.setTag(R.id.grid_price, v.findViewById(R.id.grid_price));
}
picture= (ImageView) v.getTag(R.id.picture);
name= (TextView) v.getTag(R.id.grid_name);
price= (TextView) v.getTag(R.id.grid_price);
final MenuItem foundItem = (MenuItem) mFoundList.get(position);
InputStream inputStream = null;
AssetManager assetManager = null;
try {
assetManager = getAssets();
inputStream = assetManager.open(foundItem.imageName);
picture.setImageBitmap(BitmapFactory.decodeStream(inputStream));
} catch (Exception e) {
Log.d("ActionBarLog", e.getMessage());
} finally {
}
name.setText(foundItem.name);
price.setText(foundItem.price);
return v;
}
}
}
UI 布局的另一个考虑因素是横向与纵向模式。下面是 res/layout-land 文件夹中的 search_query_grid_results.xml。在这里,您可以看到 numColumns
设置为四,res/layout-port 文件与之相同,只是该字段的值为二。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:paddingBottom="5dp"
android:paddingTop="5dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/FragmentTitle"
android:text="Results For: " />
<TextView android:id="@+id/txt_query"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/OrderTitle"/>
</LinearLayout>
<GridView
android:id="@+id/search_results"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:paddingTop="10dp"
android:numColumns="4"
android:verticalSpacing="10dp"
android:horizontalSpacing="10dp"
android:layout_weight="1"
android:stretchMode="columnWidth"
android:gravity="center"/>
</LinearLayout>
手势叠加视图
要退出搜索视图,我们希望它像菜单其余部分的视图分页器滚动一样,通过滑动来向左或向右滑动。GestureDetector 在列表视图上效果非常好,但与网格视图结合使用时无效。因此,我们必须改用 GestureOverlayView。您需要使用 SDK 示例中的 GestureBuilder 应用程序(例如,android\sdk\samples\android-19\legacy\GestureBuilder)来构建一个手势库。在您的设备上构建并启动应用程序,然后使用它来命名和创建手势。添加完所有需要的手势后(在我们的例子中是左滑和右滑),将“gestures”文件从您的设备复制到 res/raw 文件夹。应用程序会告诉您它保存手势文件的位置。在我的例子中,我所要做的就是通过 USB 连接我的设备,它就在根文件夹中。
将文件放在相应位置后,使用以下内容更新 SearchResultsActivity
类。
GestureLibrary gestureLibrary;
GestureOverlayView gestureOverlayView;
在 onCreate
方法中,初始化视图,加载库,并设置当用户执行匹配手势时执行的操作的侦听器。确保匹配您在库中创建的名称。对于动画,我们将使用 overridePendingTransition
调用来执行它。我们为进入动画设置了一个 0 值,表示没有动画。您可以创建一个空白动画 XML 文件并引用它,但大多数情况下系统会感到困惑,并且退出动画会执行得非常快。
gestureOverlayView = (GestureOverlayView)findViewById(R.id.gestures);
//initialize the gesture library and set up the gesture listener
gestureLibrary = GestureLibraries.fromRawResource(this, R.raw.gestures);
gestureLibrary.load();
gestureOverlayView.addOnGesturePerformedListener(new OnGesturePerformedListener(){
@Override
public void onGesturePerformed(GestureOverlayView view, Gesture gesture) {
ArrayList<Prediction> prediction = gestureLibrary.recognize(gesture);
if(prediction.size() > 0){
String action= prediction.get(0).name;
//our gesture library contains "left swipe" and "right swipe" gestures
if("left swipe".equals(action)){
//slide out to the left
SearchResultsActivity.this.finish();
overridePendingTransition(0, R.anim.move_left);
} else if("right swipe".equals(action)){
//slide out to the right
SearchResultsActivity.this.finish();
overridePendingTransition(0, R.anim.move_right);
}
}
}});
//gesture is transparent (no longer a yellow line)
gestureOverlayView.setGestureVisible(false);
动画文件 move_left.xml:(move_right.xml 相同,只是 toXDelta 为正)
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:fromXDelta="0"
android:toXDelta="-100%"
android:interpolator="@android:anim/decelerate_interpolator"
/>
请注意,当 GridView 位于 GestureOverlayView 中时,其 layout_height 不能为 0dp,因为它实际上只会获得 0dp,而不是像线性布局那样按需扩展。为了在我们的情况下进行此处理,我们将 layout_height
设置为 fill_parent
。我们也不希望我们的手势可见,并且不希望在等待我们隐形手势淡入时有延迟,因此您会看到 fadeOffset
和 fadeDuration
设置为 0。
<android.gesture.GestureOverlayView
android:id="@+id/gestures"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:fadeOffset="0"
android:fadeDuration="0"
android:eventsInterceptionEnabled="true">
<GridView
android:id="@+id/search_results"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:paddingTop="10dp"
android:numColumns="4"
android:verticalSpacing="10dp"
android:horizontalSpacing="10dp"
android:layout_weight="1"
android:stretchMode="columnWidth"
android:gravity="center"/>
</android.gesture.GestureOverlayView>
摘要
您现在已经了解了如何为 Android 应用程序添加本地搜索功能,并了解了为什么做出一些关键的 UI 选择。我还指出了出现的一些挑战以及如何避免它们。现在,您应该能够在考虑用户体验的同时,将搜索功能集成到您自己的应用程序中。
参考文献
https://developer.android.com.cn/training/search/index.html
关于作者
Whitney Foster 是 Intel 软件解决方案组的一名软件工程师,致力于 Android 应用程序的规模化支持项目。
*其他名称和品牌可能被声明为他人的财产。
**此示例源代码根据英特尔示例源代码许可协议发布。