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

支持预定义 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() 中添加自定义动画逻辑

相关推荐
阿巴斯甜17 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker18 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952719 小时前
Andorid Google 登录接入文档
android
黄林晴20 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android