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





1.00/5 (1投票)
在本文中,我将解释如何在保持UI连续性的同时,将本地搜索功能添加到我们现有的餐厅应用程序中。
引言
各种应用程序都需要的基本功能是搜索。在我们的案例中,我们有一个餐厅应用程序,需要用户能够轻松快速地搜索菜单,找到他们想要的东西。在本文中,我将解释如何在保持UI连续性的同时,将本地搜索功能添加到我们现有的餐厅应用程序中。除了介绍如何向Activity添加GestureOverlayView之外,我还会详细阐述我做出的UI选择及其原因。
搜索
对于搜索功能,在我们开始编码之前,需要考虑一些设计因素。您想搜索什么?我们希望搜索标题和描述,以便用户获得最大结果,因为标题并不总是能说明菜肴的实际内容。此外,您可以为每道菜添加一些隐藏的元数据进行搜索。至于搜索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"); } }); }
当我们构建列表时,我们还将处理未找到任何匹配结果的情况。如果没有匹配项,我们会向查看者显示一个对话框消息以告知他们并关闭搜索活动,这样他们就不会看到空白页面。
/** * 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 示例中(例如 android\sdk\samples\android-19\legacy\GestureBuilder)的 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 应用程序的规模化支持项目。
*其他名称和品牌可能被声明为他人的财产。
**此示例源代码根据英特尔示例源代码许可协议发布。