設計通訊錄搜尋程式 , 具有搜尋管理員(SearchManager)的程式 , 在程式一開始執行即彈出搜尋管理員 , 當使用者輸入聯絡人姓名關鍵字 , 則將搜尋的結果顯示於TextView中 (請使用者預先在通訊錄下新增聯絡人)。
要在我們的App中加入搜尋功能的話 , 為了達到這個目的我們可以使用 The search dialog and widget. 這兩個是Android 所提供的 search User Interface, 而 SearchDialog被限制出現在 Top of Activity, 而 SearchWidget 可以被自由的擺放在Activity中。
而不管你使用的是 SearchDialog or SearchWidget, 當他們被使用者執行時, 系統會產生一個Intent並且儲存使用者所下達的Query, 並且把他們送達給一個真正在處理Search動作的Activity , 因為 SearchDialog / SearchWidget 只是UI, 他們只提供一個給使用者操作的界面和傳遞資料, 不負責真正的Search動作, 此外真正在處理Search的 Activity也必須由我們自己實作, 而要完成在App中加入搜尋功能需要以下幾個部份:
- 一個 Searchable 設定是一個xml檔案, 裡面的內容包含這個Search UI 將要提供的功能例如語音搜尋或是搜尋 建議 , 提示文字等。
- 一個接收使用者Search Query的Activity, 也是真正在處理Searching 工作的Actitvity
- SearchDialog 預設是隱藏的, 要提供使用者喚起SearchDialog的機制 , 例如預設的SearchButton, MenuItem , 或是任何你所提示的按鈕。
接下來會搭配著程式碼解說如何產生以上所提到的部分:
首先是 Searchable Configuration > res/xml/searchable.xml
請記得一定要擺在 res/xml下, 另外我們可以看到searchable elements
android:label > 這是唯一被系統要求一定要定義的欄位 ,他是一個Application Name, 一般來說這個值在當我們為Quick Search Box開啟搜尋建議功能時 , 可以在系統的設定清單中的Searchable Items 裡找到它。
android:hint > 這是系統建議我們設定的欄位 , 當Search UI裡還沒被輸入任何查詢字串時 , 他會顯示並且提示使用者該輸入的查詢字串。
關於Searchable的欄位還有很多 , 詳細的可以參考官方資料
<searchable>
接著再附上 strings.xml
通訊錄搜尋 Search Search contact list....
接下來的流程是這樣的 : 使用者送出Query > Receiving the query > 真正的Searching 工作 > 搜尋的結果呈現。
這四個步驟我們使用兩個Activity來完成 , 分別是一個 Query Activity , 它的工作就是喚出Search UI 給使用者輸入相關的Query資訊 , 並且將Query資訊送達到 Searchable Activity。
Searchable Activity的工作就是 , Receiving the query 並且進行 Searching task , 然後把結果呈現出來 , 所以這麼看來Searchable Activity 是真正的主角。
首先先來看看Query Activity的程式碼 (根據這題的要求, 這個範例我們會使用的是 SearchDialog) :
package COM.TQC.GDD03; import android.app.Activity; import android.os.Bundle; public class GDD03 extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.main); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); //當使用鍵盤輸入時會自動喚起Search UI, 將輸入的字鍵入, 並且預設搜尋手機裡的資源和所提供的資訊 onSearchRequested(); //一般來說如果我們目的只是要喚起Search UI的話 //那直接進行 onSearchRequested(); 的呼叫即可 //如果我們需要添加一些能協助搜尋的資訊時 //就Override 這個 Function } @Override public boolean onSearchRequested() { // TODO Auto-generated method stub Bundle bundle = new Bundle(); //建立一個Bundle 攜帶想附加傳遞的資訊 //有甚麼要提供的額外搜尋資訊都可以加入其中 bundle.putString("Data", "搜尋結果"); startSearch("Search contact list", true, bundle, false); /* 這個Function 也可以達到呼叫 Search UI的功能 * public void startSearch (String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch) * * initialQuery : 預先顯示在Search UI的字串, 這樣就等於Search hint 起不了作用了 * selectInitialQuery : If true, the intialQuery will be preselected, which means that any further typing will replace it. This is useful for cases where an entire pre-formed query is being inserted. If false, the selection point will be placed at the end of the inserted query. This is useful when the inserted query is text that the user entered, and the user would expect to be able to keep typing. This parameter is only meaningful if initialQuery is a non-empty string. appSearchData : 如果想加強搜尋結果的品質, 可以將一些想提供的額外資訊裝入Bundle, Bundle會 insert to Search intent. Null if no extra data is required. globalSearch : If false, this will only launch the search that has been specifically defined by the application (which is usually defined as a local search). If no default search is defined in the current application or activity, global search will be launched. If true, this will always launch a platform-global (e.g. web-based) search instead. 很怕翻譯錯語意 , 所以以原文呈現 */ return true; } }
這個Activity 的任務較為簡單, 主要就是喚起 Search UI供使用者輸入 Query字串就好, 註解裡應該交代得蠻詳細了, 值得一提的是為了要讓 SEARCH button 能叫出Search UI, 也就是要讓onSearchRequested()起作用的話, 要事先在manifest.xml作宣告, 至於如何宣告在Searchable Activity 講完時會一起交待.
package COM.TQC.GDD03; import android.app.Activity; import android.app.SearchManager; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.provider.ContactsContract; import android.widget.TextView; import android.widget.Toast; public class SearchActivity extends Activity { private String queryAction; private TextView mTextView01; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mTextView01 = (TextView)this.findViewById(R.id.myTextView1); Intent intent = getIntent(); queryAction = intent.getAction(); if (Intent.ACTION_SEARCH.equals(queryAction)) // check 是否為 Search Action { String query = intent.getStringExtra(SearchManager.QUERY); //從intent中接收query字串 query = query.trim(); //刪掉空白字元, 怕使用者不經意連空白字元一起送出 Toast t = Toast.makeText(this, "", Toast.LENGTH_SHORT); Bundle bundle = intent.getBundleExtra(SearchManager.APP_DATA); if(bundle != null) { t.setText("搜尋的字串為:"+ query +" 附加的資訊為:" + bundle.getString("data")); } else t.setText("搜尋的字串為:"+ query + " 沒有附加的資訊"); t.show(); findContact(query); // 將query字串丟到 findContact() function, 這個 function 作用就是在處理Searching } } private void findContact(String query) { // TODO Auto-generated method stub String selection = ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "'" + " AND " + ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME + " LIKE " + "'%" + query + "%'"; Cursor cursor = getContentResolver().query(ContactsContract.Data.CONTENT_URI, new String[] {ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME} , selection , null , null); /* * 因為我們所要查詢手機中通訊錄的資料, 所以使用ContentResolver class 來存取Content model * 我們可以將 ContactsContract.Data.CONTENT_URI 這個參數視為想要存取的Table, 你也可以使用 null 他就會回傳所有的column 不過這樣比較沒有效率 * new String[] {ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME} 為要回傳的Column set * selection 這個先前定義好的字串相當於SQL語法中的 WHERE * * public final Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) * * uri : The URI, using the content:// scheme, for the content to retrieve. projection : A list of which columns to return. Passing null will return all columns, which is inefficient. selection A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows for the given URI. selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in the order that they appear in the selection. The values will be bound as Strings. sortOrder How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered. * */ int ColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); //取得我們想要的 Name 欄位在cursor中的column index StringBuffer name = new StringBuffer(); if(cursor.getCount()>0) { while(cursor.moveToNext()) { name.append(cursor.getString(ColumnIndex)+"\n"); } } else name.append("無資料"); mTextView01.setText(name.toString()); } }
首先要提到的就是當我們收到Search Intent之後如何從Intent中挖取我們要的資料, SearchManager, 有提到該使用哪些Data Key從Intent中獲得資料, 在成功取得搜尋結果後可以搭配ListView來顯示結果, 可以使用SimpleCusorAdapter, 不過這不是本題的重點, 想知道如何使用可以往前爬文, 或是在網誌搜尋關鍵字, 再來就是由於我們會存取到手機中的通訊錄資訊, 這部分還需要在Manifest.xml中宣告permission的權限.
現在我們要開始介紹Manifest的部分了, 這相當的重要
從這個AndroidManifest.xml看得出來我們定義了兩個Activity, 一個是GDD03 也就是之前說的 Query Activity 這是我們用來喚起Search UI 並且把Query字串傳送給 SearchActivity 也就是之前所說一個真正在處裡搜尋的Searchable Activity.
GDD03 Activity 裡的meta-data中, android:value 代表的要使用的Searchable Actvity名稱, 在這裡為我們的另一支程式 SearchActivity, 而 android:name="android.app.default_searchable"是固定的, 也因為這個宣告我們才有辦法在 GDD03 中才能使用
onSearchRequested()
啟動Search UI, 且SEARCH button才會起作用。而另一支Activity的宣告部分, 記得宣告它的Action為 <action android:name="android.intent.action.SEARCH"/> , 讓SearchActivity可以接受Search Intent, meta-data中的android:resource="@xml/searchable", 指定為我們放在 res/xml/searchable.xml 檔案, 也是設定Search configuration的來源, 另外 android:name="android.app.searchable" 這個屬性也是固定的. 最後記得宣告permission這樣我們才能存取手機中通訊錄的資料, <uses-permission android:name="android.permission.READ_CONTACTS"/>
值得一提的是如果有多個Activity都想要共用同一個SearchableActivity的話, 可以將 <meta-data android:name="android.app.default_searchable" android:value=".SearchActivity"/> , 插入到 Application 中 , 這麼一來Application中的Activity會自動得繼承meta-data元素 , 就像這樣
接下來要補充的是, 從接收Query字串到呈現搜尋結果為止我們使用了兩個Activity來完成, 如果只使用一個Activity來包辦所有工作呢? 也是可行的只是要注意的地方比較多, 我們接著探討這個作法.
首先我們會遇到一個問題, 當我們送出Query字串也就是會發送Search intent, 在之前兩個Activity的作法中, 這個Search intent會送到被指定的Searchable Activity, 這時候Searchable Activity收到相對應的intent 就會Create一個instance, 然後它會在Satck的最上層, 當我們看到搜尋的結果就會按Back鍵回到Query Activity繼續查詢動作, 這樣的流程似乎頗為合理, 但是當我們使用一個Activity包辦他們的工作時, 情況就不一樣了。
這個Activity (instance A)還是會發送出Search intent, 但是所指定的Searchable Activity就是自己本身, 當他又收到一個intent的時候, 會Create一個instance B 並且顯示搜尋結果, 造成instance隨著查詢次數增加, 其實在之前Activity的作法也會發生一樣的情況, 只要在Searchable Activity再進行搜尋動作, 他一樣會create an instance, 只是因為我們都會back到Query Activity 在進行查詢動作才把這個會產生許多instance的情況給避免了。
除了新增 android:launchMode="singleTop" 的屬性之外, <intent-filter> 中也要記得加入 <action android:name="android.intent.action.SEARCH"/>, 此外順序是有差別的, 至於為什麼呢? 這我還沒找到相關的資料, 找到之後我會再補上 XDD, <meta-data> 裡的屬性也只留下定義作為Searchable Activity即可, 因為根據預設它已經有了enable onSearchRequested() 的功能了。
但是只要設定singleTop屬性是不夠的, 之前在說明提到過Activity會去call onNewIntent() 去接收這個new intent, 所以我們必須在程式碼中去override 這個 method, 以下是程式碼的部分。
package COM.TQC.GDD03; import android.app.Activity; import android.app.SearchManager; import android.app.SearchManager.OnDismissListener; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.provider.ContactsContract; import android.util.Log; import android.widget.TextView; import android.widget.Toast; public class GDD03 extends Activity { private String queryAction; private TextView mTextView01; SearchManager SManager; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { Log.d("Test","onCreate()"); //onCreate() 只會被執行一次 super.onCreate(savedInstanceState); setContentView(R.layout.main); SManager = (SearchManager) getSystemService(SEARCH_SERVICE); SManager.setOnDismissListener(new OnDismissListener() { @Override public void onDismiss() { // TODO Auto-generated method stub Log.d("Test","onDismiss()"); continuteWork(); } }); mTextView01 = (TextView)this.findViewById(R.id.myTextView1); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); onSearchRequested(); } @Override protected void onNewIntent(Intent newIntent) { // TODO Auto-generated method stub super.onNewIntent(newIntent); Log.d("Test","onNewIntent()"); //如果Activity已經存在的話, 系統會將intent發送給 onNewIntent(), 而不是再去 call onCreate() setIntent(newIntent); //告知將換成newIntent handleIntent(newIntent); //將newIntent 送去check是不是 search intent, 是的話就進行searching動作 } private void handleIntent(Intent intent) { // TODO Auto-generated method stub queryAction = intent.getAction(); if(Intent.ACTION_SEARCH.equals(queryAction)) // check 是否為 Search Action { String query = intent.getStringExtra(SearchManager.QUERY); //從intent中接收query字串 query = query.trim(); //刪掉空白字元, 怕使用者不經意連空白字元一起送出 Toast t = Toast.makeText(this, "", Toast.LENGTH_SHORT); Bundle bundle = intent.getBundleExtra(SearchManager.APP_DATA); if(bundle != null) { t.setText("搜尋的字串為:"+ query +" 附加的資訊為:" + bundle.getString("data")); } else t.setText("搜尋的字串為:"+ query + " 沒有附加的資訊"); t.show(); findContact(query); // 將query字串丟到 findContact() function, 這個 function 作用就是在處理Searching } } private void continuteWork() { // TODO Auto-generated method stub //使剛剛暫停的工作繼續執行 Log.d("Test","continuteWork()"); } private void pauseWork() { // TODO Auto-generated method stub //使暫停正在執行的工作 , 等候使用者輸入查詢字串 Log.d("Test","pauseWork()"); } @Override public boolean onSearchRequested() { // TODO Auto-generated method stub pauseWork(); Bundle bundle = new Bundle(); bundle.putString("Data", "搜尋結果"); startSearch("Search contact list", true, bundle, false); return true; } private void findContact(String query) { // TODO Auto-generated method stub String selection = ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "'" + " AND " + ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME + " LIKE " + "'%" + query + "%'"; Cursor cursor = getContentResolver().query(ContactsContract.Data.CONTENT_URI, new String[] {ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME} , selection , null , null); int ColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); StringBuffer name = new StringBuffer(); if(cursor.getCount()>0) { while(cursor.moveToNext()) { name.append(cursor.getString(ColumnIndex)+"\n"); } } else name.append("無資料"); mTextView01.setText(name.toString()); } @Override protected void onPause() { // TODO Auto-generated method stub Log.d("Test","onPause()"); super.onPause(); } @Override protected void onResume() { // TODO Auto-generated method stub Log.d("Test","onResume()"); super.onResume(); } }
為了讓大家瞭解整個流程的LifeCycle 所以使用了Log來觀察整個流程, 從Log中可以看到, onCreate()只會被執行一次, 之後就會去call onNewIntent(Intent newIntent), 這全是因為 android:launchMode="singleTop" 的原因, 所以不會再去Create an instance了, 接著在onNewIntent() method中去判斷收到的intent 是不是我們要的Search intent 一切就大功告成了。
除此之外可以看到在onSearchRequseted()中加入了pauseWork() method, 這麼作的目的是因為當喚起了Search UI 之後, 就會自動的focus在 Search UI的輸入並且程式lifecycle 不會去call onPause(), 所以我們可能需要自己手動的暫停程式, 等到我們輸入搜尋字串或是自己取消Search UI之後再手動讓程式繼續執行, 接著我們藉由 Class SearchManager.OnDismissListener 的幫助來monitor Search的狀態, 因為當搜尋取消或是送Query字串後, SearchManager.OnDismissListener 是一定會被called, 所以我們將continuteWork() method 放到裡面等待執行。
除此之外可以看到在onSearchRequseted()中加入了pauseWork() method, 這麼作的目的是因為當喚起了Search UI 之後, 就會自動的focus在 Search UI的輸入並且程式lifecycle 不會去call onPause(), 所以我們可能需要自己手動的暫停程式, 等到我們輸入搜尋字串或是自己取消Search UI之後再手動讓程式繼續執行, 接著我們藉由 Class SearchManager.OnDismissListener 的幫助來monitor Search的狀態, 因為當搜尋取消或是送Query字串後, SearchManager.OnDismissListener 是一定會被called, 所以我們將continuteWork() method 放到裡面等待執行。
另外根據Android Developers的建議, 如果你開發的版本是在Android 3.0+ 的話, 建議是使用Search Widget , 這個部分有機會再發一篇文章介紹。
P.S. 題目中所要求的Variable和Method皆會保留 , 也會根據題目所要求的流程去實作 , 縱使題目要求繞遠路....
相關文章 : [Android] 很簡單的使用搜尋功能 Use Global Search
沒有留言:
張貼留言