You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ViewPager多Tab下多RecyclerView全局搜索实现方案咨询

当然可以实现!你想要的全局搜索+单个RecyclerView展示跨Tab结果的方案完全可行,我给你梳理一下具体的实现思路和关键步骤,适配你每个Tab都有独立Adapter、Model的场景:

核心思路

因为每个Tab的Model结构不同,直接合并数据会很混乱,所以我们需要做两件核心事情:

  1. 用一个统一的搜索结果封装类,把不同Tab的Model和类型标识打包在一起;
  2. 实现一个支持多类型的RecyclerView Adapter,根据封装类里的类型标识,加载对应Tab的Item布局并绑定数据。

这样单个RecyclerView就能完美展示来自不同Tab的搜索结果了。

具体实现步骤

1. 创建统一的搜索结果封装类

这个类的作用是把不同Tab的Model“包裹”起来,同时标记它的来源类型,让后续的Adapter能识别该用哪种布局展示。

// 枚举定义所有Tab的数据源类型,对应你的7个Tab
enum class SearchItemType {
    TYPE_TAB_ARTICLE, TYPE_TAB_VIDEO, TYPE_TAB_USER, 
    TYPE_TAB_COMMENT, TYPE_TAB_TAG, TYPE_TAB_COLLECTION, TYPE_TAB_NOTIFICATION
}

// 统一的搜索结果类
data class SearchResult(
    val itemType: SearchItemType,
    val originalModel: Any, // 持有对应Tab的原始Model对象
    val sourceTabTitle: String? = null // 可选:显示结果来自哪个Tab,提升用户体验
)

2. 实现全局搜索逻辑

搜索时需要遍历所有Tab的Adapter数据集,匹配符合关键词的项,然后封装成SearchResult列表。这里有两种方案:

  • 提前缓存数据:如果数据量不大,可以在每个Tab数据加载完成后,把Model同步到一个全局列表,搜索时直接遍历这个列表;
  • 实时遍历Adapter:如果数据量较大或者实时更新频繁,搜索时直接获取每个Tab Adapter的当前数据集进行匹配。

下面是实时遍历的示例代码(假设你持有每个Tab Adapter的引用):

private fun performGlobalSearch(keyword: String): List<SearchResult> {
    val searchResults = mutableListOf<SearchResult>()

    // 遍历所有Tab的Adapter,这里替换成你实际的Adapter引用和类型映射
    val adapterList = listOf(
        Pair(articleAdapter, SearchItemType.TYPE_TAB_ARTICLE),
        Pair(videoAdapter, SearchItemType.TYPE_TAB_VIDEO),
        Pair(userAdapter, SearchItemType.TYPE_TAB_USER),
        // ... 剩下的4个Tab的Adapter和类型
    )

    adapterList.forEach { (adapter, itemType) ->
        when (adapter) {
            is ArticleAdapter -> {
                // 获取当前Adapter的数据集,假设Adapter有getDataset()方法
                adapter.getDataset().forEach { model ->
                    // 根据业务规则匹配关键词,比如标题、内容包含关键词
                    if (model.title.contains(keyword, ignoreCase = true) 
                        || model.content.contains(keyword, ignoreCase = true)) {
                        searchResults.add(SearchResult(itemType, model, "文章"))
                    }
                }
            }
            is VideoAdapter -> {
                adapter.getDataset().forEach { model ->
                    if (model.videoName.contains(keyword, ignoreCase = true)) {
                        searchResults.add(SearchResult(itemType, model, "视频"))
                    }
                }
            }
            // ... 其他Tab的Adapter匹配逻辑
        }
    }

    return searchResults
}

3. 实现支持多类型的搜索结果Adapter

这个Adapter需要根据SearchResult的itemType,返回不同的布局类型,创建对应的ViewHolder并绑定数据:

