65.9K
CodeProject 正在变化。 阅读更多。
Home

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

2014 年 11 月 17 日

CPOL

8分钟阅读

viewsIcon

18072

在本文中,我将解释如何在保持UI连续性的同时,将本地搜索功能添加到我们现有的餐厅应用程序中。

引言

各种应用程序都需要的基本功能是搜索。在我们的案例中,我们有一个餐厅应用程序,需要用户能够轻松快速地搜索菜单,找到他们想要的东西。在本文中,我将解释如何在保持UI连续性的同时,将本地搜索功能添加到我们现有的餐厅应用程序中。除了介绍如何向Activity添加GestureOverlayView之外,我还会详细阐述我做出的UI选择及其原因。

图1:餐厅应用程序中搜索视图的屏幕截图

搜索

对于搜索功能,在我们开始编码之前,需要考虑一些设计因素。您想搜索什么?我们希望搜索标题和描述,以便用户获得最大结果,因为标题并不总是能说明菜肴的实际内容。此外,您可以为每道菜添加一些隐藏的元数据进行搜索。至于搜索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;
    }
代码示例1:搜索我们数据库的方法

接下来,我们的主 Activity 需要在操作栏中设置搜索选项。有关如何设置操作栏的更多信息,请阅读本文:为 Android* 设备构建动态 UI。搜索功能将完全在我们的应用程序内部处理;我们不希望搜索开始列出设备上安装的应用程序,或发送意图让另一个搜索应用程序来处理。

将此字符串变量添加到MainActivity类中。我们将使用它将查询字符串发送到搜索意图。

/* Search string label */
    public final static String SEARCH_MESSAGE= "com.example.restaurant.MESSAGE";
代码示例2:用于向搜索意图添加扩展数据的类变量

更新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);
    }
代码示例3:操作栏初始化代码

以及 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");

			}
		});
	}
代码示例4:主要搜索结果类(下文继续)

当我们构建列表时,我们还将处理未找到任何匹配结果的情况。如果没有匹配项,我们会向查看者显示一个对话框消息以告知他们并关闭搜索活动,这样他们就不会看到空白页面。

/**
	 * 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();
		}
	}
代码示例4(续)

类的这一部分是网格视图的适配器,我们能够从主菜单代码本身重用它,只做了少量修改。我们还可以调整布局文件,因此保持 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;
		}
	}
}
代码示例4(续)

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>
代码示例5:搜索视图横向布局XML

图2:用户点击项目时显示的详细视图

手势覆盖

要退出搜索视图,我们希望它能通过滑动向左或向右,类似于菜单其余部分中的视图翻页滚动。对于列表视图,GestureDetector 工作得非常出色,但与网格视图结合使用时无效。因此,我们必须切换到使用 GestureOverlayView。您需要使用 SDK 示例中(例如 android\sdk\samples\android-19\legacy\GestureBuilder)的 GestureBuilder 应用程序来构建手势库。在您的设备上构建并启动应用程序,并使用它来命名和创建手势。添加所有需要的手势后(在我们的例子中,左滑和右滑),将“gestures”文件从您的设备复制到 res/raw 文件夹。应用程序会告诉您手势文件的保存位置。在我的例子中,我只需通过 USB 连接我的设备,它就在根文件夹中。

图3:手势构建器应用程序和我们添加的手势的屏幕截图

一旦您的文件就位,请使用以下代码更新 SearchResultsActivity 类:

GestureLibrary gestureLibrary;
GestureOverlayView gestureOverlayView;
代码示例6: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);
代码示例7:在onCreate中初始化GestureOverlayView

动画文件 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"
/>
代码示例8:向左移动动画代码

请注意,当您的 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>
代码示例9:更新后的带有 GestureOverlayView 的 GridView 片段,用于布局 XML

摘要

您现在已经了解了如何将本地搜索添加到 Android 应用程序中,并了解了做出某些关键 UI 选择的原因。我还指出了出现的一些挑战以及如何避免它们。您现在应该能够在考虑用户体验的同时将搜索功能整合到您自己的应用程序中。

参考文献

https://developer.android.com.cn/training/search/index.html

关于作者

Whitney Foster 是 Intel 软件解决方案组的软件工程师,致力于 Android 应用程序的规模化支持项目。

*其他名称和品牌可能被声明为他人的财产。
**此示例源代码根据英特尔示例源代码许可协议发布。

© . All rights reserved.