Android 搜索框架使用詳解

搜索框架簡介

App中搜索功能是必不可少的,搜索功能可以幫助用戶快速獲取想要的信息。對此,Android提供瞭一個搜索框架,本文介紹如何通過搜索框架實現搜索功能。

Android 搜索框架提供瞭搜索彈窗和搜索控件兩種使用方式。

  • 搜索彈窗:系統控制的彈窗,激活後顯示在頁面頂部,輸入的內容提交後會通過Intent傳遞到指定的搜索Activity中處理,可以添加搜索建議。
  • 搜索控件(SearchView):系統實現的搜索控件,可以放在任意位置(可以與Toolbar結合使用),默認情況下與EditText類似,需要自己添加監聽處理用戶輸入的數據,通過配置可以達到與搜索彈窗一致的行為。

使用搜索框架實現搜索功能

可搜索配置

在res/xml目錄下創建searchable.xml(必須用此命名),如下:

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:hint="@string/search_hint"
    android:label="@string/app_name" />

android:label是此配置文件必須配置的屬性,通常配置為App的名字,android:hint配置用戶未輸入內容時的提示文案,官方建議格式為“搜索${content or product}”

更多可搜索配置包含的語法和用法可以看官方文檔。

搜索頁面

配置一個單獨的Activity用於顯示搜索內容,用戶可能會在搜索完一個內容後立刻搜索下一個內容,所以建議把搜索頁面設置為SingleTop,避免重復創建搜索頁面。

AndroidManifest中配置搜索頁面,如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        ... 
        >
        <activity
            android:name=".search.SearchActivity"
            android:exported="false"
            android:launchMode="singleTop">
            <meta-data
                android:name="android.app.searchable"
                android:resource="@xml/searchable" />
            <intent-filter>
                <action android:name="android.intent.action.SEARCH" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Activity中處理搜索數據,代碼如下:

class SearchActivity : AppCompatActivity() {
    private lateinit var binding: LayoutSearchActivityBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.layout_search_activity)
        // 當搜索頁面第一次打開時,獲取搜索內容
        getQueryKey(intent)
    }
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        // 更新Intent數據
        setIntent(intent)
        // 當搜索頁面多次打開,並仍在棧頂時,獲取搜索內容
        getQueryKey(intent)
    }
    private fun getQueryKey(intent: Intent?) {
        intent?.run {
            if (Intent.ACTION_SEARCH == action) {
                // 用戶輸入的內容
                val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
                if (queryKey.isNotEmpty()) {
                    doSearch(queryKey)
                }
            }
        }
    }
    private fun doSearch(queryKey: String) {
        // 根據用戶輸入內容執行搜索操作
    }
}

使用SearchView

SearchView可以放在頁面的任意位置,本文與Toolbar結合使用,如何在Toolbar中創建菜單項在上一篇文章中介紹過,此處省略。要使SearchView與搜索彈窗保持一致的行為需要在代碼中進行配置,如下:

class SearchActivity : AppCompatActivity() {
    private lateinit var binding: LayoutSearchActivityBinding
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.example_seach_menu, menu)
        menu?.run {
            val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
            val searchView = findItem(R.id.action_search).actionView as SearchView
            //設置搜索配置  
            searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
        }
        return true
    }
    ...
}

使用搜索彈窗

Activity中使用搜索彈窗,如果Activity已經配置為搜索頁面則無需額外配置,否則需要在AndroidManifest中添加配置,如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        ... 
        >
        <activity
            android:name=".search.SearchExampleActivity">
            <!--為當前頁面指定搜索頁面-->
            <!--如果所有頁面都使用搜索彈窗,則將此meta-data移到applicaion標簽下-->
            <meta-data
                android:name="android.app.default_searchable"
                android:value=".search.SearchActivity" />
        </activity>
    </application>
</manifest>

Activity中通過onSearchRequested方法來調用搜索彈窗,如下:

class SearchExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: LayoutSearchExampleActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_search_example_activity)
        binding.btnSearchDialog.setOnClickListener { onSearchRequested() }
    }
}

搜索彈窗對Activity生命周期的影響

搜索彈窗的顯示隱藏,不會像其他彈窗一樣觸發ActivityonPauseonResume方法。如果在搜索彈窗顯示隱藏的同時需要對其他功能進行處理,可以通過onSearchRequestedOnDismissListener來實現,代碼如下:

class SearchExampleActivity : AppCompatActivity() {
    override fun onSearchRequested(): Boolean {
        // 搜索彈窗顯示,可以在此處理其他功能
        return super.onSearchRequested()
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: LayoutSearchExampleActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_search_example_activity)
        binding.btnSearchDialog.setOnClickListener { onSearchRequested() }
        val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
        searchManager.setOnDismissListener {
            // 搜索彈窗隱藏,可以在此處理其他功能
        }
    }
}

