前语

为什么会有这篇文章呢,是因为之前关于TabLayout的运用陆陆续续也写了好几篇了,感觉比较分散,且不成体系,写这篇文章的目的便是期望能把各种作用的完成一次性讲齐,所以也有了标题的「看这篇就够了」。

TabLayout作为导航组件来说,运用场景十分的多,也意味着要满意各式各样的需求。

在作用完成上,有同学会选择自界说View来做,定制性高,但易用性、稳定性、保护性不敢确保,运用官方组件能防止这些不确定性,一是开源,有许多大佬共建,会不停的迭代;二是经过大型app验证,比方google play;有了这两点,基本能够放心大胆的运用官方组件了。

那或许有的同学又会说,道理我都懂,可是不满意需求啊,只能自界说了。是的,前期的api的确不行丰厚,在某些需求的完成上显得绰绰有余,可是google也在不断的迭代,现在为止,常见的款式都能满意。

作用图

Android原生TabLayout使用全解析,看这篇就够了

简介

Android原生TabLayout使用全解析,看这篇就够了

  • TabLayout:一个横向可滑动的菜单导航ui组件
  • Tab:TabLayout中的item,能够经过newTab()创立
  • TabView:Tab的实例,是一个包括ImageView和TextView的线性布局
  • TabItem:一种特其他“视图”,在TabLayout中能够显式声明Tab

官方文档

功用拆解

Material Design 组件最新正式版依靠:

implementation 'com.google.android.material:material:1.5.0'

1.根底完成

1.1 xml动态写法

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        app:tabIndicatorColor="@color/colorPrimary"
        app:tabMaxWidth="200dp"
        app:tabMinWidth="100dp"
        app:tabMode="fixed"
        app:tabSelectedTextColor="@color/colorPrimary"
        app:tabTextColor="@color/gray" />

只写一个Layout,item能够合作ViewPager来生成。

1.2 xml静态写法

 <com.google.android.material.tabs.TabLayout
         android:layout_height="wrap_content"
         android:layout_width="match_parent">
     <com.google.android.material.tabs.TabItem
             android:text="@string/tab_text"/>
     <com.google.android.material.tabs.TabItem
             android:icon="@drawable/ic_android"/>
 </com.google.android.material.tabs.TabLayout>

归于固定写法,比方咱们十分确定item有几个,能够经过TabItem显式声明。

1.3 kotlin/java代码写法

    val tab = mBinding.tabLayout7.newTab()
    tab.text = it.key
    //...
    mBinding.tabLayout7.addTab(tab)

这种状况适合Tab的数据是动态的,比方接口数据回来之后,再创立Tab并增加到TabLayout中。

2.增加图标

mBinding.tabLayout2.getTabAt(index)?.setIcon(R.mipmap.ic_launcher)

获取Tab然后设置icon。

Tab内部其实是一个TextViewImageView,增加图标便是给ImageView设置icon。

3.字体大小、加粗

经过app:tabTextAppearance给TabLayout设置文本款式

    <com.google.android.material.tabs.TabLayout
		...
        app:tabTextAppearance="@style/MyTabLayout"
		/>

style:

    <style name="MyTabLayout">
        <item name="android:textSize">20sp</item>
        <item name="android:textStyle">bold</item>
        <item name="android:textAllCaps">false</item>
    </style>

比方这里设置了字体大小和加粗。

默许字体大小14sp

<dimen name="design_tab_text_size">14sp</dimen>

4.去掉Tab长按提示文字

Android原生TabLayout使用全解析,看这篇就够了

长按Tab时会有一个提示文字,相似Toast相同。

    /**
     * 躲藏长按显现文本
     */
    private fun hideToolTipText(tab: TabLayout.Tab) {
        // 取消长按事情
        tab.view.isLongClickable = false
        // api 26 以上 设置空text
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
            tab.view.tooltipText = ""
        }
    }

能够取消长按事情,在api26以上也能够设置提示文本为空。

5.去掉下划线indicator

app:tabIndicatorHeight="0dp"

设置高度为0即可。

注意,单纯设置tabIndicatorColor为通明,其实不准确,默许仍是有2dp的,根本瞒不过射鸡师的眼睛。

6.下划线的款式

Android原生TabLayout使用全解析,看这篇就够了

经过app:tabIndicator能够设置自界说的款式,比方经过shape设置圆角和宽度。

    <com.google.android.material.tabs.TabLayout
        ...
        app:tabIndicator="@drawable/shape_tab_indicator"
        app:tabIndicatorColor="@color/colorPrimary"
		/>

注意:Indicator的色彩在shape中设置是无效的,需求经过app:tabIndicatorColor设置才能够

shape:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:width="15dp"
        android:height="5dp"
        android:gravity="center">
        <shape>
            <corners android:radius="5dp" />
            <!--color无效,源码用tabIndicatorColor-->
            <solid android:color="@color/colorPrimary" />
        </shape>
    </item>
</layer-list>

7.下划线的宽度

Android原生TabLayout使用全解析,看这篇就够了

默许状况下,tabIndicator的宽度是填充整个Tab的,比方上图中的第一个,咱们能够简略的设置不填充,与文本对齐,即第二个作用

app:tabIndicatorFullWidth="false"

也能够像上一节那样,经过shape自界说tabIndicator的宽度。

8.Tab分割线

Android原生TabLayout使用全解析,看这篇就够了


  /** A {@link LinearLayout} containing {@link Tab} instances for use with {@link TabLayout}. */
  public final class TabView extends LinearLayout {
  }

经过源码能够看到内部完成TabView承继至LinearLayout,咱们知道LinearLayout是能够给子view设置分割线的,那咱们就能够经过遍历来增加分割线

        //设置 分割线
        for (index in 0..mBinding.tabLayout4.tabCount) {
            val linearLayout = mBinding.tabLayout4.getChildAt(index) as? LinearLayout
            linearLayout?.let {
                it.showDividers = LinearLayout.SHOW_DIVIDER_MIDDLE
                it.dividerDrawable = ContextCompat.getDrawable(this, R.drawable.shape_tab_divider)
                it.dividerPadding = 30
            }
        }

shape_tab_divider:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/colorPrimary" />
    <size android:width="1dp" android:height="10dp" />
</shape>

9.TabLayout款式

Android原生TabLayout使用全解析,看这篇就够了

上图中的作用其实是TabLayout款式+tabIndicator款式形成的一个「全体」的作用。

TabLayout是两边半圆的一个长条,这个咱们经过编写shape设置给其布景即可完成。

shape_tab_bg:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="999dp" />
    <solid android:color="@color/colorPrimary" />
</shape>

这个作用的要害在于tabIndicator的高度与TabLayout的高度相同,所以二者高度设置共同即可。

shape_full_tab_indicator:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:gravity="center" android:top="0.5dp" android:bottom="0.5dp">
        <shape>
            <!-- 上下边距合计1dp 高度减少1dp -->
            <size android:height="41dp" />
            <corners android:radius="999dp" />
            <solid android:color="@color/white" />
        </shape>
    </item>
</layer-list>

TabLayout:

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout6"
        android:layout_width="wrap_content"
        android:layout_height="42dp"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:background="@drawable/shape_tab_bg"
        app:tabIndicator="@drawable/shape_full_tab_indicator"
        app:tabIndicatorColor="@color/white"
        app:tabIndicatorFullWidth="true"
        app:tabIndicatorHeight="42dp"
        app:tabMinWidth="96dp"
        app:tabMode="fixed"
        app:tabSelectedTextColor="@color/colorPrimary"
        app:tabTextColor="@color/black" />

10.Tab增加小红点

Android原生TabLayout使用全解析,看这篇就够了

增加小红点的功用仍是比较常见的,好在TabLayout也供给了这种才能,其实增加起来也十分简略,难在未知。

能够设置带数字的红点,也能够设置没有数字单纯的一个点。

经过getOrCreateBadge能够对红点进行简略的装备:

        // 数字
        mBinding.tabLayout5.getTabAt(defaultIndex)?.let { tab ->
            tab.orCreateBadge.apply {
                backgroundColor = Color.RED
                maxCharacterCount = 3
                number = 99999
                badgeTextColor = Color.WHITE
            }
        }
        // 红点
        mBinding.tabLayout5.getTabAt(1)?.let { tab ->
            tab.orCreateBadge.backgroundColor = ContextCompat.getColor(this, R.color.orange)
        }

getOrCreateBadge实际上是获取或创立BadgeDrawable

经过源码发现,BadgeDrawable除了TabLayout引用之外,还有NavigationBarItemView、NavigationBarMenuView、NavigationBarView,意味着它们也相同具有着小红点这种才能。其实其他view也是能够具有的。

关于小红点这里就不展开了,十分引荐查看我之前写的这篇:【涨姿势】你没用过的BadgeDrawable

Author:yechaoa

11.获取躲藏的Tab

Android原生TabLayout使用全解析,看这篇就够了

上一节中咱们完成了小红点作用,那假如一屏显现不行的状况下,怎样提示未展示的信息呢,比方上面咱们怎样把未显现的tab且有数字的Tab提示出来呢?常见的处理方案都是在尾部加一个红点提示。

