# 1. 簡單使用
可以直接結合`Toolbar`使用,也就是在添加的`item.xml`中直接使用,比如:
~~~
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/bt2"
android:title="@string/note_page_description"
android:icon="@drawable/ic_baseline_search_24"
app:actionViewClass="android.widget.SearchView"
app:showAsAction="always" />
<item
android:id="@+id/bt3"
android:title="@string/note_page_setting"
android:icon="@drawable/ic_baseline_settings_24"
app:showAsAction="ifRoom"/>
</menu>
~~~
然后可以簡單設置一下顯示格式。
參考:https://blog.csdn.net/jaynm/article/details/107172544

使用 `SearchView` 時可使用如下常用方法。
- `setIconifiedByDefault(Boolean iconified)`:設置該搜索框默認是否自動縮小為圖標。
- `setSubmitButtonEnabled(Boolean enabled)`:設置是否顯示搜索按鈕。
- `setQueryHint(CharSequence hint)`:設置搜索框內默認顯示的提示文本。
- `setOnQueryTextListener(SearchView.OnQueryTextListener listener)`:為該搜索框設置事件監聽器。
比如:
~~~
// 添加Toolbar菜單欄按鈕
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar_item, menu)
// 找到搜索框
val item = menu?.findItem(R.id.bt2)
search = item?.actionView as SearchView
// 設置搜索框
search.let {
it.isSubmitButtonEnabled = true
it.queryHint = "請輸入關鍵字"
it.imeOptions = EditorInfo.IME_ACTION_DONE
it.maxWidth = 800
}
return true
}
~~~
然后可以返回結果列表:
根據`setSuggestionsAdapter()`方法可以實現輸入搜索內容,自動提示的功能。`setSuggestionsAdapter(CursorAdapter adapter)`方法需要一個 `CursorAdapter` 參數,這里看到` Cursor`,很多人就應該清楚,`Cursor` 光標或者游標。正常情況下這里應該采用 `Cursor` 操作數據庫,可以實現查詢篩選功能。
# 2. 搜索提示功能
一般開發中遇到的需求是:輸入關鍵字就顯示搜索結果,所以需要監聽搜索框的文字輸入,一旦文字變化就查詢數據庫,更新搜索結果。這里為了模擬,創建一個數據庫。這里使用`Room`框架來構建,也就是三步:
- 創建表對應的實體類;
- 創建`Dao`層操作接口類;
- 創建繼承自`RoomDatabase`的數據庫`Dao`層接口獲取類;
至于更多`Room`操作細節,可以查看我的看云的`Kotlin`筆記處。這里不再給出。當然,對應于這里我們需要模糊查詢,這里給出一個示例:
~~~
@Query("select * from MFNote where (title like '%' || :words || '%') or (content like '%' || :words || '%') or (first_submit like '%' || :words || '%') or (last_modify like '%' || :words || '%') or (label like '%' || :words || '%') or (category like '%' || :words || '%') LIMIT 10")
fun getNodesByKeyWords(words: String): Cursor
~~~
這里沒有指定具體類型`List<MFNote>`,因為我們這里需要一個`Cursor`對象。然后為查詢后顯示的`item`創建布局文件(`search_item_layout.xml`):
~~~
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:background="@color/white"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/search_item_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="主標題"
android:textStyle="bold"
android:padding="10dp"
android:textColor="@color/black"
android:gravity="center_vertical|start"
>
</TextView>
<TextView
android:id="@+id/search_item_subTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:text="副標題"
android:paddingStart="10dp"
android:paddingBottom="10dp"
android:paddingEnd="10dp"
android:maxEms="20"
android:singleLine="true"
android:ellipsize="end"
android:textColor="@color/gray"
android:gravity="center_vertical|start"
tools:ignore="RtlSymmetry">
</TextView>
</LinearLayout>
~~~
然后找到這個`SearchView`,進行設置:
~~~
// 添加Toolbar菜單欄按鈕
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar_item, menu)
// 找到搜索框
val item = menu?.findItem(R.id.bt2)
search = item?.actionView as SearchView
// 設置搜索框
search.let {
it.isSubmitButtonEnabled = true
it.queryHint = "請輸入關鍵字"
it.imeOptions = EditorInfo.IME_ACTION_DONE
it.maxWidth = 800
}
// 設置輸入監聽函數
search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
if(mfDao == null){
mfDao = MFNoteDataBase.getInstance(this@MainActivity)?.mfDao()
}
val cursor = mfDao?.getNodesByKeyWords(newText?:"測")
Log.e("TAG", "onQueryTextChange: ${newText}, cursor count : ${cursor?.count}" )
if(search.suggestionsAdapter == null){
val cursorAdapter = SimpleCursorAdapter(this@MainActivity,
R.layout.search_item_layout, cursor,
listOf<String>("title", "content").toTypedArray(), listOf<Int>(R.id.search_item_tv, R.id.search_item_subTitle).toIntArray()
) as CursorAdapter
search.suggestionsAdapter = cursorAdapter
} else{
search.suggestionsAdapter.changeCursor(cursor)
}
return false
}
})
return true
}
~~~
效果:

但是有個弊端就是需要輸入兩個字符才會有數據提示。這里通過反射來實現:
~~~
// 通過反射設置只要有一個文字就觸發查詢
val clazz = search.javaClass
val field =
clazz.getDeclaredField("mSearchSrcTextView")
field.isAccessible = true
val searchAutoComplete = field.get(search) as AutoCompleteTextView
searchAutoComplete.threshold = 1
~~~
效果:

當然,也可以直接不使用默認的這個結果顯示,自己寫一個`ListView`,然后進行監聽`SearchView`的數據變化,自己來渲染在`xml`中寫入的`ListView`即可。這里不采用這種方式,所以這里不再給出案例。
# 3. 搜索提示監聽
當然,上面的功能還沒完,我們還需要響應點擊事件。這里就需要查閱官方文檔:[創建搜索界面 ?|? Android 開發者 ?|? Android Developers](https://developer.android.com/guide/topics/search/search-dialog)
## 3.1 配置xml文件
首先需要的是一個名為可搜索配置的 `XML `文件。名為`searchable.xml`,并且必須保存在`res/xml/`項目目錄中。
> 注意:系統使用此文件來實例化`[SearchableInfo](https://developer.android.com/reference/android/app/SearchableInfo)`對象,但您無法在運行時自行創建此對象,您必須在 `XML` 中聲明可搜索配置。比如:
~~~
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search"
android:label="@string/title">
</searchable>
~~~
## 3.2 創建可搜索 Activity
因為這里我只是在`MainActivity`中有搜索框`SearchView`,所以這里我也只能配置在`MainActivity`來接受結果。比如:
~~~
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
~~~
1. 在[`<intent-filter>`](https://developer.android.com/guide/topics/manifest/intent-filter-element)元素中聲明要接受[`ACTION_SEARCH`](https://developer.android.com/reference/android/content/Intent#ACTION_SEARCH)`intent` 的 `Activity`。
2. 在[`<meta-data>`](https://developer.android.com/guide/topics/manifest/meta-data-element)元素中指定要使用的可搜索配置。
## 3.3 配置關聯
需要為每個[`SearchView`](https://developer.android.com/reference/android/widget/SearchView)啟用輔助搜索。為此,您可以調用[`setSearchableInfo()`](https://developer.android.com/reference/android/widget/SearchView#setSearchableInfo(android.app.SearchableInfo))并向其傳遞表示可搜索配置的[`SearchableInfo`](https://developer.android.com/reference/android/app/SearchableInfo)對象。
~~~
// 響應搜索列表點擊
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
search.apply {
setSearchableInfo(searchManager.getSearchableInfo(componentName))
}
~~~
## 3.4 顯示搜索結果
在可搜索 `Activity` 中執行搜索涉及三個步驟:
1. [接收查詢](https://developer.android.com/guide/topics/search/search-dialog#ReceivingTheQuery)
2. [搜索數據](https://developer.android.com/guide/topics/search/search-dialog#SearchingYourData)
3. [顯示結果](https://developer.android.com/guide/topics/search/search-dialog#PresentingTheResults)
比如下面的代碼:
~~~
override fun onCreate(savedInstanceState: Bundle?) {
? ? ? ? super.onCreate(savedInstanceState)
? ? ? ? setContentView(R.layout.search)
? ? ? ? // Verify the action and get the query
? ? ? ? if (Intent.ACTION_SEARCH == intent.action) {
? ? ? ? ? ? intent.getStringExtra(SearchManager.QUERY)?.also { query ->
? ? ? ? ? ? ? ? doMySearch(query)
? ? ? ? ? ? }
? ? ? ? }
? ? }
~~~
更多細節可以查閱官方文檔:[設置搜索界面 ?|? Android 開發者 ?|? Android Developers](https://developer.android.com/training/search/setup)
因為這里所有的控件都是在`MainActivity`中,所以這里需要代碼設置部分顯示和部分不顯示。而且我們需要設置這個`Activity`的啟動模式:
```
android:launchMode="singleTop"
```
然后,在[`onNewIntent()`](https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent))方法中處理[`ACTION_SEARCH`](https://developer.android.com/reference/android/content/Intent#ACTION_SEARCH)`intent`。
比如:
~~~
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
processSearchData(intent)
}
fun processSearchData(intent: Intent?){
if(intent?.action == Intent.ACTION_SEARCH){
val stringExtra = intent.extras?.get(SearchManager.QUERY)
Log.e("TAG", "processSearchData: ${ stringExtra }" )
}
}
~~~
但是呢,很不幸,結果如下:

這里并沒有獲取到任何數據,所以這里需要再次查閱其源碼:
~~~
private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
try {
// use specific action if supplied, or default action if supplied, or fixed default
String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
if (action == null) {
action = mSearchable.getSuggestIntentAction();
}
if (action == null) {
action = Intent.ACTION_SEARCH;
}
// use specific data if supplied, or default data if supplied
String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
if (data == null) {
data = mSearchable.getSuggestIntentData();
}
// then, if an ID was provided, append it.
if (data != null) {
String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
if (id != null) {
data = data + "/" + Uri.encode(id);
}
}
Uri dataUri = (data == null) ? null : Uri.parse(data);
String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
} catch (RuntimeException e ) {
int rowNum;
try { // be really paranoid now
rowNum = c.getPosition();
} catch (RuntimeException e2 ) {
rowNum = -1;
}
Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
" returned exception.", e);
return null;
}
}
~~~
以及:
~~~
private Intent createIntent(String action, Uri data, String extraData, String query,
int actionKey, String actionMsg) {
// Now build the Intent
Intent intent = new Intent(action);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// We need CLEAR_TOP to avoid reusing an old task that has other activities
// on top of the one we want. We don't want to do this in in-app search though,
// as it can be destructive to the activity stack.
if (data != null) {
intent.setData(data);
}
intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
if (query != null) {
intent.putExtra(SearchManager.QUERY, query);
}
if (extraData != null) {
intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
}
if (mAppSearchData != null) {
intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
}
if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
intent.putExtra(SearchManager.ACTION_KEY, actionKey);
intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
}
intent.setComponent(mSearchable.getSearchActivity());
return intent;
}
~~~
根據上面源碼可以很容易知道放置進去的鍵值對,故而這里可以多測試幾組:
~~~
fun processSearchData(intent: Intent?){
if(intent?.action == Intent.ACTION_SEARCH){
Log.e("TAG", "QUERY: ${ intent.extras?.get(SearchManager.QUERY) }" )
Log.e("TAG", "USER_QUERY: ${ intent.extras?.get(SearchManager.USER_QUERY) }" )
Log.e("TAG", "EXTRA_DATA_KEY: ${ intent.extras?.get(SearchManager.EXTRA_DATA_KEY) }" )
Log.e("TAG", "ACTION_KEY: ${ intent.extras?.get(SearchManager.ACTION_KEY) }" )
Log.e("TAG", "ACTION_MSG: ${ intent.extras?.get(SearchManager.ACTION_MSG) }" )
}
}
~~~
輸入:

然后隨機點擊一個:

可以看見此時有效的只有`USER_QUERY`。故而這里還需要看源碼,看看如何設置。可以看見:
~~~
// Cursor c
String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
~~~
而這個方法來自:
~~~
import static android.widget.SuggestionsAdapter.getColumnString;
~~~
其方法細節為:
~~~
public static String getColumnString(Cursor cursor, String columnName) {
int col = cursor.getColumnIndex(columnName);
return getStringOrNull(cursor, col);
}
~~~
也就是從`cursor`中獲取指定名字的列的數據。所以我們只需要確保查詢到的數據中有`SearchManager.SUGGEST_COLUMN_QUERY`這么一列。而這個值為:
~~~
public final static String SUGGEST_COLUMN_QUERY = "suggest_intent_query";
~~~
這里由于我使用的是`Room`所以這里我直接在`Entity`類中添加一個屬性:
~~~
@ColumnInfo(name = "suggest_intent_query")
var query: Int = 0
~~~
然后重新生成數據庫即可。然后添加兩天數據,再次測試:

再次隨機點擊一條,查看控制臺打印信息:

可以看到此時對應的`QUERY`字段就有值了。由于我們可能需要通過這個字段來查詢數據,進而顯示詳細的數據,所以這里需要其每個記錄的唯一,所以后面可能需要修改為`Long`,來存儲毫秒數。
# 3.5 重新修改邏輯
如果均在`MainActivity`顯示主要內容,以及顯示搜索結果,那么就需要設置很多的顯示和隱藏,比如:
~~~
// 進入顯示搜索結果的時候
fun enterSearchResultView(search_result_ll: LinearLayout){
search_result_ll.visibility = View.VISIBLE
swiperefreshLayout.visibility = View.GONE
bottom_tab.visibility = View.GONE
floatingactionbutton.visibility = View.GONE
}
~~~
但是,感覺這樣太麻煩了,所以這里我預期將搜索結果顯示放置在另外一個`Activity`中。首先修改一下清單文件:
~~~
<activity android:name=".MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".activitys.TestActivity">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
~~~
然后,需要關聯對應的`TestActivity`文件:
~~~
// 響應搜索列表點擊
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
search.apply {
setSearchableInfo(searchManager.getSearchableInfo(componentName))
}
~~~
但是,`componentName`是在當前`Activity`的`this`中獲取到的,這里我無法直接獲取到`TestActivity`的`componentName`,所以直接手動創建一個對應的對象。
~~~
// 響應搜索列表點擊
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
search.apply {
val search_componentName = ComponentName("com.weizu.myapplication.activitys", "com.weizu.myapplication.activitys.TestActivity")
setSearchableInfo(searchManager.getSearchableInfo(search_componentName))
}
~~~
但是這里失敗了:

這里也就懶得繼續看源碼來看如何解決了。這里就不修改了!!!直接修改邏輯為查詢到數據,就跳轉到一個新的`Activity`:
~~~
fun processSearchData(intent: Intent?){
if(intent?.action == Intent.ACTION_SEARCH){
// 用戶點擊的數據在數據庫表MFNote對應的suggest_intent_query的值
val suggest_intent_query = intent.getStringExtra(SearchManager.QUERY)
Log.e("TAG", "QUERY: ${suggest_intent_query}")
// 直接跳轉到顯示Activity
startActivity(Intent().setClass(this, TestActivity::class.java))
}
}
~~~
# 3.6 清除搜索框文本
~~~
fun processSearchData(intent: Intent?){
if(intent?.action == Intent.ACTION_SEARCH){
// 用戶點擊的數據在數據庫表MFNote對應的suggest_intent_query的值
val suggest_intent_query = intent.getStringExtra(SearchManager.QUERY)
// 直接跳轉到顯示Activity
if(!TextUtils.isEmpty(suggest_intent_query)){
// 清除搜索框文本,關閉鍵盤,關閉搜索框
searchAutoComplete?.setText("")
searchAutoComplete?.clearFocus()
val clazz = search.javaClass
val declaredMethod = clazz.getDeclaredMethod("onCloseClicked")
declaredMethod.setAccessible(true);
declaredMethod.invoke(search)
val searchResultsIntent = Intent()
searchResultsIntent.apply {
setClass(this@MainActivity, TestActivity::class.java)
putExtra("suggest_intent_query", suggest_intent_query)
}
startActivity(searchResultsIntent)
}
}
}
~~~
至于`searchAutoComplete`來自反射:
~~~
// 通過反射設置只要有一個文字就觸發查詢
val clazz = search.javaClass
val field =
clazz.getDeclaredField("mSearchSrcTextView")
field.isAccessible = true
if(searchAutoComplete == null){
searchAutoComplete = field.get(search) as AutoCompleteTextView
}
searchAutoComplete?.threshold = 1
~~~
結果就可以做到返回后關閉鍵盤,清空搜索框,關閉搜索框。
- 介紹
- UI
- MaterialButton
- MaterialButtonToggleGroup
- 字體相關設置
- Material Design
- Toolbar
- 下拉刷新
- 可折疊式標題欄
- 懸浮按鈕
- 滑動菜單DrawerLayout
- NavigationView
- 可交互提示
- CoordinatorLayout
- 卡片式布局
- 搜索框SearchView
- 自定義View
- 簡單封裝單選
- RecyclerView
- xml設置點擊樣式
- adb
- 連接真機
- 小技巧
- 通過字符串ID獲取資源
- 自定義View組件
- 使用系統控件重新組合
- 旋轉菜單
- 輪播圖
- 下拉輸入框
- 自定義VIew
- 圖片組合的開關按鈕
- 自定義ViewPager
- 聯系人快速索引案例
- 使用ListView定義側滑菜單
- 下拉粘黏效果
- 滑動沖突
- 滑動沖突之非同向沖突
- onMeasure
- 繪制字體
- 設置畫筆Paint
- 貝賽爾曲線
- Invalidate和PostInvalidate
- super.onTouchEvent(event)?
- setShadowLayer與陰影效果
- Shader
- ImageView的scaleType屬性
- 漸變
- LinearGradient
- 圖像混合模式
- PorterDuffXfermode
- 橡皮擦效果
- Matrix
- 離屏繪制
- Canvas和圖層
- Canvas簡介
- Canvas中常用操作總結
- Shape
- 圓角屬性
- Android常見動畫
- Android動畫簡介
- View動畫
- 自定義View動畫
- View動畫的特殊使用場景
- LayoutAnimation
- Activity的切換轉場效果
- 屬性動畫
- 幀動畫
- 屬性動畫監聽
- 插值器和估值器
- 工具
- dp和px的轉換
- 獲取屏幕寬高
- JNI
- javah命令
- C和Java相互調用
- WebView
- Android Studio快捷鍵
- Bitmap和Drawable圖像
- Bitmap簡要介紹
- 圖片縮放和裁剪效果
- 創建指定顏色的Bitmap圖像
- Gradle本地倉庫
- Gradle小技巧
- RxJava+Okhttp+Retrofit構建網絡模塊
- 服務器相關配置
- node環境配置
- 3D特效