10、ViewPager与TabLayout:实现页面滑动切换、与Fragment结合、自定义指示器样式
说实话,ViewPager 和 TabLayout 这对组合,在 Android 开发里太常见了。几乎每个带多页面切换的 App 都会用到。我记得刚入行那会儿,看到别人写的滑动页面效果,觉得特别酷。后来自己动手做,才发现坑也不少。
今天我就带你彻底搞懂它。从基础用法到 Fragment 结合,再到自定义指示器,咱们一步步来。
10.1 ViewPager 基础:页面滑动切换
ViewPager 是 Android 官方提供的一个布局容器。它允许用户左右滑动,切换不同的页面。说白了,就是一个可以滑动的“页面容器”。
它的核心用法很简单:
- 在布局文件里放一个 ViewPager
- 创建一个 PagerAdapter(适配器)
- 把适配器设置给 ViewPager
来看一个最基础的例子:
<!-- activity_main.xml -->
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
// MainActivity.java
public class MainActivity extends AppCompatActivity {
private ViewPager viewPager;
private List<View> pageList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager = findViewById(R.id.viewPager);
// 准备页面数据
pageList = new ArrayList<>();
LayoutInflater inflater = LayoutInflater.from(this);
pageList.add(inflater.inflate(R.layout.page_one, null));
pageList.add(inflater.inflate(R.layout.page_two, null));
pageList.add(inflater.inflate(R.layout.page_three, null));
// 设置适配器
MyPagerAdapter adapter = new MyPagerAdapter(pageList);
viewPager.setAdapter(adapter);
}
// 自定义适配器
class MyPagerAdapter extends PagerAdapter {
private List<View> views;
public MyPagerAdapter(List<View> views) {
this.views = views;
}
@Override
public int getCount() {
return views.size();
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
View view = views.get(position);
container.addView(view);
return view;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View) object);
}
}
}
嗯,这里要注意 isViewFromObject 方法。它用来判断当前显示的 View 是不是适配器返回的那个对象。我习惯直接用 == 比较,简单直接。
10.2 与 Fragment 结合:更优雅的页面管理
直接用 View 做页面,虽然简单,但功能有限。一旦页面复杂起来,比如需要处理生命周期、保存状态,那就得用 Fragment 了。
这时候要用 FragmentPagerAdapter 或 FragmentStatePagerAdapter。我个人更推荐后者,尤其是页面数量多的时候。为什么呢?
- FragmentPagerAdapter:会把所有 Fragment 都保存在内存里。适合页面少、轻量级的场景。
- FragmentStatePagerAdapter:只保留当前页面和相邻页面的 Fragment。其他页面会被销毁,节省内存。适合页面多、内容重的场景。
我在项目中遇到过一个问题:用 FragmentPagerAdapter 加载了 10 个页面,结果内存直接飙到 200MB。后来换成 FragmentStatePagerAdapter,内存降到了 80MB。你想想看,差距有多大。
来看代码:
public class MyFragmentPagerAdapter extends FragmentStatePagerAdapter {
private List<Fragment> fragmentList;
private List<String> titleList;
public MyFragmentPagerAdapter(@NonNull FragmentManager fm,
List<Fragment> fragments,
List<String> titles) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.fragmentList = fragments;
this.titleList = titles;
}
@NonNull
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return titleList.get(position);
}
}
注意看构造函数里的 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。这是 AndroidX 引入的新行为模式。它保证只有当前可见的 Fragment 才会调用 onResume()。以前的老版本是所有 Fragment 都会调用,容易出问题。
10.3 TabLayout:让页面切换更直观
光有 ViewPager 还不够。用户怎么知道当前在第几页?怎么快速跳转到指定页?这时候 TabLayout 就派上用场了。
TabLayout 是 Material Design 库里的组件。它和 ViewPager 是天作之合。用法也简单:
<!-- 布局文件 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
// 代码中关联
TabLayout tabLayout = findViewById(R.id.tabLayout);
ViewPager viewPager = findViewById(R.id.viewPager);
// 创建适配器(假设已经有了 Fragment 和标题)
MyFragmentPagerAdapter adapter = new MyFragmentPagerAdapter(
getSupportFragmentManager(), fragmentList, titleList);
viewPager.setAdapter(adapter);
// 关键一步:把 TabLayout 和 ViewPager 关联起来
tabLayout.setupWithViewPager(viewPager);
就这一行 setupWithViewPager,TabLayout 会自动读取 PagerAdapter 里的 getPageTitle() 方法,生成对应的 Tab 标签。用户点击 Tab,ViewPager 就滑动到对应页面。反过来,滑动 ViewPager,Tab 的高亮也会跟着变。
说白了,这就是一个双向绑定的机制。
10.4 自定义指示器样式:让 UI 更出彩
默认的 TabLayout 指示器是一条细细的横线。说实话,看多了有点腻。很多 App 都会自定义指示器样式,比如改成圆形、渐变条、或者带图标的。
自定义指示器,主要有两种方式:
- 通过 XML 属性:简单调整颜色、高度、宽度
- 通过自定义 View:完全自己画,想画什么画什么
10.4.1 通过 XML 属性调整
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabIndicatorColor="@color/colorAccent"
app:tabIndicatorHeight="4dp"
app:tabIndicatorGravity="bottom"
app:tabMode="fixed"
app:tabGravity="fill" />
这些属性可以快速改变指示器的颜色、高度、位置。但如果你想让它变成圆形,或者有动画效果,那就得用第二种方式了。
10.4.2 自定义指示器 View
我记得有一次做项目,设计师给了一个“波浪形”指示器。默认的 TabLayout 根本做不出来。最后我只好自定义了一个指示器 View。
核心思路是:监听 TabLayout 的滑动事件,动态绘制指示器。
public class CustomIndicatorView extends View {
private Paint paint;
private float indicatorLeft;
private float indicatorRight;
public CustomIndicatorView(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(getResources().getColor(R.color.colorAccent));
paint.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 画一个圆形指示器
float centerX = (indicatorLeft + indicatorRight) / 2;
float centerY = getHeight() / 2;
float radius = (indicatorRight - indicatorLeft) / 2;
canvas.drawCircle(centerX, centerY, radius, paint);
}
public void setIndicatorPosition(float left, float right) {
this.indicatorLeft = left;
this.indicatorRight = right;
invalidate();
}
}
然后在 Activity 里监听 TabLayout 的滑动:
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// Tab 被选中
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
// Tab 取消选中
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
// Tab 重新选中
}
});
// 更精细的控制:监听滑动偏移
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// 计算指示器的位置
View tabView = ((LinearLayout) tabLayout.getChildAt(0)).getChildAt(position);
if (tabView != null) {
float left = tabView.getLeft() + tabView.getWidth() * positionOffset;
float right = tabView.getRight() + tabView.getWidth() * positionOffset;
customIndicatorView.setIndicatorPosition(left, right);
}
}
@Override
public void onPageSelected(int position) {}
@Override
public void onPageScrollStateChanged(int state) {}
});
10.5 知识体系总览
下面这张图,帮你梳理了本章的核心知识点。建议保存下来,写代码时对照着看。
10.6 避坑指南
最后,分享几个我踩过的坑。希望能帮你省点时间。
我曾经在切换 Tab 时,发现 Fragment 里的数据莫名其妙没了。后来发现是
FragmentStatePagerAdapter 在页面不可见时销毁了 Fragment。解决方案:在 Fragment 的 onSaveInstanceState() 里保存关键数据,或者在 ViewModel 里持有数据。
这个问题很常见。原因通常是 PagerAdapter 没有正确实现
getPageTitle() 方法。记得检查一下,这个方法返回 null 的话,Tab 上就不会显示文字。
如果你在
onPageScrolled 里做了太多计算,比如频繁创建对象、做复杂绘图,就会导致卡顿。我的建议是:把计算逻辑尽量简化,能用预计算的就提前算好。另外,invalidate() 不要调太频繁。
好了,关于 ViewPager 和 TabLayout 的内容,今天就聊到这里。这些东西看起来简单,但真正用好、用对,还是需要一些实践积累的。希望你能动手写一写,遇到问题再回来翻翻这篇文章。