Android 如何实现搜索功能:本地搜索?数据模型如何设计?数据如何展示和保存?
目录
- 效果图
- 为什么需要搜索功能
- 如何设计搜索本地的功能,如何维护呢?
- 总结
一、效果图
二、为什么需要搜索功能
找一个选项,需要花非常多的时间,并且每次都需要指导客户在哪里,现在只要让他们搜索一下就可以。这也是模仿手机里面的设置功能来进行开发的。这些选项我是存储在本地的。参数太多,暂时还没考虑做到后台,当然,即使后面做到后台,也只是替换数据而已。
三、如何设计搜索本地的功能,如何维护呢?
我们可以看到效果图:
- 有开关类的、有输入类的。
- 有分类:系统设置、串口设置、功能开启等等。
- 有默认值,如:123456,默认是关的等等。
- …
so,我们需要设计数据模型。
3.1 设计数据模型
大家可以思考一下为什么数据模型要这样设计?有默认值,有key,有nameId
/**
* nameId:显示名称,这里存储的是rid,方便后续国际化
* nameS:名称
* category:分类
* drfault:默认值
* type:类型 1 多选项值 2 开关 3 输入类
* mmkvName:保存key
*/
enum class OtherEnum(var nameId:Int,var nameS:String,var category:String,var drfault:Any,var type:Int,var mmkvName: String) {
MDB(R.string.base_two_code,"通讯协议","串口设置","MDB", 2,MMKVName.MDB),
CHANGE(R.string.base_nayax,"找零功能","找零设置",false, 1,MMKVName.CHANGE),
CONTACT_NUMBER(R.string.leak_canary_test_class_name,"联系方式","系统设置","123456", 2,MMKVName.CONTACT_NUMBER),
}
3.2 Repo类
这里只是增加了一个要展示的数据,后面如果把数据放在了后端,也就只是替换这一部分就可以。
class OtherFragmentRepo @Inject constructor() : BaseRepository() {
var arrayList: MutableList<OtherEnum> = Arrays.asList(//用于列表展示
OtherEnum.MDB,
OtherEnum.CHANGE,
OtherEnum.CONTACT_NUMBER,
)
}
如果有新增的数据,只需要在这里增加就可以了。这样也非常好维护,所以需要数据模型设置好来。
3.3 VM:主要是提供搜索的功能
- search方法其实就是遍历所有的集合元素,找到匹配的内容,存放到一个list里面进行展示,通过_readAllDataSuccess进行数据通知界面刷新。因为数据量较小,不到100个左右,所以这里使用的for循环遍历。
class OtherFragmentVM @Inject constructor(private val mRepo: OtherFragmentRepo) : BaseViewModel() {
//搜索功能【通信设置、系统设置、功能开启等等】
//1. 首先我们需要先添加我们的所有设置。【通过一个bean来存储,一个list来存储所有的】
//bean:名称,分类,类型【开关类、输入类、多值类、音量调节】,value
//举例:优惠券、功能开启、开关、false
//2. 首先把所有的功能项拿到,进行遍历
//3. 具体的数据展示:如何展示呢?navigation+fragment??【搜索的时候展示另外一个fragment,而不搜索的时候展示其中一个。】
//1.拿到所有的通讯设置
var list :ArrayList<OtherEnum> = ArrayList()
private var _readAllDataSuccess = MutableLiveData<Int>()//是否读取所有数据成功
val readAllDataSuccess: LiveData<Int> get() = _readAllDataSuccess
//2.搜索功能
fun search(searchText: CharSequence) {
list.clear()
val arrayList = mRepo.arrayList
for (otherEnum in arrayList) {
val stringRes = UiUtil.getStringRes(otherEnum.nameId).uppercase()
if (stringRes.contains(searchText.toString().uppercase())) {
list.add(otherEnum)
}
}
_readAllDataSuccess.value = list.size
}
}
3.4 Fragment
- 搜索功能:先判断是否输入内容为空,如果为空就提示用户。符合条件就调用search进行模糊匹配搜索。搜索到以后,就将默认显示的navigation进行GONE,将搜索结果进行VISIBLE
class OtherFragment : BaseFragment<BackstageFragmentOtherBinding, OtherFragmentVM>() {
private val TAG = "OtherFragment"
override val mViewModel: OtherFragmentVM by viewModels()
override fun createVB() = BackstageFragmentOtherBinding.inflate(layoutInflater)
private var otherAdapter: OtherAdapter? = null
override fun BackstageFragmentOtherBinding.initView() {
Log.d(TAG, "OtherFragment initView: ")
ivSearch.setOnClickListener {
val searchText = etSearch.text.trim()
Log.d(TAG, "searchText: "+searchText)
if(""==searchText){
ToastUtil.switchToastStyleToWarn("请输入内容后搜索")
return@setOnClickListener
}
//进行数据的搜索
mViewModel.search(searchText)
}
with(rvOtherSearch){
//设置布局排列方式,默认垂直排列
val gridLayoutManager: GridLayoutManager =
GridLayoutManager(this@OtherFragment.context, 2, androidx.recyclerview.widget.GridLayoutManager.VERTICAL, false)
layoutManager = gridLayoutManager
mViewModel.list.add(OtherEnum.CHANGE)
otherAdapter = OtherAdapter(mViewModel.list)
otherAdapter!!.setItemListener(object : AdapterClickListener {
override fun onClickListener(view: View?, position: Int, data: String?) {
}
})
adapter = otherAdapter
}
}
override fun initObserve() {
observeLiveData(mViewModel.readAllDataSuccess,::searchResult)
}
fun searchResult(i: Int) {
if (i==0) {
ToastUtil.switchToastStyleToWarn("搜索结果为空")
mBinding.rvOtherSearch.visibility = View.GONE
mBinding.fcvOther.visibility = View.VISIBLE
return
}
mBinding.rvOtherSearch.visibility = View.VISIBLE
mBinding.fcvOther.visibility = View.GONE
Log.d(TAG, "searchResult: "+mViewModel.list)
otherAdapter?.setData(mViewModel.list)
otherAdapter?.notifyDataSetChanged()
}
override fun initRequestData() {
}
}
3.5 xml:UI应该如何编写呢?
UI方面:
(1)最简单的:一个输入框;一个搜索按钮;一个recycleview展示搜索结果;一个navigation+FragmentContainerView展示默认的内容,也就是不搜索的时候全部展示。
(2)扩展:1. 搜索历史,热门搜索,输入的时候补全提示。后面可以增加。我们先实现最简单的。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="@drawable/no_nav_bg"
android:layout_height="match_parent">
<net.lucode.hackware.magicindicator.MagicIndicator
android:id="@+id/magic_other"
android:layout_width="match_parent"
android:layout_height="88dp"
android:background="@drawable/backstage_shape_product_nav"
android:paddingLeft="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_search"
android:layout_width="1000dp"
android:layout_height="80dp"
android:background="@drawable/home_other_rectangle_background"
android:paddingLeft="20dp"
android:maxLines="1"
android:inputType="text"
android:layout_marginVertical="10dp"
android:hint="@string/backstage_search_hint"
android:textColor="#2E80DD"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.51"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/magic_other" />
<ImageView
android:id="@+id/iv_search"
android:layout_width="90dp"
android:layout_height="90dp"
android:padding="10dp"
android:layout_marginLeft="10dp"
android:src="@drawable/search"
app:layout_constraintBottom_toBottomOf="@+id/et_search"
app:layout_constraintStart_toEndOf="@+id/et_search"
app:layout_constraintTop_toTopOf="@+id/et_search" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fcv_other"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_search"
app:navGraph="@navigation/backstage_other_nav" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_other_search"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_search" />
</androidx.constraintlayout.widget.ConstraintLayout>
3.6 Adapter:这里我们需要思考子项不一样应该如何设计,保存也数据逻辑也不一样!
- 子项不一样,我们应该如何处理呢?我们可以看到otherEnum里面有一个type属性,就是用于定义不同的布局的,如下:
- 那么开关类的保存,输入类的保存有应该如何呢?只需要使用不同的布局进行不同的逻辑进行处理就可以。如下:
class OtherAdapter(var productList: MutableList<OtherEnum>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val TAG = "HomeProductAdapter"
var SWITCH_TYPE = 1
var INPUT_TYPE = 2
fun setItemListener(itemListener: AdapterClickListener?) {
this.itemListener = itemListener
}
private var itemListener: AdapterClickListener? = null
private var generalParamData: MutableList<String>? = null
//开关类布局
inner class MyViewHolder(binding: BackstageItemSystemSettingsBinding) :
ViewHolder(binding.root) {
private val mBinding = binding
fun bind(otherEnum: OtherEnum) {
mBinding.run {
Log.d(TAG, "MyViewHolder bind: " + otherEnum)
tvName.text = UiUtil.getStringRes(productList[layoutPosition].nameId)
//开关类的数据保存
rgSwitch.setOnCheckedChangeListener { group, checkedId ->
when (checkedId) {
R.id.rb_close -> {
SpUtils.putBoolean(otherEnum.mmkvName,false)
}
R.id.rb_open -> {
SpUtils.putBoolean(otherEnum.mmkvName,true)
}
else -> {}
}
}
var flag = SpUtils.getBoolean(otherEnum.mmkvName,otherEnum.drfault as Boolean)
if(flag == true){
rbOpen.isChecked = true
}else{
rbClose.isChecked = true
}
}
}
}
//输入类的布局
inner class InputViewHolder(binding: BackstageItemInputBinding) : ViewHolder(binding.root) {
private val mBinding = binding
fun bind(otherEnum: OtherEnum) {
mBinding.run {
Log.d(TAG, "InputViewHolder bind: " +otherEnum.mmkvName+":"+ SpUtils.contains(otherEnum.mmkvName))
tvName.text = UiUtil.getStringRes(productList[layoutPosition].nameId)
var defaultValue = SpUtils.getString(otherEnum.mmkvName,otherEnum.drfault as String)
etValue.setText(defaultValue)
//输入类的数据保存
btnUpdate.setOnClickListener {
val value = etValue.text.trim().toString()
if(value==""){
ToastUtil.switchToastStyleToWarn("输入为空")
return@setOnClickListener
}
SpUtils.putString(otherEnum.mmkvName,value)
ToastUtil.switchToastStyleToSuccess("更新成功:"+otherEnum.mmkvName)
}
}
}
}
override fun getItemViewType(position: Int): Int {
//不同的类型,使用不同的布局
return productList[position].type
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
when (viewType) {
SWITCH_TYPE -> {
return MyViewHolder(
BackstageItemSystemSettingsBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
INPUT_TYPE -> {
Log.d(TAG, "onCreateViewHolder: InputViewHolder:"+viewType)
return InputViewHolder(
BackstageItemInputBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
else -> {
}
}
return MyViewHolder(
BackstageItemSystemSettingsBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun getItemCount(): Int {
return productList.size
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is MyViewHolder) {
holder.bind(productList.get(position))
} else if (holder is InputViewHolder) {
holder.bind(productList.get(position))
}
itemListener?.onClickListener(holder.itemView, position, null)
}
fun setData(productList: MutableList<OtherEnum>) {
this.productList = productList
}
}
四、总结:
其实搜索功能的重点在于数据模型的设计,还有apdater布局的设置。以前都是一个一个控件的增加,数据也可以直接就增加,所以维护很模仿,现在换成了recycleview,所以我们需要思考每一个item,他的数据应该如何展示,默认值是如何,在哪个分类,如何保存数据。