那么问题来了,怎样判别某一个Tab是否可见呢,翻看了源码,惋惜并没有供给相应的api,那只能咱们自己完成了。

咱们前面增加小红点是依据Tab增加的,Tab内部完成也是一个view,那view就能够判别其是否可见。

    private fun isShowDot(): Boolean {
        var showIndex = 0
        var tipCount = 0
        companyMap.keys.forEachIndexed { index, _ ->
            mBinding.tabLayout7.getTabAt(index)?.let { tab ->
                val tabView = tab.view as LinearLayout
                val rect = Rect()
                val visible = tabView.getLocalVisibleRect(rect)
                // 可见规模小于80%也在核算规模之内,剩余20%宽度满足红点透出(可自界说)
                if (visible && rect.right > tab.view.width * 0.8) {
                    showIndex = index
                } else {
                    //if (index > showIndex) // 任意一个有count的tab躲藏就会显现,比方第一个在滑动过程中会躲藏,也在核算规模之内
                    if (index > lastShowIndex) { // 只检测右侧躲藏且有count的tab 才在核算规模之内
                        tab.badge?.let { tipCount += it.number }
                    }
                }
            }
        }
        lastShowIndex = showIndex
        return tipCount > 0
    }

上面的办法中便是判别是否需求显现右侧提示的小红点。

核算规则:Tab不可见,且Tab上的红点数字大于0的即在核算规模之内。

这里有一个优化的点,比方上图中的“腾讯”Tab,它是可见的,可是红点不可见,那么问题就来了,假如咱们没有提示到,是很容易产生客诉的,所以这里在核算的时候也加了一个条件,便是可见规模小于80%也在核算规模之内,剩余20%的宽度是满足Tab上的红点透出的(也可自界说)。

一起在TabLayout滑动的过程中也应该加上判别显现的逻辑:

        // mBinding.tabLayout7.setOnScrollChangeListener() // min api 23 (6.0)
        // 适配 5.0  滑动过程中判别右侧小红点是否需求显现
        mBinding.tabLayout7.viewTreeObserver.addOnScrollChangedListener {
            mBinding.vArrowDot.visibility = if (isShowDot()) View.VISIBLE else View.INVISIBLE
        }

还有初始化时的判别逻辑:

    override fun onResume() {
        super.onResume()
        // 初始化判别右侧小红点是否需求显现
        mBinding.tabLayout7.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                mBinding.vArrowDot.visibility = if (isShowDot()) View.VISIBLE else View.INVISIBLE
                mBinding.tabLayout7.viewTreeObserver.removeOnGlobalLayoutListener(this)
            }
        })
    }

12.Tab宽度自适应

Android原生TabLayout使用全解析,看这篇就够了

细心的同学会发现,这个TabLayout的item左右距离都是相同的,不论标题是两个字仍是四个字的,左右距离都是持平的,而实际上的作用是两个字的Tab要比四个字的Tab左右距离要大一些的,那这个作用是怎样完成的呢?

实际上是咱们设置了tabMinWidth

app:tabMinWidth="50dp"

源码中默许的是:

  private int getTabMinWidth() {
    if (requestedTabMinWidth != INVALID_WIDTH) {
      // If we have been given a min width, use it
      return requestedTabMinWidth;
    }
    // Else, we'll use the default value
    return (mode == MODE_SCROLLABLE || mode == MODE_AUTO) ? scrollableTabMinWidth : 0;
  }
  1. requestedTabMinWidth是依据xml设置获取的。
  2. 假如xml没设置tabMinWidth的状况下,且tabMode是scrollable的状况下,会返回默许装备,否则为0,即tabMode为fixed的状况。

体系默许装备scrollableTabMinWidth:

<dimen name="design_tab_scrollable_min_width">72dp</dimen>

在两个字和四个字的标题都存在的状况下,两个字用这个默许宽度就会有剩余的距离,所以会呈现距离不平等的状况,经过设置掩盖默许即可处理。

13.自界说Item View

Android原生TabLayout使用全解析,看这篇就够了

前面讲到Tab内部完成是一个View,那咱们就能够经过官方供给api(setCustomView)来自界说这个view。

setCustomView的两种方法:

  • public Tab setCustomView(@Nullable View view)
  • public Tab setCustomView(@LayoutRes int resId)

咱们先编写一个自界说的布局文件,布局文件比较简略,一个LottieAnimationView和TextView。

Android原生TabLayout使用全解析,看这篇就够了

