💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 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 ![](https://img.kancloud.cn/e0/bf/e0bff67276f12dcd24df00500ccd9331_1224x607.png) 使用 `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 } ~~~ 效果: ![](https://img.kancloud.cn/b6/32/b632a210925431a686c810ccf1b06f26_365x587.png) 但是有个弊端就是需要输入两个字符才会有数据提示。这里通过反射来实现: ~~~ // 通过反射设置只要有一个文字就触发查询 val clazz = search.javaClass val field = clazz.getDeclaredField("mSearchSrcTextView") field.isAccessible = true val searchAutoComplete = field.get(search) as AutoCompleteTextView searchAutoComplete.threshold = 1 ~~~ 效果: ![](https://img.kancloud.cn/62/d9/62d96ee7038050f161b3e04128cc3662_306x257.png) 当然,也可以直接不使用默认的这个结果显示,自己写一个`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 }" ) } } ~~~ 但是呢,很不幸,结果如下: ![](https://img.kancloud.cn/78/36/783624883f1a5ce439218d3af6794fef_1025x174.png) 这里并没有获取到任何数据,所以这里需要再次查阅其源码: ~~~ 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) }" ) } } ~~~ 输入: ![](https://img.kancloud.cn/6e/f3/6ef30e5361bfefb700418380fba6d7b7_567x359.png) 然后随机点击一个: ![](https://img.kancloud.cn/d3/5b/d35ba9448a3dfef4deb1ab5053605c35_1030x213.png) 可以看见此时有效的只有`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 ~~~ 然后重新生成数据库即可。然后添加两天数据,再次测试: ![](https://img.kancloud.cn/5a/3a/5a3afd553a5782a88cd21b5973e26002_514x349.png) 再次随机点击一条,查看控制台打印信息: ![](https://img.kancloud.cn/69/d6/69d659a7cf7352a854e65ffa052e07e9_1024x187.png) 可以看到此时对应的`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)) } ~~~ 但是这里失败了: ![](https://img.kancloud.cn/fb/09/fb09c29316232c35648c6aeb10736bd7_1649x183.png) 这里也就懒得继续看源码来看如何解决了。这里就不修改了!!!直接修改逻辑为查询到数据,就跳转到一个新的`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 ~~~ 结果就可以做到返回后关闭键盘,清空搜索框,关闭搜索框。