(Kotlin)Android 高效底部导航方案:基于预定义 Menu 和 ViewPager2 的 Fragment 动态绑定实现

news2025/4/2 22:08:13

支持预定义 Menu 并绑定 Fragment,同时保留动态添加 Tab 的能力
BottomTabHelper.kt

package smartconnection.com.smartconnect.home.util

import android.content.Context
import android.util.SparseArray
import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.bottomnavigation.BottomNavigationView

/**
 * 增强版底部导航助手
 * 功能:
 * 1. 支持预定义 Menu 绑定 Fragment
 * 2. 保留动态添加/移除 Tab 能力
 * 3. 完善的 Fragment 生命周期管理
 * 4. 内置平滑过渡动画
 */
class BottomTabHelper private constructor(
    private val activity: FragmentActivity,
    @IdRes private val viewPagerId: Int,
    @IdRes private val bottomNavigationViewId: Int
) {
    private val viewPager: ViewPager2 by lazy { activity.findViewById(viewPagerId) }
    private val bottomNav: BottomNavigationView by lazy { activity.findViewById(bottomNavigationViewId) }
    
    private val fragmentCache = SparseArray<Fragment>()
    private var currentPosition = 0
    private var isSmoothScrollEnabled = true

    // Tab 配置数据类
    data class TabConfig(
        @IdRes val menuItemId: Int,
        val fragment: Fragment,
        val title: String? = null,
        val iconResId: Int? = null
    )

    init {
        initViewPager()
        initBottomNav()
    }

    private fun initViewPager() {
        viewPager.adapter = TabPagerAdapter(activity)
        viewPager.offscreenPageLimit = 1
        
        viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                currentPosition = position
                bottomNav.menu.getItem(position).isChecked = true
                triggerLazyLoad(position)
            }
        })
    }

    private fun initBottomNav() {
        bottomNav.setOnNavigationItemSelectedListener { item ->
            val position = getPositionForMenuItem(item.itemId)
            if (position != INVALID_POSITION) {
                viewPager.setCurrentItem(position, isSmoothScrollEnabled)
                true
            } else {
                false
            }
        }
    }

    /**
     * 绑定预定义 Menu 项到 Fragment
     */
    fun bindPredefinedTab(@IdRes menuItemId: Int, fragment: Fragment) {
        val position = getPositionForMenuItem(menuItemId)
        if (position != INVALID_POSITION) {
            fragmentCache.put(position, fragment)
            viewPager.adapter?.notifyItemChanged(position)
            
            // 如果是第一个绑定的 Tab,自动选中
            if (fragmentCache.size() == 1) {
                viewPager.setCurrentItem(0, false)
            }
        }
    }

    /**
     * 动态添加 Tab (可选)
     */
    fun addTab(config: TabConfig) {
        val newPosition = fragmentCache.size()
        
        // 添加到底部导航
        bottomNav.menu.add(0, config.menuItemId, newPosition, config.title ?: "").apply {
            config.iconResId?.let { setIcon(it) }
        }
        
        // 缓存 Fragment
        fragmentCache.put(newPosition, config.fragment)
        viewPager.adapter?.notifyItemInserted(newPosition)
    }

    private fun getPositionForMenuItem(menuItemId: Int): Int {
        val menu = bottomNav.menu
        for (i in 0 until menu.size()) {
            if (menu.getItem(i).itemId == menuItemId) {
                return i
            }
        }
        return INVALID_POSITION
    }

    private fun triggerLazyLoad(position: Int) {
        (fragmentCache[position] as? LazyLoadFragment)?.onLazyLoad()
    }

    fun setSmoothScrollEnabled(enabled: Boolean) {
        isSmoothScrollEnabled = enabled
    }

    fun getCurrentFragment(): Fragment? = fragmentCache[currentPosition]

    private inner class TabPagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
        override fun getItemCount(): Int = fragmentCache.size()
        
        override fun createFragment(position: Int): Fragment {
            return fragmentCache[position] 
                ?: throw IllegalStateException("Fragment not found at position $position")
        }
    }

    interface LazyLoadFragment {
        fun onLazyLoad()
    }

    companion object {
        private const val INVALID_POSITION = -1

        fun create(
            activity: FragmentActivity,
            @IdRes viewPagerId: Int,
            @IdRes bottomNavId: Int
        ): BottomTabHelper {
            return BottomTabHelper(activity, viewPagerId, bottomNavId)
        }
    }
}

使用说明
1. 预定义 Menu 使用方式
步骤 1:定义 Menu 资源

<!-- res/menu/bottom_nav_menu.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/nav_home"
        android:icon="@drawable/ic_home"
        android:title="Home"/>
    <item
        android:id="@+id/nav_search"
        android:icon="@drawable/ic_search"
        android:title="Search"/>
</menu>

步骤 2:在布局中绑定 Menu

