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