Android 底部 Tab 导航终极指南:封装 BottomTabHelper 实现高效、灵活的 Tab 布局
在 Android 开发中,底部 Tab 导航是常见的 UI 设计模式。本文将带你从零开始,封装一个高复用性的 BottomTabHelper
工具类,结合 BottomNavigationView
和 ViewPager2
,实现高效、灵活的底部 Tab 导航功能。你将学到:
- 如何封装
BottomTabHelper
工具类,简化 Tab 导航的实现。 - 支持动态 Tab、Badge 提示、懒加载和 Tab 选中监听等高级功能。
- 通过代码示例和布局文件,快速上手并应用到实际项目中。
1. BottomTabHelper 工具类
import android.app.Activity;
import android.util.SparseArray;
import android.view.View;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.badge.BadgeDrawable;
/**
* 底部 Tab 导航工具类
* 功能:封装 BottomNavigationView 和 ViewPager2 的联动逻辑,支持懒加载、动态 Tab、Badge 提示和自定义配置
*/
public class BottomTabHelper {
private final Activity activity;
private final ViewPager2 viewPager;
private final BottomNavigationView bottomNavigationView;
private final SparseArray<Fragment> fragmentCache = new SparseArray<>(); // Fragment 缓存
private final SparseArray<String> tabTitles = new SparseArray<>(); // Tab 标题缓存
private OnTabSelectedListener onTabSelectedListener; // Tab 选中监听器
/**
* 构造函数
*
* @param activity Activity 实例
* @param viewPagerId ViewPager2 的 ID
* @param bottomNavigationViewId BottomNavigationView 的 ID
*/
public BottomTabHelper(@NonNull Activity activity, @IdRes int viewPagerId, @IdRes int bottomNavigationViewId) {
this.activity = activity;
this.viewPager = activity.findViewById(viewPagerId);
this.bottomNavigationView = activity.findViewById(bottomNavigationViewId);
init();
}
/**
* 初始化方法
*/
private void init() {
// 设置 ViewPager2 的适配器
viewPager.setAdapter(new ViewPagerAdapter((FragmentActivity) activity));
// 禁用预加载(只保留当前页面和相邻页面)
viewPager.setOffscreenPageLimit(1);
// 监听 ViewPager2 的页面切换
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
// 同步 BottomNavigationView 的选中状态
bottomNavigationView.getMenu().getItem(position).setChecked(true);
// 触发 Fragment 的懒加载
Fragment fragment = fragmentCache.get(position);
if (fragment instanceof LazyLoadFragment) {
((LazyLoadFragment) fragment).onLazyLoad();
}
// 触发 Tab 选中监听
if (onTabSelectedListener != null) {
onTabSelectedListener.onTabSelected(position, tabTitles.get(position));
}
}
});
// 监听 BottomNavigationView 的 Tab 切换
bottomNavigationView.setOnNavigationItemSelectedListener(item -> {
int position = getPositionByItemId(item.getItemId());
if (position != -1) {
viewPager.setCurrentItem(position, false); // 切换到对应页面,禁用滑动动画
return true;
}
return false;
});
}
/**
* 根据菜单项 ID 获取位置
*/
private int getPositionByItemId(int itemId) {
for (int i = 0; i < bottomNavigationView.getMenu().size(); i++) {
if (bottomNavigationView.getMenu().getItem(i).getItemId() == itemId) {
return i;
}
}
return -1;
}
/**
* 设置 Tab 切换动画(可选)
*/
public void setupTabAnimation() {
bottomNavigationView.setOnNavigationItemSelectedListener(item -> {
// 获取当前选中的 Tab 视图
View tabView = bottomNavigationView.findViewById(item.getItemId());
if (tabView != null) {
// 缩放动画
ObjectAnimator scaleX = ObjectAnimator.ofFloat(tabView, "scaleX", 1f, 1.2f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(tabView, "scaleY", 1f, 1.2f, 1f);
scaleX.setDuration(300).start();
scaleY.setDuration(300).start();
}
return true;
});
}
/**
* 动态添加 Tab
*
* @param menuItemId 菜单项 ID
* @param title Tab 标题
* @param fragment Fragment 实例
*/
public void addTab(int menuItemId, String title, Fragment fragment) {
bottomNavigationView.getMenu().add(0, menuItemId, 0, title); // 添加菜单项
fragmentCache.put(bottomNavigationView.getMenu().size() - 1, fragment); // 缓存 Fragment
tabTitles.put(bottomNavigationView.getMenu().size() - 1, title); // 缓存标题
viewPager.getAdapter().notifyItemInserted(bottomNavigationView.getMenu().size() - 1); // 刷新适配器
}
/**
* 设置 Badge 提示
*
* @param position Tab 位置
* @param count 提示数量
*/
public void setBadge(int position, int count) {
BadgeDrawable badge = bottomNavigationView.getOrCreateBadge(bottomNavigationView.getMenu().getItem(position).getItemId());
badge.setNumber(count);
badge.setVisible(count > 0);
}
/**
* 设置 Tab 选中监听器
*/
public void setOnTabSelectedListener(OnTabSelectedListener listener) {
this.onTabSelectedListener = listener;
}
/**
* Tab 选中监听器接口
*/
public interface OnTabSelectedListener {
void onTabSelected(int position, String title);
}
/**
* ViewPager2 的适配器
*/
private class ViewPagerAdapter extends FragmentStateAdapter {
public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
@NonNull
@Override
public Fragment createFragment(int position) {
Fragment fragment = fragmentCache.get(position);
if (fragment == null) {
throw new IllegalArgumentException("Fragment not found for position: " + position);
}
return fragment;
}
@Override
public int getItemCount() {
return bottomNavigationView.getMenu().size(); // Tab 的数量
}
}
}
2. LazyLoadFragment 基类
import android.os.Bundle;
import androidx.fragment.app.Fragment;
/**
* 支持懒加载的 Fragment 基类
*/
public abstract class LazyLoadFragment extends Fragment {
private boolean isLoaded = false; // 是否已加载
@Override
public void onResume() {
super.onResume();
if (!isLoaded) {
onLazyLoad();
isLoaded = true;
}
}
/**
* 懒加载回调方法
*/
public abstract void onLazyLoad();
}
3. Fragment 实现
HomeFragment
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class HomeFragment extends LazyLoadFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_home, container, false);
}
@Override
public void onLazyLoad() {
// 懒加载逻辑(例如加载数据)
}
}
SearchFragment
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class SearchFragment extends LazyLoadFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_search, container, false);
}
@Override
public void onLazyLoad() {
// 懒加载逻辑(例如加载数据)
}
}
ProfileFragment
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class ProfileFragment extends LazyLoadFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_profile, container, false);
}
@Override
public void onLazyLoad() {
// 懒加载逻辑(例如加载数据)
}
}
4. Activity 使用示例
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity implements BottomTabHelper.OnTabSelectedListener {
private BottomTabHelper bottomTabHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化 BottomTabHelper
bottomTabHelper = new BottomTabHelper(this, R.id.viewPager, R.id.bottomNavigationView);
// 设置 Tab 切换动画(可选)
bottomTabHelper.setupTabAnimation();
// 动态添加 Tab(可选)
bottomTabHelper.addTab(R.id.nav_settings, "Settings", new SettingsFragment());
// 设置 Badge 提示(可选)
bottomTabHelper.setBadge(1, 5); // 在第二个 Tab 上显示提示
// 设置 Tab 选中监听器
bottomTabHelper.setOnTabSelectedListener(this);
}
@Override
public void onTabSelected(int position, String title) {
// 更新 Activity 标题
setTitle(title);
}
}
5. 布局文件
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- ViewPager2 -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<!-- BottomNavigationView -->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_nav_menu"/>
</LinearLayout>
bottom_nav_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<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"/>
<item
android:id="@+id/nav_profile"
android:icon="@drawable/ic_profile"
android:title="Profile"/>
</menu>
fragment_home.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Home Fragment"
android:textSize="24sp"/>
</LinearLayout>
fragment_search.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Search Fragment"
android:textSize="24sp"/>
</LinearLayout>
fragment_profile.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Profile Fragment"
android:textSize="24sp"/>
</LinearLayout>
6. 总结
通过以上代码和布局文件,你可以快速实现一个功能强大且灵活的底部 Tab 导航功能。BottomTabHelper
工具类封装了核心逻辑,支持动态 Tab、Badge 提示、懒加载和 Tab 选中监听等功能,适合大多数应用场景。
如果有其他问题,欢迎继续提问!