class GlobalSearchAdapter(private val resultList: List<SearchResult>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    // 定义不同类型对应的布局ID
    companion object {
        private const val VIEW_TYPE_ARTICLE = 1
        private const val VIEW_TYPE_VIDEO = 2
        private const val VIEW_TYPE_USER = 3
        // ... 其他类型的布局ID
    }

    override fun getItemViewType(position: Int): Int {
        return when (resultList[position].itemType) {
            SearchItemType.TYPE_TAB_ARTICLE -> VIEW_TYPE_ARTICLE
            SearchItemType.TYPE_TAB_VIDEO -> VIEW_TYPE_VIDEO
            SearchItemType.TYPE_TAB_USER -> VIEW_TYPE_USER
            // ... 其他类型的映射
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return when (viewType) {
            VIEW_TYPE_ARTICLE -> ArticleResultViewHolder(inflater.inflate(R.layout.item_article, parent, false))
            VIEW_TYPE_VIDEO -> VideoResultViewHolder(inflater.inflate(R.layout.item_video, parent, false))
            VIEW_TYPE_USER -> UserResultViewHolder(inflater.inflate(R.layout.item_user, parent, false))
            else -> throw IllegalArgumentException("未知的视图类型")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val result = resultList[position]
        when (holder) {
            is ArticleResultViewHolder -> {
                // 强转成对应的Model
                val articleModel = result.originalModel as ArticleModel
                // 绑定数据到布局
                holder.titleTv.text = articleModel.title
                holder.descTv.text = articleModel.content.substring(0, 50) + "..."
                holder.sourceTv.text = result.sourceTabTitle
            }
            is VideoResultViewHolder -> {
                val videoModel = result.originalModel as VideoModel
                holder.videoNameTv.text = videoModel.videoName
                holder.durationTv.text = videoModel.duration
                holder.sourceTv.text = result.sourceTabTitle
            }
            // ... 其他类型的绑定逻辑
        }
    }

    override fun getItemCount() = resultList.size

    // 各个类型对应的ViewHolder
    class ArticleResultViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val titleTv: TextView = itemView.findViewById(R.id.tv_title)
        val descTv: TextView = itemView.findViewById(R.id.tv_desc)
        val sourceTv: TextView = itemView.findViewById(R.id.tv_source)
    }

    class VideoResultViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val videoNameTv: TextView = itemView.findViewById(R.id.tv_video_name)
        val durationTv: TextView = itemView.findViewById(R.id.tv_duration)
        val sourceTv: TextView = itemView.findViewById(R.id.tv_source)
    }
    // ... 其他ViewHolder
}

4. 实现搜索Dialog

用Android自带的SearchView结合AlertDialog快速实现搜索弹窗,监听用户输入并触发搜索:

private fun showGlobalSearchDialog() {
    val searchView = SearchView(this)
    searchView.queryHint = "搜索文章、视频、用户..."
    // 调整SearchView的样式,比如去掉默认的下划线
    searchView.setBackgroundColor(Color.WHITE)

    val dialog = AlertDialog.Builder(this)
        .setTitle("全局搜索")
        .setView(searchView)
        .setNegativeButton("取消") { _, _ -> }
        .create()

    // 监听搜索输入
    searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
        override fun onQueryTextSubmit(query: String?): Boolean {
            query?.trim()?.let {
                // 执行搜索,注意要放在子线程避免阻塞UI
                CoroutineScope(Dispatchers.IO).launch {
                    val results = performGlobalSearch(it)
                    // 切换回主线程更新UI
                    withContext(Dispatchers.Main) {
                        updateSearchResultsRecyclerView(results)
                        dialog.dismiss()
                    }
                }
            }
            return true
        }

        override fun onQueryTextChange(newText: String?): Boolean {
            // 可选:实现实时搜索,用户输入时立即更新结果
            newText?.trim()?.let {
                if (it.isNotEmpty()) {
                    CoroutineScope(Dispatchers.IO).launch {
                        val results = performGlobalSearch(it)
                        withContext(Dispatchers.Main) {
                            updateSearchResultsRecyclerView(results)
                        }
                    }
                } else {
                    // 输入为空时清空结果
                    updateSearchResultsRecyclerView(emptyList())
                }
            }
            return true
        }
    })

    dialog.show()
}

// 更新搜索结果RecyclerView的方法
private fun updateSearchResultsRecyclerView(results: List<SearchResult>) {
    val adapter = GlobalSearchAdapter(results)
    searchResultRecyclerView.adapter = adapter
    // 如果结果为空,可以显示空状态布局
    if (results.isEmpty()) {
        emptyStateLayout.visibility = View.VISIBLE
        searchResultRecyclerView.visibility = View.GONE
    } else {
        emptyStateLayout.visibility = View.GONE
        searchResultRecyclerView.visibility = View.VISIBLE
    }
}
关键细节优化
  • 性能优化:如果数据量很大,一定要把搜索逻辑放在子线程(比如用Coroutines、AsyncTask),避免卡顿;
  • 数据同步:如果原Tab的RecyclerView数据有更新(比如下拉刷新、加载更多),要确保搜索时能获取到最新的数据,要么同步更新全局缓存,要么实时从Adapter取数;
  • 类型安全:如果用Kotlin,推荐用sealed class代替enum + Any的组合,能避免强转时的类型错误;
  • 点击事件:搜索结果的点击事件可以根据SearchResult的类型,跳转到对应的Tab并定位到原Item,或者直接打开详情页,提升用户体验。

内容的提问来源于stack exchange,提问作者Sadegh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 09:08:11