【Android】使用ViewPager2与TabLayout实现顶部导航栏+页面切换
TabLayout与ViewPager2概述
TabLayout
TabLayout
是 Android 支持库中的一个组件,它是 Design
支持库的一部分。TabLayout
提供了一个水平的标签页界面,允许用户在不同的视图或数据集之间进行切换。以下是 TabLayout
的一些主要特性和使用方法
主要特性
- 标签页模式:
TabLayout
可以显示多个标签页,每个标签页可以包含文本、图标或两者兼有。 - 动态标签页:可以动态地添加、删除或重新排序标签页。
- 与
ViewPager
结合使用:TabLayout
可以与ViewPager
无缝集成,使得用户在滑动ViewPager
的页面时,TabLayout
的选中标签页也会相应地变化。 - 自定义标签页:可以自定义标签页的布局、样式和行为。
- 滚动标签页:当标签页数量超过屏幕宽度时,它们可以水平滚动。
官方文档:
TabLayout | Android Developers (google.cn)
ViewPager2
ViewPager2
是 Android Jetpack 库中的一个组件,它是 ViewPager
的一个改进版本,提供了更好的性能和更多的功能。ViewPager2
允许用户左右滑动来浏览不同的视图,类似于一个滑动页面的控件。
主要特性
- 更好的性能:
ViewPager2
优化了滑动性能,特别是在处理大量页面或复杂视图时。 - 垂直和水平滑动:支持水平和垂直滑动,而
ViewPager
仅支持水平滑动。 - 动态添加和删除页面:可以动态地添加、删除或重新排序页面。
- 与
TabLayout
集成:可以与TabLayout
集成,实现标签页和页面视图的联动。 - 自定义适配器:支持自定义适配器,可以灵活地控制页面内容的显示。
- 滑动事件监听:提供滑动事件的监听,可以获取用户滑动的详细信息。
官方文档:
ViewPager2 | Jetpack | Android Developers (google.cn)
使用 ViewPager2 创建包含标签的滑动视图 | Android Developers (google.cn)
示例展示
本篇博客,笔者将带大家实现这样一个标题栏+Fragment水平切换页面的案例。
步骤详解
创建Activity并加入TabLayout和ViewPager2组件
创建Activity的步骤不再赘述,这里放上写好的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"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"android:orientation="vertical"style="@style/LayoutStyle"><com.google.android.material.tabs.TabLayoutandroid:id="@+id/tabLayout"android:layout_width="match_parent"android:layout_height="wrap_content"style="@style/TabStyle"/><androidx.viewpager2.widget.ViewPager2android:id="@+id/viewPager"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="horizontal" /></LinearLayout>
顺便展示一下Tab的样式配置:
<style name="TabStyle"><item name="tabGravity">fill</item><item name="tabIndicatorFullWidth">false</item><item name="tabIndicatorAnimationMode">elastic</item><item name="tabMode">fixed</item><item name="tabUnboundedRipple">false</item><item name="tabBackground">@color/white</item><item name="tabTextAppearance">@style/TabTextStyle</item><item name="tabIndicator">@drawable/shape_tab_indicator</item><item name="tabIndicatorColor">@color/text_change_color</item><item name="tabTextColor">@color/black</item><item name="tabSelectedTextColor">@color/text_change_color</item></style>
tabGravity
: 这个属性设置Tab的对齐方式,fill
表示Tab将填充整个TabLayout的宽度。tabIndicatorFullWidth
: 设置指示器是否占据整个Tab的宽度,false
表示指示器不会占据整个Tab的宽度。tabIndicatorAnimationMode
: 指示器动画模式,elastic
表示弹性动画效果。tabMode
: 设置Tab的模式,fixed
表示Tab的数量是固定的。tabUnboundedRipple
: 是否允许未绑定的涟漪效果,false
表示不允许。tabBackground
: 设置Tab的背景颜色,这里使用了颜色资源@color/white
。tabTextAppearance
: 设置Tab文本的样式,这里引用了一个样式资源@style/TabTextStyle
。tabIndicator
: 设置Tab指示器的形状,这里引用了一个可绘制资源@drawable/shape_tab_indicator
。tabIndicatorColor
: 设置Tab指示器的颜色,使用了颜色资源@color/text_change_color
。tabTextColor
: 设置Tab未选中时的文本颜色,使用了颜色资源@color/black
。tabSelectedTextColor
: 设置Tab选中时的文本颜色,同样使用了颜色资源@color/text_change_color
。
仅作样式展示,大家喜欢的话可以直接cv走,如果想要自定义可以自行去网络上搜索配置属性,或者是查看源码。
创建需要的Fragments
下一步,我们创建需要的Fragments,这里仅演示一下标题栏的写法,故而Fragment就简单一些。
简简单单,一个Fragment。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".TextFragment"><TextViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:text="人尔 女子"android:textSize="40dp"/></LinearLayout>
大概长这样:
创建FragmentAdapter(关联Fragment和ViewPager2)
接下来我们来写FragmentAdapter。
首先创建一个类,使他继承FragmentStateAdapter。
FragmentStateAdapter
是 Android 开发中的一部分,它是一个适配器类,用于管理与 Fragment 相关的数据集合。FragmentStateAdapter
继承自FragmentActivity.Callbacks
和FragmentManager.FragmentLifecycleCallbacks
,因此它可以接收到 Fragment 生命周期的回调,并根据数据的变化来管理 Fragment 的状态。
FragmentStateAdapter
通常与ViewPager2
一起使用,为ViewPager2
提供 Fragment。
刚继承完就爆红了,不要慌,alt+enter重写几个方法。
FragmentStateAdapter要求我们重写createFragment方法和getItemCount方法,
createFragment(int position)
:- 这个方法用于创建并返回一个
Fragment
对象,对应于ViewPager2
中的每个页面。 position
参数表示当前页面的位置(索引),从0开始。- 你需要重写这个方法来提供具体的
Fragment
实例。
- 这个方法用于创建并返回一个
getItemCount()
:- 这个方法返回
ViewPager2
中页面的数量。 - 你需要重写这个方法来指定你的
ViewPager2
应该有多少个页面。
- 这个方法返回
先创建一个管理fragment的list成员变量,用于管理fragment页面,然后填充几个函数即可,别忘了写一个新的构造方法。
代码如下:
public class FragmentAdapter extends FragmentStateAdapter {private List<Fragment> fragmentList;public FragmentAdapter(@NonNull FragmentActivity fragmentActivity, List<Fragment> fragmentList) {super(fragmentActivity);this.fragmentList = fragmentList;}@NonNull@Overridepublic Fragment createFragment(int position) {return fragmentList == null ? null : fragmentList.get(position);}@Overridepublic int getItemCount() {return fragmentList == null ? 0:fragmentList.size();}
}
为了防止空指针异常,我们在每次返回时,判断一次fragment是否为空。
将ViewPager2配置好
配置ViewPager2的步骤也不复杂,首先回到MainActivity中,把刚刚写好的适配器和用于管理的List列表写出来:
private FragmentAdapter fragmentAdapter;
private List<Fragment> fragmentList;
然后将适配器实例化,再设置给viewPager组件即可,完整代码如下:
public class MainActivity extends AppCompatActivity {private ActivityMainBinding binding;private FragmentAdapter fragmentAdapter;private List<Fragment> fragmentList;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);EdgeToEdge.enable(this);binding = ActivityMainBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);return insets;});initData();fragmentAdapter = new FragmentAdapter(this,fragmentList);binding.viewPager.setAdapter(fragmentAdapter);}private void initData() {fragmentList = new ArrayList<>();fragmentList.add(new TextFragment());fragmentList.add(new TextFragment());fragmentList.add(new TextFragment());}
}
省去实例化Fragment的过程,仅仅两行代码就完成配置了,真是便便又捷捷呀。
我们不妨点进去这个ViewPager2的setAdapter方法,看看他都干了些什么。在网上搜索ViewPager2和ViewPager的区别的时候,总能看到一句话:两者最大的区别就是可以直接把ViewPager2看成RecyclerView,先放下这些疑惑,我们点进去看看:
点进去第一眼就是这个非常显眼的RecyclerView,真是封封又装装啊。
来都来了,我们看看这个方法都做了些什么。
public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {final Adapter<?> currentAdapter = mRecyclerView.getAdapter();mAccessibilityProvider.onDetachAdapter(currentAdapter);unregisterCurrentItemDataSetTracker(currentAdapter);mRecyclerView.setAdapter(adapter);mCurrentItem = 0;restorePendingState();mAccessibilityProvider.onAttachAdapter(adapter);registerCurrentItemDataSetTracker(adapter);}
笔者大概查询了一下,这段代码首先用
final Adapter<?> currentAdapter = mRecyclerView.getAdapter();
这段代码获取当前RecyclerView的适配器,并将其存储在
currentAdapter
变量中。
mAccessibilityProvider.onDetachAdapter(currentAdapter);
unregisterCurrentItemDataSetTracker(currentAdapter);
这段代码通知辅助功能提供者(Accessibility Provider)当前适配器已经被分离(detached)。这是为了在适配器更换时更新辅助功能的状态,而后取消注册当前适配器的数据集变化跟踪器。这通常是在适配器更换时进行的,以确保旧的适配器不再被跟踪。
mRecyclerView.setAdapter(adapter);
这行代码将一个新的适配器
adapter
设置给RecyclerView。这是实际更换适配器的操作。
restorePendingState();
这行代码尝试恢复挂起的状态。这可能是一个自定义方法,用于在适配器更换后恢复之前的状态,例如恢复滚动位置等。
mAccessibilityProvider.onAttachAdapter(adapter);
这行代码通知辅助功能提供者新的适配器已经被附加(attached)。这是为了确保辅助功能能够正确地与新的适配器交互。
registerCurrentItemDataSetTracker(adapter);
这行代码注册新的适配器的数据集变化跟踪器。这允许RecyclerView监听数据集的变化,并在变化发生时进行适当的更新。
大概总结一下,这段代码的作用就是更加安全地为内置的RecyclerView更换了适配器,并且确保一系列的辅助功能能正确的运行与更新。
看都看了,不妨再看一点:
翻着翻着发现ViewPager2的构造方法都要用到一个initalize方法,我们看看这个initalize方法都干了什么:
非常长一大串,不过没关系,我们忽略其中的大部分内容。看到这句话:
这两段想必并不陌生,这是创建RecyclerView视图时必不可缺的两句话,第一句用于创建Recycler实例,第二句用于创建一个线性布局管理器,而后将管理器设置到RecyclerView。
最后的这句代码也能望文生义,也就是将刚刚创建的RecyclerView附加到父视图上。
以上就是对ViewPager2的简单介绍了,希望对大家理解这个组件能有一些帮助。
关联TabLayout
在Activity的onCreate方法中增加一句话即可:
new TabLayoutMediator(binding.tabLayout, binding.viewPager, new TabLayoutMediator.TabConfigurationStrategy() {@Overridepublic void onConfigureTab(@NonNull TabLayout.Tab tab, int i) {tab.setText("nihao");}
}).attach();
这段代码是Android开发中用于连接TabLayout
和ViewPager2
(或ViewPager
)的TabLayoutMediator
类的使用示例。TabLayoutMediator
是一个实用工具类,它帮助开发者将TabLayout
的标签与ViewPager
或ViewPager2
的页面同步。以下是代码的详细分析:
创建TabLayoutMediator
实例:
new TabLayoutMediator(binding.tabLayout, binding.viewPager, …)
这行代码创建了一个新的TabLayoutMediator对象。它接收三个参数:
binding.tabLayout
: 一个TabLayout
实例,表示包含标签的组件。binding.viewPager
: 一个ViewPager
或ViewPager2
实例,表示用户可以左右滑动浏览的组件。- 一个实现了
TabLayoutMediator.TabConfigurationStrategy
接口的匿名类实例,这个接口定义了如何配置每个标签。
配置标签:
new TabLayoutMediator.TabConfigurationStrategy() {…}
这是一个匿名内部类,实现了TabLayoutMediator.TabConfigurationStrategy
接口。这个接口包含一个方法onConfigureTab
,用于配置每个标签。
@Override public void onConfigureTab(@NonNull TabLayout.Tab tab, int i) { … }
这是onConfigureTab方法的重写,它接收两个参数:
@NonNull TabLayout.Tab tab
: 当前需要配置的Tab
对象。int i
: 表示当前标签在TabLayout
中的位置(索引)。
连接TabLayout
和ViewPager
:
.attach();
- 这行代码调用
TabLayoutMediator
实例的attach
方法,它的作用是将TabLayout
和ViewPager
连接起来。一旦连接,当ViewPager
的页面发生变化时,TabLayout
会相应地更新当前选中的标签。同样,当用户点击某个标签时,ViewPager
会滚动到对应的页面。
笔者不小心手滑,刚写完代码就点进去了一个陌生的页面。
原来是attach()方法的实现原理,正巧笔者也对TabLayout如何连接两个组件非常好奇,不妨来看一看。
首先这个方法会检查Mediator是否连接到了viewPager,而后会检查viewPager的适配器是否存在,保障其安全性。
一切准备就绪后,将attached的状态设置为true,接下来就是激动人心的逻辑环节了:
创建页面变化回调:
-
this.onPageChangeCallback = new TabLayoutOnPageChangeCallback(this.tabLayout);
- 创建一个
TabLayoutOnPageChangeCallback
实例,用于处理ViewPager2
页面变化事件,并更新TabLayout
。
- 创建一个
注册页面变化回调:
-
this.viewPager.registerOnPageChangeCallback(this.onPageChangeCallback);
- 将创建的页面变化回调注册到
ViewPager2
。
- 将创建的页面变化回调注册到
创建标签选择监听器:
-
this.onTabSelectedListener = new ViewPagerOnTabSelectedListener(this.viewPager, this.smoothScroll);
创建一个
ViewPagerOnTabSelectedListener
实例,用于处理TabLayout
标签选择事件,并更新ViewPager2
。
添加标签选择监听器:
-
this.tabLayout.addOnTabSelectedListener(this.onTabSelectedListener);
将创建的标签选择监听器添加到
TabLayout
。
设置标签滚动位置:
this.tabLayout.setScrollPosition(this.viewPager.getCurrentItem(), 0.0F, true);
- 设置
TabLayout
中当前选中标签的滚动位置,确保用户可以看到当前页面对应的标签。
中间有一段实在不知道干什么用的,就留给读者去解决啦~
结语
本文参考:
【Android】ViewPager2和TabLayout协同使用,实现多Fragment页面切换类似于QQ音乐,bilibili效果_tablayout和viewpager2-CSDN博客
【Android】ViewPager2监听页面切换事件_viewpager2 监听-CSDN博客
安卓:TabLayout+ViewPager2+Fragment使用(java)_tablayout viewpager2 fragment-CSDN博客