Android 底部 Tab 导航终极指南:封装 BottomTabHelper 实现高效、灵活的 Tab 布局

embedded/2025/3/13 18:42:17/

在 Android 开发中,底部 Tab 导航是常见的 UI 设计模式。本文将带你从零开始,封装一个高复用性的 BottomTabHelper 工具类,结合 BottomNavigationViewViewPager2,实现高效、灵活的底部 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() {@Overridepublic 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); // 缓存 FragmenttabTitles.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@Overridepublic Fragment createFragment(int position) {Fragment fragment = fragmentCache.get(position);if (fragment == null) {throw new IllegalArgumentException("Fragment not found for position: " + position);}return fragment;}@Overridepublic 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; // 是否已加载@Overridepublic 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 {@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {return inflater.inflate(R.layout.fragment_home, container, false);}@Overridepublic void onLazyLoad() {// 懒加载逻辑(例如加载数据)}
}
SearchFragment
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;public class SearchFragment extends LazyLoadFragment {@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {return inflater.inflate(R.layout.fragment_search, container, false);}@Overridepublic void onLazyLoad() {// 懒加载逻辑(例如加载数据)}
}
ProfileFragment
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;public class ProfileFragment extends LazyLoadFragment {@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {return inflater.inflate(R.layout.fragment_profile, container, false);}@Overridepublic void onLazyLoad() {// 懒加载逻辑(例如加载数据)}
}

4. Activity 使用示例

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity implements BottomTabHelper.OnTabSelectedListener {private BottomTabHelper bottomTabHelper;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// 初始化 BottomTabHelperbottomTabHelper = 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);}@Overridepublic 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.ViewPager2android:id="@+id/viewPager"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"/><!-- BottomNavigationView --><com.google.android.material.bottomnavigation.BottomNavigationViewandroid: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"><itemandroid:id="@+id/nav_home"android:icon="@drawable/ic_home"android:title="Home"/><itemandroid:id="@+id/nav_search"android:icon="@drawable/ic_search"android:title="Search"/><itemandroid: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"><TextViewandroid: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"><TextViewandroid: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"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Profile Fragment"android:textSize="24sp"/>
</LinearLayout>

6. 总结

通过以上代码和布局文件,你可以快速实现一个功能强大且灵活的底部 Tab 导航功能。BottomTabHelper 工具类封装了核心逻辑,支持动态 Tab、Badge 提示、懒加载和 Tab 选中监听等功能,适合大多数应用场景。

如果有其他问题,欢迎继续提问!


http://www.ppmy.cn/embedded/172323.html

相关文章

深入浅出 IndexedDB:浏览器的 NoSQL 数据库

在现代 Web 开发中&#xff0c;前端数据存储的需求越来越复杂。无论是需要离线访问的应用&#xff0c;还是需要缓存大量数据的场景&#xff0c;传统的 LocalStorage 和 SessionStorage 已经无法满足需求。这时&#xff0c;IndexedDB 作为一种强大的浏览器端 NoSQL 数据库&#…

从零构建CNN:框架与自定义实现对比

文章目录 引言项目结构一、代码结构解析1.1 训练流程控制 (main.py)1.2 PyTorch实现的CNN模型 (cnn_pytorch.py)1.3 自定义实现CNN模型 (cnn_custom.py) 二、关键算法细节剖析2.1 卷积操作2.2 自定义实现卷积层2.3 ReLU与池化2.4 全连接层 总结 引言 卷积神经网络 (Convolutio…

AJAX的作用

AJAX&#xff08;Asynchronous JavaScript And XML&#xff09;的工作原理基于浏览器与服务器的异步通信&#xff0c;其核心细节可分为以下几个关键步骤&#xff1a; 1. 事件触发与请求创建 触发源&#xff1a;用户操作&#xff08;点击按钮、输入文本等&#xff09;或定时事件…

【每日八股】Redis篇(七):集群

目录 Redis 集群模式有哪些&#xff1f;Redis 切片集群的工作原理&#xff1f;哈希槽和 Redis 节点如何对应&#xff1f;主从模式的同步过程&#xff1f;全量同步增量同步 主服务器如何知道要将哪些增量数据发送给从服务器&#xff1f;如何避免主从数据不一致&#xff1f;主从架…

每日算法:力扣343.整数差分(动态规划)

题目&#xff1a; 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 示例 1: 输入: n 2 输出: 1 解释: 2 1 1, 1 1 1。 示例 2: 输入: n 10 输出…

3.11记录

leetcode刷题&#xff1a; 1. 334. 递增的三元子序列 - 力扣&#xff08;LeetCode&#xff09; 方法一&#xff1a;使用贪心算法求解 class Solution(object):def increasingTriplet(self, nums):first nums[0]second float(inf)for i in nums:if i>second:return Truee…

c++20 Concepts的简写形式与requires 从句形式

c20 Concepts的简写形式与requires 从句形式 原始写法&#xff08;简写形式&#xff09;等效写法&#xff08;requires 从句形式&#xff09;关键区别说明&#xff1a;组合多个约束的示例&#xff1a;两种形式的编译结果&#xff1a;更复杂的约束示例&#xff1a;标准库风格的约…

【网络编程】WSAAsyncSelect 模型

十、基于I/O模型的网络开发 接着上次的博客继续分享&#xff1a;select模型 10.8 异步选择模型WSAAsyncSelect 10.8.1 基本概念 WSAAsyncSelect模型是Windows socket的一个异步I/O 模型&#xff0c;利用这个模型&#xff0c;应用程序 可在一个套接字上接收以Windows 消息为基…