<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottom_nav"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:menu="@menu/bottom_nav_menu" />

步骤 3:在 Activity 中绑定 Fragment

class MainActivity : AppCompatActivity() {
    private lateinit var tabHelper: BottomTabHelper

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tabHelper = BottomTabHelper.create(
            activity = this,
            viewPagerId = R.id.view_pager,
            bottomNavId = R.id.bottom_nav
        )

        // 绑定预定义 Menu 项到 Fragment
        tabHelper.bindPredefinedTab(R.id.nav_home, HomeFragment())
        tabHelper.bindPredefinedTab(R.id.nav_search, SearchFragment())
    }
}

2. 动态添加 Tab (可选)
需要提前在 res/values/ids.xml 中声明id

// 添加动态 Tab (会追加到预定义 Menu 后面)
tabHelper.addTab(
    BottomTabHelper.TabConfig(
        menuItemId = R.id.nav_profile, // 需要提前在 res/values/ids.xml 中声明
        fragment = ProfileFragment(),
        title = "Profile",
        iconResId = R.drawable.ic_profile
    )
)

功能特点
1.混合模式支持

同时支持预定义 Menu 和动态添加 Tab

自动处理位置映射关系

2.生命周期安全:

Fragment 由 ViewPager2 自动管理

支持 LazyLoadFragment 接口实现懒加载

3.配置灵活

可禁用平滑滚动:setSmoothScrollEnabled(false)

随时获取当前 Fragment:getCurrentFragment()

4.性能优化:

使用 SparseArray 存储 Fragment

默认只预加载相邻页面

5.扩展性强

通过 TabConfig 可扩展更多 Tab 属性

易于添加 Badge 等 Material Design 功能

最佳实践建议:
1.对于固定 Tab:使用预定义 Menu + bindPredefinedTab()

2.对于动态 Tab:使用 addTab()

3.需要懒加载:让 Fragment 实现 LazyLoadFragment 接口

4.修改默认动画:在 initBottomNav() 中添加自定义动画逻辑

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2326020.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI 驱动的安全分析的价值是什么?

作者&#xff1a;来自 Elastic Riya Juneja, Alyssa VanNice 与 Enterprise Strategy Group 一起量化经济影响 安全行业十分复杂&#xff0c;变化速度极快。攻击面、利益相关者需求、对手战术以及你使用的工具都在不断演变&#xff0c;导致许多安全团队不确定自己是否已做好准备…

AWE 2025:当AI科技遇见智能家居

3月20日&#xff0c;以“AI科技、AI生活”为主题的AWE2025&#xff08;中国家电及消费电子博览会&#xff09;在上海新国际博览中心开幕。作为全球家电行业风向标&#xff0c;本届展会最大的亮点莫过于健康理念在家电领域的全面渗透。从食材保鲜到空气净化&#xff0c;从衣物清…

win10之mysql server 8.0.41安装

一 mysql server 下载 官网下载地址页面 https://dev.mysql.com/downloads/mysql/二 免装版使用步骤 1 解压 下载完成后,解压文件夹,如下所示: 2 执行安装命令 D:\soft\mysql\mysql-8.0.41-winx64\mysql-8.0.41-winx64\bin>mysqld --install Service successfully in…

蓝桥杯专项复习——二分

目录 二分查找、二分答案基础知识 二分查找模版 【模版题】数的范围 借教室 二分查找、二分答案基础知识 二分查找模版 【模版题】数的范围 输入样例 6 3 1 2 2 3 3 4 3 4 5输出样例 3 4 5 5 -1 -1 思路&#xff1a; 对应两个模版&#xff0c;起始位置是对应第一个模版…

oracle中java类的使用

方式一&#xff1a; 编写一个简单的java类 vi OracleJavaDemo.java public class OracleJavaDemo { public static String processData(String input) { return "Processed: " input; } } 编译 javac OracleJavaDemo.java 生成OracleJavaDemo…

高并发内存池(一):项目介绍和Thread Cache实现

前言&#xff1a;本文将要介绍的高并发内存池&#xff0c;它的原型是Google的⼀个开源项⽬tcmalloc&#xff0c;全称Thread-Caching Malloc&#xff0c;近一个月我将以学习为目的来模拟实现一个精简版的高并发内存池&#xff0c;并对核心技术分块进行精细剖析&#xff0c;分享在…

MySQL与Redis数据一致性保障方案详解

前言 在现代分布式系统中&#xff0c;MySQL和Redis的结合使用非常普遍。MySQL作为关系型数据库负责持久化存储&#xff0c;而Redis则作为高性能缓存层提升系统的响应速度。然而&#xff0c;在这种架构下&#xff0c;如何保证MySQL与Redis之间的数据一致性是一个重要的挑战。本…

“钉耙编程”2025春季联赛(2)题解(更新中)

1001 学位运算导致的 1002 学历史导致的 // Problem: 学历史导致的 // Contest: HDOJ // URL: https://acm.hdu.edu.cn/contest/problem?cid1151&pid1002 // Memory Limit: 524288 MB // Time Limit: 1000 ms // // Powered by CP Editor (https://cpeditor.org)#include …

双向链表的理解

背景 代码中经常会出现双向链表&#xff0c;对于双向链表的插入和删除有对应的API函数接口&#xff0c;但直观的图表更容易理解&#xff0c;所以本文会对rt-thread内核代码中提供的双向链表的一些API函数操作进行绘图&#xff0c;方便后续随时查看。 代码块 rt-thread中提供…

基于Spring Boot的家庭理财系统app的设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

【Python 算法】动态规划

本博客笔记内容来源于灵神&#xff0c;视频链接如下&#xff1a;https://www.bilibili.com/video/BV16Y411v7Y6?vd_source7414087e971fef9431117e44d8ba61a7&spm_id_from333.788.player.switch 01背包 计算了f[i1]&#xff0c;f[i]就没用了&#xff0c;相当于每时每刻只有…

nginx的自定义错误页面

正常访问一个不存在的页面是会报404这个错误 我们可以自定义错误页面 error_page 404 /40x.html 然后调用location 最后创建文件 写入你想写的内容 最后实验成功 注意在nginx的配置文件里&#xff0c;注意在加分号 在写完配置时

制作service列表并打印出来

制作service列表并打印出来 在Linux中&#xff0c;服务&#xff08;Service&#xff09;是指常驻在内存中的进程&#xff0c;这些进程通常监听某个端口&#xff0c;等待其他程序的请求。服务也被称为守护进程&#xff08;Daemon&#xff09;&#xff0c;它们提供了系统所需的各…

tkinter 库(设计图形界面系统)

几何管理的应用 # tkinter 库 是Python的标准GUI库&#xff0c;提供了创建图形用户界面的功能。 tkinter是一个跨平台的GUI库&#xff0c;支持Windows、macOS和Linux等操作系统。它是Python的标准库之一&#xff0c;无需额外安装。 #tkinter.Entry 是 Tkinter 的输入框控件类&…

在线文档协作工具选型必看:14款产品对比

本文将深入对比14款在线文档协作工具&#xff1a;PingCode; 2. Worktile; 3. 语雀; 4. 金山文档; 5. WPS云文档; 6. Google Docs; 7. 轻雀文档; 8. Microsoft 365 Online; 9. 明道云文档等。 在数字化办公日益普及的今天&#xff0c;企业对高效协同的需求不断升级&#xff0c;在…

Java虚拟机JVM知识点(持续更新)

JVM内存模型 介绍下内存模型 根据JDK8的规范&#xff0c;我们的JVM内存模型可以拆分为&#xff1a;程序计数器、Java虚拟机栈、堆、元空间、本地方法栈&#xff0c;还有一部分叫直接内存&#xff0c;属于操作系统的本地内存&#xff0c;也是可以直接操作的。 详细解释一下 程…

【计算机网络】HTTP与HTTPS

文章目录 1. HTTP定义2. HTTP交互3. HTTP报文格式3.1 抓包工具-fiddler3.2 抓包操作3.3 报文格式3.3.1 请求报文3.3.2 响应报文 4. URL5. 请求头中的方法6. GET和POST的区别7. HTTP报头7.1 Host7.2 Content_Length7.3 Content_Type7.4 User-Agent(UA)7.5 Referer7.6 Cookie 8 状…

数据结构:树的5种存储方案详解(C语言完整实现)

数据结构中的树结构常用来存储逻辑关系为 "一对多" 的数据。树结构可以细分为两类&#xff0c;分别是二叉树和非二叉树&#xff08;普通树&#xff09;&#xff0c;存储它们的方案是不一样的&#xff1a; 二叉树的存储方案有 2 种&#xff0c;既可以用顺序表存储二叉…

【蓝桥杯】 枚举和模拟练习题

系列文章目录 蓝桥杯例题 枚举和模拟 文章目录 系列文章目录前言一、好数&#xff1a; 题目参考&#xff1a;核心思想&#xff1a;代码实现&#xff1a; 二、艺术与篮球&#xff1a; 题目参考&#xff1a;核心思想&#xff1a;代码实现: 总结 前言 今天距离蓝桥杯还有13天&…

WebGL图形编程实战【3】:矩阵操控 × 从二维到三维的跨越

上一篇文章&#xff1a;WebGL图形编程实战【2】&#xff1a;动态着色 纹理贴图技术揭秘 仓库地址&#xff1a;github…、gitee… 矩阵操控 矩阵变换 回到前面关于平移缩放、旋转的例子当中&#xff0c;我们是通过改变传递进去的xy的值来改变的。 在进行基础变换的时候&…