前语
为什么会有这篇文章呢,是因为之前关于TabLayout的运用陆陆续续也写了好几篇了,感觉比较分散,且不成体系,写这篇文章的目的便是期望能把各种作用的完成一次性讲齐,所以也有了标题的「看这篇就够了」。
TabLayout
作为导航组件来说,运用场景十分的多,也意味着要满意各式各样的需求。
在作用完成上,有同学会选择自界说View来做,定制性高,但易用性、稳定性、保护性不敢确保,运用官方组件能防止这些不确定性,一是开源,有许多大佬共建,会不停的迭代;二是经过大型app验证,比方google play;有了这两点,基本能够放心大胆的运用官方组件了。
那或许有的同学又会说,道理我都懂,可是不满意需求啊,只能自界说了。是的,前期的api的确不行丰厚,在某些需求的完成上显得绰绰有余,可是google也在不断的迭代,现在为止,常见的款式都能满意。
作用图
简介
- 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内部其实是一个TextView
和ImageView
,增加图标便是给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长按提示文字
长按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.下划线的款式
经过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.下划线的宽度
默许状况下,tabIndicator
的宽度是填充整个Tab的,比方上图中的第一个,咱们能够简略的设置不填充,与文本对齐,即第二个作用
app:tabIndicatorFullWidth="false"
也能够像上一节那样,经过shape自界说tabIndicator的宽度。
8.Tab分割线
/** 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款式
上图中的作用其实是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增加小红点
增加小红点的功用仍是比较常见的,好在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
上一节中咱们完成了小红点作用,那假如一屏显现不行的状况下,怎样提示未展示的信息呢,比方上面咱们怎样把未显现的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宽度自适应
细心的同学会发现,这个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;
}
- requestedTabMinWidth是依据xml设置获取的。
- 假如xml没设置tabMinWidth的状况下,且tabMode是
scrollable
的状况下,会返回默许装备,否则为0,即tabMode为fixed的状况。
体系默许装备scrollableTabMinWidth:
<dimen name="design_tab_scrollable_min_width">72dp</dimen>
在两个字和四个字的标题都存在的状况下,两个字用这个默许宽度就会有剩余的距离,所以会呈现距离不平等的状况,经过设置掩盖默许即可处理。
13.自界说Item View
前面讲到Tab内部完成是一个View,那咱们就能够经过官方供给api(setCustomView)来自界说这个view。
setCustomView的两种方法:
- public Tab setCustomView(@Nullable View view)
- public Tab setCustomView(@LayoutRes int resId)
咱们先编写一个自界说的布局文件,布局文件比较简略,一个LottieAnimationView和TextView。
再经过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
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)
以上即可把TabLayout
和ViewPager
相关起来,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);
}
- 先是创立了TabLayout.TabLayoutOnPageChangeListener,并设置给了viewPager.addOnPageChangeListener。
- 然后又创立了TabLayout.ViewPagerOnTabSelectedListener(viewPager),并传入当时viewPager,然后设置给了addOnTabSelectedListener。
- 所以,经过这种
你来我往
的操作之后,设置TabLayout的选中下标和设置ViewPager的选中下标,其实作用是一毛相同的,因为联动起来了…
别的,FragmentPagerAdapter已经抛弃了,官方引荐运用viewpager2
和 FragmentStateAdapter
代替。
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…
最终
写作不易,感谢点赞支持 ^ – ^