ViewPager2系列--与TabLayout的结合

前言

ViewPager我们在之前的文章也已经提到了,它是Android平台上的一个布局容器,用于实现多个页面的滑动切换。它通常用于构建用户界面中的多页内容,例如轮播图、图片浏览器、引导页等。ViewPager可以滑动切换不同的页面,并且支持左右/上下滑动手势。

TabLayout是一个用于创建标签式导航栏的UI组件。它通常与ViewPager结合使用,用于展示ViewPager中不同页面的标题或图标,并提供切换页面的导航功能。TabLayout可以以标签的形式展示页面,使用户能够快速切换到所需的页面。

结合使用ViewPagerTabLayout能够为应用程序提供更好的用户体验和导航方式。ViewPager可以让用户通过滑动来浏览不同的页面,而TabLayout则提供了清晰的标签导航,使用户能够快速找到并切换到所需的页面。这种结合使用的模式在许多应用中被广泛采用。

TabLayout与ViewPager2的结合使用

要将 ViewPager2TabLayout 结合使用,可以按照以下步骤进行操作:

  1. 在 XML 布局文件中添加 TabLayoutViewPager2
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        app:tabMode="fixed"
        app:tabGravity="fill"
        android:layout_marginBottom="10dp"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/tabLayout"
        android:orientation="horizontal"/>

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 在代码中获取 TabLayoutViewPager2 的实例:
val viewPager = binding.viewPager
val tabLayout = binding.tabLayout
  1. 创建 Fragment 列表和相应的标签标题:
val fragments = listOf(FirstFragment(), SecondFragment(), ThirdFragment())
val titles = listOf("Tab 1", "Tab 2", "Tab 3")
  1. 创建 FragmentStateAdapter 并设置给 ViewPager2
val adapter = MyFragmentStateAdapter(this, fragments)
viewPager.adapter = adapter
  1. ViewPager2TabLayout 绑定在一起:
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
    tab.text = titles[position]
}.attach()

完整示例代码如下:

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

    private val binding by lazy {
        ActivityMainBinding.inflate(
            layoutInflater
        )
    }

    private val fragments = listOf(FirstFragment(), SecondFragment(), ThirdFragment())
    private val fragmentAdapter by lazy { MyFragmentStateAdapter(this, fragments) }
    private val titles = listOf("Tab 1", "Tab 2", "Tab 3")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        initView()
    }

    private fun initView() {
        val viewPager = binding.viewPager
        viewPager.adapter = fragmentAdapter
        val pageTransformer = CustomPageTransformer()
        viewPager.setPageTransformer(pageTransformer)
        viewPager.offscreenPageLimit = 3
        TabLayoutMediator(binding.tabLayout, viewPager) { tab, position ->
            tab.text = titles[position]
        }.attach()
    }
 }

其中, MyFragmentStateAdapter 是自定义的 FragmentStateAdapter

TabLayout要求应用的theme 必须是Theme.AppCompat,所以运行前需要注意:

  1. 应用的Theme必须是Theme.AppCompat及其子主题;
  2. ActivityMainBinding.inflate( )中的LayoutInflater不能是LayoutInflater.from(baseContext)或者LayoutInflater.from(applicationContext)

如果不满足以上两点,会报如下错误:

android.view.InflateException: Binary XML file line #15 in com.example.viewpager2demo:layout/activity_main: Binary XML file line #15 in com.example.viewpager2demo:layout/activity_main: Error inflating class com.google.android.material.tabs.TabLayout
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4324)
Caused by: android.view.InflateException: Binary XML file line #15 in com.example.viewpager2demo:layout/activity_main: Binary XML file line #15 in com.example.viewpager2demo:layout/activity_main: Error inflating class com.google.android.material.tabs.TabLayout
Caused by: android.view.InflateException: Binary XML file line #15 in com.example.viewpager2demo:layout/activity_main: Error inflating class com.google.android.material.tabs.TabLayout
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Constructor.newInstance0(Native Method)
at android.view.LayoutInflater.inflate(LayoutInflater.java:708)
at android.view.LayoutInflater.inflate(LayoutInflater.java:552)
at com.example.viewpager2demo.databinding.ActivityMainBinding.inflate(ActivityMainBinding.java:50)
at com.example.viewpager2demo.databinding.ActivityMainBinding.inflate(ActivityMainBinding.java:44)
at com.example.viewpager2demo.MainActivity$binding$2.invoke(MainActivity1.kt:18)
at com.example.viewpager2demo.MainActivity$binding$2.invoke(MainActivity1.kt:17)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at com.example.viewpager2demo.MainActivity.getBinding(MainActivity1.kt:17)
at com.example.viewpager2demo.MainActivity.onCreate(MainActivity1.kt:29)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:582)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:968)
Caused by: java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat (or a descendant).
at com.google.android.material.internal.ThemeEnforcement.checkTheme(ThemeEnforcement.java:241)
at com.google.android.material.internal.ThemeEnforcement.checkAppCompatTheme(ThemeEnforcement.java:211)
at com.google.android.material.internal.ThemeEnforcement.checkCompatibleTheme(ThemeEnforcement.java:146)
at com.google.android.material.internal.ThemeEnforcement.obtainStyledAttributes(ThemeEnforcement.java:75)
at com.google.android.material.tabs.TabLayout.<init>(TabLayout.java:509)
at com.google.android.material.tabs.TabLayout.<init>(TabLayout.java:489)
... 32 more

