42、Material Design之TabLayout:Tab模式、自定义Tab、与ViewPager2结合

TabLayout,说白了就是Android里做顶部导航标签的标配。我最早接触它是在Material Design刚推出那会儿,那时候大家还在用第三方的PagerSlidingTabStrip。后来Google官方出了TabLayout,我就彻底弃坑了。嗯,今天咱们就把它聊透。

一、TabLayout的三种Tab模式

TabLayout支持三种模式,我习惯用MODE_FIXED,但具体用哪个得看场景。

模式 常量 适用场景
固定模式 MODE_FIXED Tab数量少(≤4个),所有Tab等宽显示
可滚动模式 MODE_SCROLLABLE Tab数量多(≥5个),左右滑动查看
自动模式 MODE_AUTO 根据宽度自动选择固定或滚动

我的经验:MODE_AUTO看起来智能,但实际效果有时会尴尬。比如屏幕旋转时模式突然切换,用户体验并不好。我个人建议:明确场景,手动指定模式。

代码里设置很简单:

TabLayout tabLayout = findViewById(R.id.tabLayout);
tabLayout.setTabMode(TabLayout.MODE_FIXED); // 或 MODE_SCROLLABLE

二、自定义Tab:别再用默认样式了

默认的Tab样式太素了。我在项目中做过一个电商App,产品经理要求Tab里带小红点和图标。默认的TabLayout根本满足不了。怎么办?自定义Tab视图。

核心思路:通过setCustomView()给每个Tab注入你写的布局。

先写一个自定义Tab布局 tab_custom.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/tab_icon"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:src="@drawable/ic_home" />

    <TextView
        android:id="@+id/tab_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="首页"
        android:textSize="12sp" />

    <!-- 小红点 -->
    <View
        android:id="@+id/tab_badge"
        android:layout_width="8dp"
        android:layout_height="8dp"
        android:layout_marginTop="2dp"
        android:background="@drawable/bg_red_dot"
        android:visibility="gone" />
</LinearLayout>

然后在代码里绑定:

for (int i = 0; i < titles.length; i++) {
    TabLayout.Tab tab = tabLayout.newTab();
    View customView = getLayoutInflater().inflate(R.layout.tab_custom, null);
    TextView tvTitle = customView.findViewById(R.id.tab_title);
    ImageView ivIcon = customView.findViewById(R.id.tab_icon);
    tvTitle.setText(titles[i]);
    ivIcon.setImageResource(icons[i]);
    tab.setCustomView(customView);
    tabLayout.addTab(tab);
}

小技巧:自定义Tab视图的高度不要写死,用wrap_content。否则不同屏幕密度下容易变形。我曾经踩过这个坑,在平板上一看,图标都挤变形了。

三、TabLayout + ViewPager2:黄金搭档

ViewPager2是ViewPager的升级版,支持RTL、垂直滑动,还解决了嵌套滑动冲突。我建议新项目直接用ViewPager2,别再用老版了。

两者结合的核心是 TabLayoutMediator。注意,不是以前的 setupWithViewPager(),那个方法在ViewPager2里已经废弃了。

ViewPager2 viewPager2 = findViewById(R.id.viewPager2);
TabLayout tabLayout = findViewById(R.id.tabLayout);

// 先给ViewPager2设置Adapter
MyPagerAdapter adapter = new MyPagerAdapter(this);
viewPager2.setAdapter(adapter);

// 再用TabLayoutMediator绑定
new TabLayoutMediator(tabLayout, viewPager2,
    (tab, position) -> {
        tab.setText(titles[position]);
        tab.setIcon(icons[position]);
    }
).attach();

这里有个细节:TabLayoutMediator的第三个参数是一个回调,你在里面配置每个Tab的样式。如果你用了自定义视图,就在这里调用tab.setCustomView()

注意:一定要先给ViewPager2设置Adapter,再调用attach()。顺序反了会报空指针。我曾经调试了半小时才发现是顺序问题。

四、避坑指南

  • Tab选中状态不更新:检查是否在TabLayoutMediator的回调里正确设置了选中/未选中的样式。我习惯在回调里根据position判断当前Tab是否选中。
  • 自定义Tab点击区域太小:给自定义布局的根布局设置android:clickable="true",否则点击事件可能被TabLayout拦截。
  • ViewPager2滑动卡顿:检查Fragment是否在onCreateView()里做了耗时操作。ViewPager2默认预加载一个相邻页面,如果Fragment初始化太重,就会卡。
  • Tab指示器宽度不对:自定义Tab时,指示器宽度默认是Tab的宽度。如果你想让指示器只包裹文字,可以设置app:tabIndicatorFullWidth="false"

五、知识体系图

下面这张图梳理了TabLayout的核心知识点和关联关系,方便你快速回顾:

TabLayout 知识体系 TabLayout Tab 模式 MODE_FIXED MODE_SCROLLABLE MODE_AUTO 自定义 Tab 视图 setCustomView() 图标 + 文字 + 角标 选中/未选中状态 ViewPager2 结合 TabLayoutMediator 先设Adapter,再attach() Fragment预加载策略

六、总结

TabLayout + ViewPager2这套组合,是Android里做页面导航最常用的方案。记住三点:

  • Tab模式根据Tab数量选,别偷懒用AUTO
  • 自定义Tab视图时注意布局层级,别嵌套太深
  • TabLayoutMediator是绑定的正确姿势,别忘了先设Adapter

我在项目里用这套方案做过十几个页面,从简单的设置页到复杂的电商首页,基本都能覆盖。你只要把上面这些点吃透,遇到类似需求就能信手拈来。


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