10、ViewPager与TabLayout:实现页面滑动切换、与Fragment结合、自定义指示器样式

说实话,ViewPager 和 TabLayout 这对组合,在 Android 开发里太常见了。几乎每个带多页面切换的 App 都会用到。我记得刚入行那会儿,看到别人写的滑动页面效果,觉得特别酷。后来自己动手做,才发现坑也不少。

今天我就带你彻底搞懂它。从基础用法到 Fragment 结合,再到自定义指示器,咱们一步步来。

10.1 ViewPager 基础:页面滑动切换

ViewPager 是 Android 官方提供的一个布局容器。它允许用户左右滑动,切换不同的页面。说白了,就是一个可以滑动的“页面容器”。

它的核心用法很简单:

  1. 在布局文件里放一个 ViewPager
  2. 创建一个 PagerAdapter(适配器)
  3. 把适配器设置给 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 是不是适配器返回的那个对象。我习惯直接用 == 比较,简单直接。

小提示:PagerAdapter 有四个必须实现的方法:getCount()、isViewFromObject()、instantiateItem()、destroyItem()。少一个都会报错。

10.2 与 Fragment 结合:更优雅的页面管理

直接用 View 做页面,虽然简单,但功能有限。一旦页面复杂起来,比如需要处理生命周期、保存状态,那就得用 Fragment 了。

这时候要用 FragmentPagerAdapterFragmentStatePagerAdapter。我个人更推荐后者,尤其是页面数量多的时候。为什么呢?

  • 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 都会调用,容易出问题。

警告:如果你用的是旧版 support 包,记得升级到 AndroidX。否则 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 都会自定义指示器样式,比如改成圆形、渐变条、或者带图标的。

自定义指示器,主要有两种方式:

  1. 通过 XML 属性:简单调整颜色、高度、宽度
  2. 通过自定义 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) {}
});
核心要点:自定义指示器的本质,就是监听 ViewPager 的滑动偏移量,动态更新指示器的位置和大小。掌握了这个思路,你可以画出任何形状的指示器。

10.5 知识体系总览

下面这张图,帮你梳理了本章的核心知识点。建议保存下来,写代码时对照着看。

ViewPager + TabLayout 知识体系 ViewPager + TabLayout ViewPager 基础 PagerAdapter 适配器 instantiateItem / destroyItem Fragment 结合 FragmentPagerAdapter FragmentStatePagerAdapter TabLayout 关联 setupWithViewPager() getPageTitle() 自动读取 自定义指示器样式 XML 属性调整(颜色/高度) 自定义 View 绘制(圆形/波浪)

10.6 避坑指南

最后,分享几个我踩过的坑。希望能帮你省点时间。

坑1:Fragment 重建导致数据丢失
我曾经在切换 Tab 时,发现 Fragment 里的数据莫名其妙没了。后来发现是 FragmentStatePagerAdapter 在页面不可见时销毁了 Fragment。解决方案:在 Fragment 的 onSaveInstanceState() 里保存关键数据,或者在 ViewModel 里持有数据。
坑2:TabLayout 和 ViewPager 关联后,Tab 不显示标题
这个问题很常见。原因通常是 PagerAdapter 没有正确实现 getPageTitle() 方法。记得检查一下,这个方法返回 null 的话,Tab 上就不会显示文字。
坑3:自定义指示器时,滑动卡顿
如果你在 onPageScrolled 里做了太多计算,比如频繁创建对象、做复杂绘图,就会导致卡顿。我的建议是:把计算逻辑尽量简化,能用预计算的就提前算好。另外,invalidate() 不要调太频繁。

好了,关于 ViewPager 和 TabLayout 的内容,今天就聊到这里。这些东西看起来简单,但真正用好、用对,还是需要一些实践积累的。希望你能动手写一写,遇到问题再回来翻翻这篇文章。


公众号:蓝海资料掘金营,微信deep3321