上面第二点会导致主题错误,是我始料未及的,而如果context填入this(即activity)则不会出错,于是抱着好奇的心态去了解了一下,结论有两点:

  1. LayoutInflater.from() 中传入 this baseContext/applicationContext 会得到不同的 LayoutInflater 对象,是因为Activity继承自ContextThemeWrapper,而ContextThemeWrapper中重写了getSystemService方法; 具体可以看blog.csdn.net/cj_286/arti…

  2. ContextThemeWrapper 和它的 mBase 成员在 Resource 以及 Theme 相关的行为上是不同的 详情可以查看juejin.cn/post/684490…

回到本文的主题,将工程运行起来,效果如下:

此刻,简单的与TabLayout结合使用示例便完成了,接下来我们学习设置TabLayout的标签和样式

自定义TabLayout的标签和样式

TabLayout属性设置

TabLayout 提供了许多属性,用于自定义标签的外观和行为。以下是一些常用的 TabLayout 属性:

属性描述
tabMode设置 Tab 的模式,可选值为 "fixed"(固定模式)、 "scrollable"(可滚动模式)、"auto"(自动模式,会根据屏幕宽度和Tab个数自动选择固定模式或者可滚动模式),可滚动模式下Tab可以像列表一样滚动
tabGravity设置 Tab 的对齐方式,可选值为 "fill"(填充方式)、"center"(居中方式)、"start"(起始对齐方式)
tabIndicatorColor设置指示器(下划线)的颜色
tabIndicatorHeight设置指示器的高度
tabBackground设置标签的背景
tabTextColor设置标签的文本颜色
tabTextAppearance设置标签的文本样式
tabSelectedTextColor设置选中标签的文本颜色
tabRippleColor设置标签的点击效果颜色
tabIconTint设置标签图标的着色颜色
tabIconSize设置标签图标的尺寸
tabContentStart设置标签内容的起始边距
tabContentEnd设置标签内容的末尾边距

这些属性可以在 XML 布局文件中通过 app 命名空间来设置,例如:

<com.google.android.material.tabs.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:tabMode="scrollable"
    app:tabGravity="center"
    app:tabIndicatorColor="@color/tab_indicator_color"
    app:tabTextAppearance="@style/TabTextAppearance"
    app:tabSelectedTextColor="@color/tab_selected_text_color" />

自定义Tab样式

在大多数应用中,TabLayout自带的属性都不能满足设计的要求,需要我们自定义Tab样式来完成,比如:

为了完成这个需求,我们需要做以下几件事:

  1. Tab指示器高度设为0,从而隐藏下划线指示器
<com.google.android.material.tabs.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    app:tabMode="fixed"
    app:tabGravity="fill"
    app:tabIndicatorHeight="0dp"
    android:layout_marginBottom="10dp"
    app:layout_constraintBottom_toBottomOf="parent"/>
  1. 自定义Tab样式的XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:gravity="center">

    <ImageView
        android:id="@+id/tab_icon"
        android:layout_width="30dp"
        android:layout_height="30dp"
        tools:src="@drawable/ic_instagram_default" />

    <TextView
        android:id="@+id/tab_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:textSize="20sp"
        tools:text="Instagram" />

</LinearLayout>
  1. 定义Tab的text以及选中未选中状态的Icon:
