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

作用如图: