搜索是各種應用程序的一個基本功能需求. 在我們的案例中,我們擁有一個餐廳的應用程序,需要它能讓用戶可以方便和快速的搜索菜品清單,找到他們想要的東西. 在本文中,我將會描述我在對UI進行持續的維護時先我們現有的餐廳應用程序加入本地搜索功能過程. 我會詳細講述我選擇的UI方案及其理由, 此外還涉及到像activity中加入一個GestureOverlayView.
圖 1: 餐廳應用程序的搜索視圖截圖
搜索
對於搜索這個功能,在我們開始編寫代碼之前還需要做諸多設計上面的考慮. 你想要搜索的都是些什麼東西? 我們想要為用戶對標題和描述一並進行搜索,以獲得做大的結果集,而這是因為標題並不總是會告知這份菜品實際是什麼樣子的. 此外你還可以增加一些針對每個菜品的隱藏元數據的搜索. 而對於搜索activity的布局,你像讓搜索結果集如何顯示出來呢? 一開始我是用一個列表視圖來展示結果集,就像購物車視圖的activity. 然後,這樣的話菜品看上去就並不會吸引人來在上面點擊,因為圖片很小,而當我讓圖片變得更大的時候,頁面上能夠一屏展示的結果集就少了. 因此我決定把菜品清單的主題部分顯示方案鎖定在了網格視圖上, 但不去在一側顯示一個更大的詳細視圖,而是會讓網格視圖占據屏幕的整個顯示空間,使其能很容易的不同於一般的菜品清單. 現在為了查詢單項的詳細視圖,用戶要用手機單擊一項,然後它就會以一個浮動在界面之上的對話式的fragment顯示出來 (見圖 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. 搜索功能的處理將完全在我們的應用程序裡面; 我們並不想要在搜索一開始的時候列出已經安裝在設備上的應用程序清單,或者發送一個intent來讓另外一個搜索應用程序來進行處理.
向 MainActivity 類添加如下這個字符串變量. 我們會使用這個變量來向搜索的intent發送所要查詢的字符串:
/* Search string label */
public final static String SEARCH_MESSAGE= "com.example.restaurant.MESSAGE";
代碼示例 2: 向搜索intent中添加擴展數據的類變量
更新 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: 主要的搜索結果類 (下面還會有)
當我們構建這個列表是,我們也將會處理沒有查詢到任何匹配項的情況下應該怎麼做. 如果沒有匹配,我們會查搜索的人顯示一個消息對話框,讓他們知曉,並且關閉搜索的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();
}
}
代碼示例 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 續
需要在布局上考慮的另外一個問題就是屏幕的橫向和縱向模式. 底下是 res/layout-land 目錄中的 search_query_grid_results.xml 文件. 你可以從這兒發現 numColumns 被設置成了4, 除了這個值是2以外, 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"
android:text="Results For: " />
<TextView android:id="@+id/txt_query"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</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. 你見更需要使用 GestureBuilder 應用程序構建一個手勢庫,這個應用程序可以再SDK示例 (例如. android\sdk\samples\android-19\legacy\GestureBuilder) 中找到. 在你的蛇擺上構建並運行這個應用,並使用它來命名和創建出手勢. 把所有你需要的手勢都添加好了(在我們的案例中,就是向左掃動和向右掃動), 然後就將‘gestures’文件從你的設備復制到 res/raw 目錄中. 應用會告訴你將手勢文件保存到哪個地方. 在我這兒,所有要做的就是通過USB連接我們設備,手勢文件就在root目錄中.
圖 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: (除了toXDelta是正數之外 move_right.xml 跟 move_left.xml是一樣的)
<?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: 向左移動動畫的代碼
注意當用在一個GestureOverlayView裡面的時候,你的網格視圖不能有一個0dp的layout_height值, 因為將真的只是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: 得到用於布局 xml 的 GestureOverlayView 的更新的 GridView 塊
現在你已經了解到本地搜索可以被添加到Android應用程序中,此外還了解到一些關鍵的UI是怎麼做出選擇決策的. 我也指出了一些其中會出現的挑戰,以及如何去避免他們. 你現在應該能夠把搜索整合到你自己的應用程序去了,同時對用戶體驗進行更多的考慮.
https://developer.android.com/training/search/index.html
Whitney Foster 是Intel的軟件解決方案組的軟件工程師,工作於在Android應用程序上大規模應用的項目上.
*其它名稱和品牌可能已經被聲稱是屬於其它人的財產.
**這個實例的代碼以Intel示例源代碼許可證發布.
英文原文:Adding Local Search Functionality to Your Android* Application
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11