在我们日常使用的应用中,常常会遇到可以左右滑动切换内容的界面,比如知乎首页的推荐/热榜/关注标签页、微博的频道分类、相册中的多图浏览,甚至首次打开 App 时的欢迎引导页。这类界面背后的滑动切换机制,通常就是通过 Android 提供的 ViewPager 或其升级版 ViewPager2 实现的。ViewPager2 是 Android 提供的现代化分页滑动组件,支持横向与纵向滑动,具备高效的页面管理能力,是实现多页切换、Tab 联动与内容滑动浏览等功能的首选方案。
注:ViewPager2 已全面替代 ViewPager,是官方推荐的新项目默认使用的分页滑动组件。
概述:ViewPager2 是基于 RecyclerView 实现的页面切换控件,继承了其高效的视图复用机制和灵活布局能力,支持水平与垂直滑动,通过设置 offscreenPageLimit
控制缓存策略,提供更高性能和更低内存开销。使用 FragmentStateAdapter 或 RecyclerView.Adapter 作为适配器,适配灵活,支持页面生命周期自动管理,同时还支持 PageTransformer 实现炫酷的切换动画,是替代旧版 ViewPager 的推荐方案。
使用前先添加依赖 (build.gradle):
dependencies {
// ...
implementation 'androidx.viewpager2:viewpager2:1.1.0'
implementation 'com.google.android.material:material:1.12.0'
}
注: 检查并使用最新稳定版本号(ViewPager2|Google 的 Maven 存储库)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
LinearLayout>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#19CFBE">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="泥嚎"
android:layout_gravity="center"
android:textSize="45sp"/>
FrameLayout>
public class HomeFragment extends Fragment {
private FragmentBinding binding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentBinding.inflate(inflater, container, false);
return binding.getRoot();
}
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
public class MyAdapter extends FragmentStateAdapter {
private final List<Fragment> fragmentList;
public MyAdapter(@NonNull FragmentActivity fragmentActivity, List<Fragment> fragmentList) {
super(fragmentActivity);
this.fragmentList = fragmentList;
}
@NonNull
@Override
public Fragment createFragment(int position) {
// 用于返回指定的fragment界面
return fragmentList.get(position);
}
@Override
public int getItemCount() {
// 用于加载容器大小
return fragmentList != null ? fragmentList.size() : 0;
}
}
自定义 Adapter 并继承 FragmentStateAdapter
ViewPager2 的两种适配器类型:
RecyclerView.Adapter
- 这是一个基础类,需要你自己创建 ViewHolder、绑定 View。
- 适合每个页面是简单的布局 View,不需要 Fragment。
- 可以直接继承
RecyclerView.Adapter
来实现。- 优点:轻量,适合纯视图页面,不用管理 Fragment 复杂生命周期。
- 缺点:没有 Fragment 的独立生命周期,不能使用 Fragment 特性。
FragmentStateAdapter
- 继承自
RecyclerView.Adapter
,但专门为 ViewPager2 管理 Fragment 做了封装。- 内部会自动帮你管理 Fragment 的添加、显示、销毁和状态保存。
- 适合每页是 Fragment 的场景,比如每页一个独立的界面。
- 需要实现
createFragment(int position)
来返回对应位置的 Fragment 实例。
List<Fragment> fragmentList = new ArrayList<>();
fragmentList.add(new HomeFragment());
fragmentList.add(new HomeFragment());
fragmentList.add(new HomeFragment());
fragmentList.add(new HomeFragment());
fragmentList.add(new HomeFragment());
fragmentList.add(new HomeFragment());
fragmentList.add(new HomeFragment());
fragmentList.add(new HomeFragment());
// 设置适配器
MyAdapter adapter = new MyAdapter(this, fragmentList);
binding.viewPager.setAdapter(adapter);
TabLayout
是 Android 中用于实现选项卡导航的控件,常与 ViewPager2
搭配使用,它可以在界面顶部展示一排可切换的标签页,每个标签通常对应一个页面内容(如 Fragment),用户点击或滑动即可在各页面之间切换。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:elevation="0dp"
app:tabMode="scrollable"
app:tabIndicatorColor="@color/pink"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
LinearLayout>
XML 属性 | 方法 | 取值 | 说明 |
---|---|---|---|
app:tabMode |
setTabMode() |
fixed scrollable |
标签显示模式:固定宽度/可滚动 |
app:tabGravity |
setTabGravity() |
center fill |
标签对齐方式:居中/填充 |
app:tabContentStart |
setPadding() |
尺寸值 (如 16dp ) |
第一个标签的起始边距 |
app:tabInlineLabel |
setInlineLabel() |
true false |
图标与文本是否同行显示 |
app:tabIndicatorColor |
setSelectedTabIndicatorColor() |
颜色值 | 指示器颜色 |
app:tabIndicatorHeight |
setSelectedTabIndicatorHeight() |
尺寸值 | 指示器高度 |
app:tabIndicatorFullWidth |
setTabIndicatorFullWidth() |
true false |
指示器是否与标签等宽 |
app:tabIndicator |
setSelectedTabIndicator() |
Drawable 资源 | 自定义指示器 Drawable |
app:tabIndicatorGravity |
setTabIndicatorGravity() |
bottom top center stretch |
指示器位置:底部/顶部/居中/拉伸 |
app:tabIndicatorAnimationDuration |
无 | 毫秒值 (如 300 ) |
指示器动画时长 |
app:tabIndicatorAnimationMode |
无 | linear elastic |
指示器动画模式:线性/弹性 |
app:tabTextAppearance |
setTabTextAppearance() |
样式资源 (如 @style/MyTab ) |
文本外观样式 |
app:tabTextColor |
setTabTextColors() |
ColorStateList | 文本颜色(支持选择器) |
app:tabIconTint |
setTabIconTint() |
ColorStateList | 图标着色(支持选择器) |
app:tabBackground |
setTabBackground() |
Drawable 资源 | 标签背景(支持选择器) |
app:tabRippleColor |
setTabRippleColor() |
ColorStateList | 点击波纹效果颜色 |
app:tabMinWidth |
setTabMinWidth() |
尺寸值 | 标签最小宽度 |
app:tabMaxWidth |
setTabMaxWidth() |
尺寸值 | 标签最大宽度 |
app:tabPaddingStart |
setTabPadding() |
尺寸值 | 标签起始内边距 |
new TabLayoutMediator(binding.tabLayout, binding.viewPager,
new TabLayoutMediator.TabConfigurationStrategy() {
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
//自定义TabView
TextView tabView = new TextView(MainActivity.this);
tabView.setText("Fragment " + position);
//将tabbView绑定到tab
tab.setCustomView(tabView);
}
}).attach();
创建一个 TabLayoutMediator
实例,传入三个参数:
binding.tabLayout
:要联动的 TabLayout。binding.viewPager
:要联动的 ViewPager2。TabConfigurationStrategy
:回调接口,用于配置每一个 Tab(比如设置标题、图标等)。
onConfigureTab(...)
每个 Tab 初始化时,这个方法都会被调用一次,对应 ViewPager2 的每一页。
tab.setCustomView(tabView);
设置 Tab 的视图,可以完全控制 Tab 的外观,而不再受默认样式限制。也可以用自己写的 layout 文件,比如:
View customTab = LayoutInflater.from(context).inflate(R.layout.my_tab_layout, null); tab.setCustomView(customTab);
binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
// 这里可以处理页面切换时的逻辑
}
});
这是给 ViewPager2
注册一个 页面变更回调监听器(PageChangeCallback),当用户手动或编程切换页面时,可以在对应的方法中执行你想要的逻辑。
registerOnPageChangeCallback(...)
向 ViewPager2 注册一个页面变化的监听器,类型是
ViewPager2.OnPageChangeCallback
,可以监听 当前页面的状态变化(比如选中、滑动、滚动状态变化等)
new ViewPager2.OnPageChangeCallback()
这是创建一个监听器对象,并重写其回调方法。该监听器包含以下常用方法:
onPageSelected(int position)
- 调用时机:页面切换完成后(即滑动结束,页面完全展示出来时)。
- 适合场景:
- 更新 TabLayout 高亮
- 改变底部按钮状态
- 动态加载数据(懒加载)
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
- 当页面正在滑动中不断回调。
- 可用于页面间动画联动、Tab 动画跟随效果等。
onPageScrollStateChanged(int state)
- 页面滑动状态变化时回调。
state
可能的值:
SCROLL_STATE_IDLE
(0): 空闲状态,页面滑动停止。SCROLL_STATE_DRAGGING
(1): 用户正在滑动。SCROLL_STATE_SETTLING
(2): 页面正在自动滑动到最终位置。
TabLayout tabLayout = findViewById(binding.tabLayout);
// 添加文本标签
tabLayout.addTab(tabLayout.newTab().setText("首页"));
// 添加图标标签
tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.ic_home));
// 添加自定义视图标签
TabLayout.Tab tab = tabLayout.newTab();
tab.setCustomView(R.layout.custom_tab_view);
tabLayout.addTab(tab);
类型 | 方法 | 适用场景 |
---|---|---|
文本标签 | setText("标题") |
简洁的标题导航 |
图标标签 | setIcon(R.drawable.xxx) |
图标导航栏(常见于底部) |
自定义标签 | setCustomView(R.layout.xxx) |
图文结合、动画、徽章等复杂样式 |
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// 标签被选中时触发
int position = tab.getPosition();
viewPager2.setCurrentItem(position);
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
// 标签取消选中时触发
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
// 已选中标签再次点击时触发
}
});
//false表示禁止,true表示允许
binding.viewPager.setUserInputEnabled(false);
ViewPager2
内部是通过 RecyclerView
实现滑动的。setUserInputEnabled(false)
会禁用一切手势滑动,但不会影响通过 setCurrentItem()
代码设置页面切换。
代码如下:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
EdgeToEdge.enable(this);
ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
List<Fragment> fragmentList = List.of(
new LiveStreamingFragment(),
new RecommendFragment(),
new HotFragment(),
new AnimationFragment(),
new FilmFragment(),
new RedFragment()
);
MyAdapter adapter = new MyAdapter(this, fragmentList);
binding.viewPager.setAdapter(adapter);
new TabLayoutMediator(binding.tabLayout, binding.viewPager,
new TabLayoutMediator.TabConfigurationStrategy() {
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
switch (position) {
case 0:
tab.setText("直播");
break;
case 1:
tab.setText("推荐");
break;
case 2:
tab.setText("热门");
break;
case 3:
tab.setText("动画");
break;
case 4:
tab.setText("影视");
break;
case 5:
tab.setText("新征程");
break;
default:
tab.setText("未知");
break;
}
}
}).attach();
binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
}
});
}
}
public class MyAdapter extends FragmentStateAdapter {
private final List<Fragment> fragmentList;
public MyAdapter(@NonNull FragmentActivity fragmentActivity, List<Fragment> fragmentList) {
super(fragmentActivity);
this.fragmentList = fragmentList;
}
@NonNull
@Override
public Fragment createFragment(int position) {
return fragmentList.get(position);
}
@Override
public int getItemCount() {
return fragmentList != null ? fragmentList.size() : 0;
}
}
public class LiveStreamingFragment extends Fragment {
private FragmentLiveStreamingBinding binding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentLiveStreamingBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}
public class RecommendFragment extends Fragment {
private FragmentRecommendBinding binding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentRecommendBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}
// 其余几个碎片和上面的类似
// ...
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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:elevation="0dp"
app:tabMode="auto"
app:tabSelectedTextColor="@color/pink"
app:tabIndicatorColor="@color/pink"
app:tabIndicatorGravity="bottom"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="60dp"
android:src="@drawable/p0"/>
</LinearLayout>
每个碎片布局都是简单的用了一个 ImageView 用来展示图片:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/p12"/>
</FrameLayout>