附加額外的參數

使用搜索彈窗時,如果需要附加額外的參數用於優化搜索查詢的過程,例如用戶的性別、年齡等,可以通過如下代碼實現:

// 配置額外參數
class SearchExampleActivity : AppCompatActivity() {
    override fun onSearchRequested(): Boolean {
        val appData = Bundle()
        appData.putString("gender", "male")
        appData.putInt("age", 24)
        startSearch(null, false, appData, false)
        // 返回true表示已經發起瞭查詢
        return true
    }
    ...
}
// 在搜素頁面中獲取額外參數
class SearchActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)  
        intent?.run {
            if (Intent.ACTION_SEARCH == action) {
                // 用戶輸入的內容
                val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
                // 額外參數
                val appData = getBundleExtra(SearchManager.APP_DATA)
                appData?.run {
                    val gender = getString("gender") ?: ""
                    val age = getInt("age")
                }
            }
        }
    }
}

語音搜索

語音搜索讓用戶無需輸入內容就可進行搜索,要開啟語音搜索,需要在searchable.xml增加配置,如下:

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:hint="@string/search_hint"
    android:label="@string/app_name"
    android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />

語音搜索必須配置showVoiceSearchButton用於顯示語音搜索按鈕,配置launchRecognizer指定語音搜索按鈕啟動一個語音識別程序用於識別語音轉錄為文本並發送至搜索頁面。

更多語音搜索配置包含的語法和用法可以看官方文檔。

註意,語音識別後的文本會直接發送至搜索頁面,無法更改,需要進行完備的測試確保語音搜索功能適合你的App。

搜索記錄

用戶執行過搜索後,可以將搜索的內容保存下來,下次要搜索相同的內容時,輸入部分文字後就會顯示匹配的搜索記錄。

要實現此功能,需要完成下列步驟:

創建SearchRecentSuggestionsProvider

自定義RecentSearchProvider繼承SearchRecentSuggestionsProvider,代碼如下:

class RecentSearchProvider : SearchRecentSuggestionsProvider() {
    companion object {
        // 授權方的名稱(建議設置為文件提供者的完整名稱)
        const val AUTHORITY = "com.chenyihong.exampledemo.search.RecentSearchProvider"
        // 數據庫模式 
        // 必須配置 DATABASE_MODE_QUERIES 
        // 可選配置 DATABASE_MODE_2LINES,為搜索記錄提供第二行文本,可用於作為詳情補充
        const val MODE: Int = DATABASE_MODE_QUERIES or DATABASE_MODE_2LINES
    }
    init {
        // 設置搜索授權方的名稱與數據庫模式
        setupSuggestions(AUTHORITY, MODE)
    }
}

AndroidManifest中配置Provider,如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        ... 
        >
        <!--android:authorities的值與RecentSearchProvider中的AUTHORITY一致-->
        <provider
            android:name=".search.RecentSearchProvider"
            android:authorities="com.chenyihong.exampledemo.search.RecentSearchProvider"
            android:exported="false" />
    </application>
</manifest>

修改可搜索配置

searchable.xml增加配置,如下:

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:hint="@string/search_hint"
    android:label="@string/app_name"
    android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
    android:searchSuggestAuthority="com.chenyihong.exampledemo.search.RecentSearchProvider"
    android:searchSuggestSelection=" ?"/>

android:searchSuggestAuthority 的值與RecentSearchProvider中的AUTHORITY保持一致。android:searchSuggestSelection的值必須為" ?",該值為數據庫選擇參數的占位符,自動由用戶輸入的內容替換。

在搜索頁面中保存查詢

獲取到用戶輸入的數據時保存,代碼如下:

class SearchActivity : BaseGestureDetectorActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        intent?.run {
            if (Intent.ACTION_SEARCH == action) {
                val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
                if (queryKey.isNotEmpty()) {
                    // 第一個參數為用戶輸入的內容
                    // 第二個參數為第二行文本,可為null,僅當RecentSearchProvider.MODE為DATABASE_MODE_QUERIES or DATABASE_MODE_2LINES時有效。
                    SearchRecentSuggestions(this@SearchActivity, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
                        .saveRecentQuery(queryKey, "history $queryKey")
                }
            }
        }
    }
}

清除搜索歷史

為瞭保護用戶的隱私,官方的建議是App必須提供清除搜索記錄的功能。請求搜索記錄可以通過如下代碼實現:

class SearchActivity : BaseGestureDetectorActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        SearchRecentSuggestions(this, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
            .clearHistory()
    }
}

示例

整合之後做瞭個示例Demo,代碼如下:

// 可搜索配置
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:hint="@string/search_hint"
    android:label="@string/app_name"
    android:searchSuggestAuthority="com.chenyihong.exampledemo.search.RecentSearchProvider"
    android:searchSuggestSelection=" ?"
    android:voiceSearchMode="showVoiceSearchButton|launchRecognizer" />