再经过Tab增加进去即可。

        val animMap = mapOf("party" to R.raw.anim_confetti, "pizza" to R.raw.anim_pizza, "apple" to R.raw.anim_apple)
        animMap.keys.forEach { s ->
            val tab = mBinding.tabLayout8.newTab()
            val view = LayoutInflater.from(this).inflate(R.layout.item_tab, null)
            val imageView = view.findViewById<LottieAnimationView>(R.id.lav_tab_img)
            val textView = view.findViewById<TextView>(R.id.tv_tab_text)
            imageView.setAnimation(animMap[s]!!)
            imageView.setColorFilter(Color.BLUE)
            textView.text = s
            tab.customView = view
            mBinding.tabLayout8.addTab(tab)
        }

14.运用Lottie

Android原生TabLayout使用全解析,看这篇就够了

Lottie是一个能够在多平台展示动画的库,信任许多同学都已经用过了,就不详细展开了,感兴趣的能够查看Lottie官方文档。

Lottie依靠:

implementation "com.airbnb.android:lottie:5.0.1"

上一节中咱们完成了自界说TabLayout的Item View,在这个自界说的布局中,咱们用LottieAnimationView来承载动画的展示。

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/item_tab"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="vertical">
    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/lav_tab_img"
        android:layout_width="30dp"
        android:layout_height="30dp"
        app:lottie_colorFilter="@color/black"
        app:lottie_rawRes="@raw/anim_confetti" />
    <TextView
        android:id="@+id/tv_tab_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/app_name"
        android:textColor="@color/black"
        android:textSize="14sp" />
</androidx.appcompat.widget.LinearLayoutCompat>

增加的方法也在上一节中讲过了,咱们只需求控制好选中、未选中的状况即可。

        mBinding.tabLayout8.addOnTabSelectedListener(object : OnTabSelectedListener {
            override fun onTabSelected(tab: TabLayout.Tab?) {
                tab?.setSelected()
                tab?.let { mBinding.viewPager.currentItem = it.position }
            }
            override fun onTabUnselected(tab: TabLayout.Tab?) {
                tab?.setUnselected()
            }
            override fun onTabReselected(tab: TabLayout.Tab?) {
            }
        })

这里经过两个扩展办法分别处理不同的状况。

  • 选中状况,播放动画并设置icon色彩
    /**
     * 选中状况
     */
    fun TabLayout.Tab.setSelected() {
        this.customView?.let {
            val textView = it.findViewById<TextView>(R.id.tv_tab_text)
            val selectedColor = ContextCompat.getColor(this@TabLayoutActivity, R.color.colorPrimary)
            textView.setTextColor(selectedColor)
            val imageView = it.findViewById<LottieAnimationView>(R.id.lav_tab_img)
            if (!imageView.isAnimating) {
                imageView.playAnimation()
            }
            setLottieColor(imageView, true)
        }
    }
  • 未选中状况,停止动画并复原初始状况,然后设置icon色彩
    /**
     * 未选中状况
     */
    fun TabLayout.Tab.setUnselected() {
        this.customView?.let {
            val textView = it.findViewById<TextView>(R.id.tv_tab_text)
            val unselectedColor = ContextCompat.getColor(this@TabLayoutActivity, R.color.black)
            textView.setTextColor(unselectedColor)
            val imageView = it.findViewById<LottieAnimationView>(R.id.lav_tab_img)
            if (imageView.isAnimating) {
                imageView.cancelAnimation()
                imageView.progress = 0f // 复原初始状况
            }
            setLottieColor(imageView, false)
        }
    }

关于修正lottie icon的色彩,现在网上的答案参差不齐,仍是源码来的直接。

源码:

    if (ta.hasValue(R.styleable.LottieAnimationView_lottie_colorFilter)) {
      int colorRes = ta.getResourceId(R.styleable.LottieAnimationView_lottie_colorFilter, -1);
      ColorStateList csl = AppCompatResources.getColorStateList(getContext(), colorRes);
      SimpleColorFilter filter = new SimpleColorFilter(csl.getDefaultColor());
      KeyPath keyPath = new KeyPath("**");
      LottieValueCallback<ColorFilter> callback = new LottieValueCallback<>(filter);
      addValueCallback(keyPath, LottieProperty.COLOR_FILTER, callback);
    }

所以直接借鉴即可:

    /**
     * set lottie icon color
     */
    private fun setLottieColor(imageView: LottieAnimationView?, isSelected: Boolean) {
        imageView?.let {
            val color = if (isSelected) R.color.colorPrimary else R.color.black
            val csl = AppCompatResources.getColorStateList(this@TabLayoutActivity, color)
            val filter = SimpleColorFilter(csl.defaultColor)
            val keyPath = KeyPath("**")
            val callback = LottieValueCallback<ColorFilter>(filter)
            it.addValueCallback(keyPath, LottieProperty.COLOR_FILTER, callback)
        }
    }

动画文件的下载网站引荐: lordicon

15.相关ViewPager

15.1 编写FragmentPagerAdapter

    private inner class SimpleFragmentPagerAdapter constructor(fm: FragmentManager) :
        FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
        private val tabTitles = arrayOf("Android", "Kotlin", "Flutter")
        private val fragment = arrayOf(Fragment1(), Fragment2(), Fragment3())
        override fun getItem(position: Int): Fragment {
            return fragment[position]
        }
        override fun getCount(): Int {
            return fragment.size
        }
        override fun getPageTitle(position: Int): CharSequence {
            return tabTitles[position]
        }
    }

15.2 给ViewPager设置Adapter

mBinding.viewPager.adapter = SimpleFragmentPagerAdapter(supportFragmentManager)

15.3 给TabLayout相关ViewPager

mBinding.tabLayout1.setupWithViewPager(mBinding.viewPager)

以上即可把TabLayoutViewPager相关起来,TabLayout的Tab也会由FragmentPagerAdapter中的标题主动生成。

15.4 setupWithViewPager源码剖析

究竟是怎样相关起来的呢? 下面是setupWithViewPager中的部分源码:

        if (viewPager != null) {
            this.viewPager = viewPager;
            if (this.pageChangeListener == null) {
            	// 过程1
                this.pageChangeListener = new TabLayout.TabLayoutOnPageChangeListener(this);
            }
            this.pageChangeListener.reset();
            viewPager.addOnPageChangeListener(this.pageChangeListener);
            // 过程2
            this.currentVpSelectedListener = new TabLayout.ViewPagerOnTabSelectedListener(viewPager);
            // 过程3
            this.addOnTabSelectedListener(this.currentVpSelectedListener);
            PagerAdapter adapter = viewPager.getAdapter();
            if (adapter != null) {
                this.setPagerAdapter(adapter, autoRefresh);
            }
            if (this.adapterChangeListener == null) {
                this.adapterChangeListener = new TabLayout.AdapterChangeListener();
            }
            this.adapterChangeListener.setAutoRefresh(autoRefresh);
            // 过程4
            viewPager.addOnAdapterChangeListener(this.adapterChangeListener);
            this.setScrollPosition(viewPager.getCurrentItem(), 0.0F, true);
        }
  1. 先是创立了TabLayout.TabLayoutOnPageChangeListener,并设置给了viewPager.addOnPageChangeListener。
  2. 然后又创立了TabLayout.ViewPagerOnTabSelectedListener(viewPager),并传入当时viewPager,然后设置给了addOnTabSelectedListener。
  3. 所以,经过这种你来我往的操作之后,设置TabLayout的选中下标和设置ViewPager的选中下标,其实作用是一毛相同的,因为联动起来了…

别的,FragmentPagerAdapter已经抛弃了,官方引荐运用viewpager2FragmentStateAdapter 代替。

Deprecated Switch to androidx.viewpager2.widget.ViewPager2 and use androidx.viewpager2.adapter.FragmentStateAdapter instead.

16.常用API整理

16.1 TabLayout

API 意义
background TabLayout布景色彩
tabIndicator 指示器(一般下划线)
tabIndicatorColor 指示器色彩
tabIndicatorHeight 指示器高度,不显现写0dp
tabIndicatorFullWidth 指示器宽度是否撑满item
tabMode tab显现方法,1.auto主动,2.fixed固定宽度,3.scrollable可滑动
tabSelectedTextColor tab选中文字色彩
tabTextColor tab未选中文字色彩
tabRippleColor tab点击作用色彩
tabGravity tab对齐方法
tabTextAppearance tab文本款式,可引用style
tabMaxWidth tab最大宽度
tabMinWidth tab最小宽度
setupWithViewPager tabLayout相关ViewPager
addOnTabSelectedListener tab选中监听事情

16.2 TabLayout.Tab

API 意义
setCustomView 设置tab自界说view
setIcon 设置tab icon
setText 设置tab文本
getOrCreateBadge 获取或创立badge(小红点)
removeBadge 移除badge(小红点)
select 设置tab选中
isSelected 获取tab选中状况

16.3 BadgeDrawable

API 意义
setVisible 设置显现状况
setBackgroundColor 设置小红点布景色彩
getBadgeTextColor 设置小红点文本色彩
setNumber 设置小红点显现数量
clearNumber 铲除小红点数量
setBadgeGravity 设置小红点方位对齐方法

Github

github.com/yechaoa/Mat…

最终

写作不易,感谢点赞支持 ^ – ^