private val tabTitles = listOf("Instagram", "Wechat", "Twitter")
private val tabIcons = listOf(R.drawable.ic_instagram_default, R.drawable.ic_wechat_default, R.drawable.ic_twitter_default)
private val tabSelectedIcons = listOf(R.drawable.ic_instagram_selected, R.drawable.ic_wechat_selected, R.drawable.ic_twitter_selected)
  1. 设置Tab自定义View,并关联ViewPager:
TabLayoutMediator(binding.tabLayout, viewPager) { tab, position ->
    val customTabView = ItemCustomTabBinding.inflate(layoutInflater)
    customTabView.tabText.text = tabTitles[position]
    customTabView.tabIcon.setImageDrawable(getDrawable(tabIcons[position]))
    tab.customView = customTabView.root
}.attach()
  1. 添加Tab选中/未选中监听,请注意:Tab监听要放在Tab初始化(即第4步的设置)的前面,否则初始状态会不符合预期:
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
    @RequiresApi(Build.VERSION_CODES.M)
    override fun onTabSelected(tab: TabLayout.Tab) {
        // Tab 被选中
        val position = tab.position
        tab.customView?.let {
            val title = it.findViewById<TextView>(R.id.tab_text)
            val icon = it.findViewById<ImageView>(R.id.tab_icon)
            title.setTextColor(getColor(R.color.green))
            icon.setImageDrawable(getDrawable(tabSelectedIcons[position]))
        }
    }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onTabUnselected(tab: TabLayout.Tab) {
        // Tab 取消选中
        val position = tab.position
        tab.customView?.let {
            val title = it.findViewById<TextView>(R.id.tab_text)
            val icon = it.findViewById<ImageView>(R.id.tab_icon)
            title.setTextColor(getColor(R.color.black))
            icon.setImageDrawable(getDrawable(tabIcons[position]))
        }
    }

    override fun onTabReselected(tab: TabLayout.Tab) {
        // Tab 被重新选中(点击已选中的 Tab)
    }
})

整体代码如下:


class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

    private val binding by lazy {
        ActivityMainBinding.inflate(
            layoutInflater
        )
    }

    private val fragments = listOf(FirstFragment(), SecondFragment(), ThirdFragment())
    private val fragmentAdapter by lazy { MyFragmentStateAdapter(this, fragments) }
    private val tabTitles = listOf("Instagram", "Wechat", "Twitter")
    private val tabIcons = listOf(R.drawable.ic_instagram_default, R.drawable.ic_wechat_default, R.drawable.ic_twitter_default)
    private val tabSelectedIcons = listOf(R.drawable.ic_instagram_selected, R.drawable.ic_wechat_selected, R.drawable.ic_twitter_selected)


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        initView()
    }

    private fun initView() {
        val viewPager = binding.viewPager
        viewPager.adapter = fragmentAdapter
        val pageTransformer = CustomPageTransformer()
        viewPager.setPageTransformer(pageTransformer)
        viewPager.offscreenPageLimit = 3

        binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
            @RequiresApi(Build.VERSION_CODES.M)
            override fun onTabSelected(tab: TabLayout.Tab) {
                // Tab 被选中
                val position = tab.position
                tab.customView?.let {
                    val title = it.findViewById<TextView>(R.id.tab_text)
                    val icon = it.findViewById<ImageView>(R.id.tab_icon)
                    title.setTextColor(getColor(R.color.green))
                    icon.setImageDrawable(getDrawable(tabSelectedIcons[position]))
                }
            }

            @RequiresApi(Build.VERSION_CODES.M)
            override fun onTabUnselected(tab: TabLayout.Tab) {
                // Tab 取消选中
                val position = tab.position
                tab.customView?.let {
                    val title = it.findViewById<TextView>(R.id.tab_text)
                    val icon = it.findViewById<ImageView>(R.id.tab_icon)
                    title.setTextColor(getColor(R.color.black))
                    icon.setImageDrawable(getDrawable(tabIcons[position]))
                }
            }

            override fun onTabReselected(tab: TabLayout.Tab) {
                // Tab 被重新选中(点击已选中的 Tab)
            }
        })

        TabLayoutMediator(binding.tabLayout, viewPager) { tab, position ->
            val customTabView = ItemCustomTabBinding.inflate(layoutInflater)
            customTabView.tabText.text = tabTitles[position]
            customTabView.tabIcon.setImageDrawable(getDrawable(tabIcons[position]))
            tab.customView = customTabView.root
        }.attach()
    }

    private fun printLog(msg: String) {
        Log.d(TAG, msg)
    }
}

至此,就可以完成自定Tab样式的需求了

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务