// 清單文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        ...
        >
        <activity
            android:name=".search.SearchExampleActivity"
            android:screenOrientation="portrait">
            <!--為當前頁面指定搜索頁面-->
            <meta-data
                android:name="android.app.default_searchable"
                android:value=".search.SearchActivity" />
        </activity>
        <activity
            android:name=".search.SearchActivity"
            android:exported="false"
            android:launchMode="singleTop"
            android:parentActivityName="com.chenyihong.exampledemo.search.SearchExampleActivity"
            android:screenOrientation="portrait">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="com.chenyihong.exampledemo.search.SearchExampleActivity" />
            <meta-data
                android:name="android.app.searchable"
                android:resource="@xml/searchable" />
            <intent-filter>
                <action android:name="android.intent.action.SEARCH" />
            </intent-filter>
        </activity>
        <provider
            android:name=".search.RecentSearchProvider"
            android:authorities="com.chenyihong.exampledemo.search.RecentSearchProvider"
            android:exported="false" />
    </application>
</manifest>
// 示例Activity
class SearchExampleActivity : BaseGestureDetectorActivity() {
    override fun onSearchRequested(): Boolean {
        val appData = Bundle()
        appData.putString("gender", "male")
        appData.putInt("age", 24)
        startSearch(null, false, appData, false)
        return true
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: LayoutSearchExampleActivityBinding = DataBindingUtil.setContentView(this, R.layout.layout_search_example_activity)
        binding.btnSearchView.setOnClickListener { startActivity(Intent(this, SearchActivity::class.java)) }
        binding.btnSearchDialog.setOnClickListener { onSearchRequested() }
        val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
        searchManager.setOnDismissListener {
            runOnUiThread { Toast.makeText(this, "Search Dialog dismiss", Toast.LENGTH_SHORT).show() }
        }
    }
}
class SearchActivity : BaseGestureDetectorActivity() {
    private lateinit var binding: LayoutSearchActivityBinding
    private val textDataAdapter = TextDataAdapter()
    private val originData = ArrayList<String>()
    private var lastQueryValue = ""
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.example_seach_menu, menu)
        menu?.run {
            val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
            val searchView = findItem(R.id.action_search).actionView as SearchView
            searchView.setOnCloseListener {
                textDataAdapter.setNewData(originData)
                false
            }
            searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
            if (lastQueryValue.isNotEmpty()) {
                searchView.setQuery(lastQueryValue, false)
            }
        }
        return true
    }
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (item.itemId == R.id.action_clear_search_histor) {
            SearchRecentSuggestions(this, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
                .clearHistory()
        }
        return super.onOptionsItemSelected(item)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.layout_search_activity)
        setSupportActionBar(binding.toolbar)
        supportActionBar?.run {
            title = "SearchExample"
            setHomeAsUpIndicator(R.drawable.icon_back)
            setDisplayHomeAsUpEnabled(true)
        }
        binding.rvContent.adapter = textDataAdapter
        originData.add("test data qwertyuiop")
        originData.add("test data asdfghjkl")
        originData.add("test data zxcvbnm")
        originData.add("test data 123456789")
        originData.add("test data /.,?-+")
        textDataAdapter.setNewData(originData)
        // 獲取搜索內容
        getQueryKey(intent, false)
    }
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        // 更新Intent數據
        setIntent(intent)
        // 獲取搜索內容
        getQueryKey(intent, true)
    }
    private fun getQueryKey(intent: Intent?, newIntent: Boolean) {
        intent?.run {
            if (Intent.ACTION_SEARCH == action) {
                val queryKey = getStringExtra(SearchManager.QUERY) ?: ""
                if (queryKey.isNotEmpty()) {
                    SearchRecentSuggestions(this@SearchActivity, RecentSearchProvider.AUTHORITY, RecentSearchProvider.MODE)
                        .saveRecentQuery(queryKey, "history $queryKey")
                    if (!newIntent) {
                        lastQueryValue = queryKey
                    }
                    val appData = getBundleExtra(SearchManager.APP_DATA)
                    doSearch(queryKey, appData)
                }
            }
        }
    }
    private fun doSearch(queryKey: String, appData: Bundle?) {
        appData?.run {
            val gender = getString("gender") ?: ""
            val age = getInt("age")
        }
        val filterData = originData.filter { it.contains(queryKey) } as ArrayList<String>
        textDataAdapter.setNewData(filterData)
    }
}

ExampleDemo github

ExampleDemo gitee

效果如圖:

以上就是Android 搜索框架使用詳解的詳細內容,更多關於Android 搜索框架使用的資料請關註WalkonNet其它相關文章!

推薦閱讀: