Android 原生 TabLayout 使用全解析
前言
為什么會有這篇文章呢,是因為之前關于TabLayout的使用陸陸續續也寫了好幾篇了,感覺比較分散,且不成體系,寫這篇文章的目的就是希望能把各種效果的實現一次性講齊,所以也有了標題的「看這篇就夠了」。
TabLayout作為導航組件來說,使用場景非常的多,也意味著要滿足各種各樣的需求。
在效果實現上,有同學會選擇自定義View來做,定制性高,但易用性、穩定性、維護性不敢保證,使用官方組件能避免這些不確定性,一是開源,有很多大佬共建,會不停的迭代;二是經過大型app驗證,比如google play;有了這兩點,基本可以放心大膽的使用官方組件了。
那可能有的同學又會說,道理我都懂,可是不滿足需求啊,只能自定義了。是的,早期的api確實不夠豐富,在某些需求的實現上顯得捉襟見肘,但是google也在不斷的迭代,目前為止,常見的樣式都能滿足。
效果圖
簡介
TabLayout:一個橫向可滑動的菜單導航ui組件。
Tab:TabLayout中的item,可以通過newTab()創建。
TabView:Tab的實例,是一個包含ImageView和TextView的線性布局。
TabItem:一種特殊的“視圖”,在TabLayout中可以顯式聲明Tab。
官方文檔:
https://developer.android.google.cn/reference/com/google/android/material/tabs/TabLayout?hl=en功能拆解
Material Design 組件最新正式版依賴:
implementation 'com.google.android.material:material:1.5.0'1.基礎實現
1.1 xml動態寫法
<com.google.android.material.tabs.TabLayoutandroid: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.TabLayoutandroid:layout_height="wrap_content"android:layout_width="match_parent"><com.google.android.material.tabs.TabItemandroid:text="@string/tab_text"/><com.google.android.material.tabs.TabItemandroid: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 以上 設置空textif (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"><itemandroid: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? LinearLayoutlinearLayout?.let {it.showDividers = LinearLayout.SHOW_DIVIDER_MIDDLEit.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.TabLayoutandroid: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.REDmaxCharacterCount = 3number = 99999badgeTextColor = 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。
https://blog.csdn.net/yechaoa/article/details/122272822?spm=1001.2014.3001.550111.獲取隱藏的Tab
上一節中我們實現了小紅點效果,那如果一屏顯示不夠的情況下,如何提示未展示的信息呢,比如上面我們如何把未顯示的tab且有數字的Tab提示出來呢?常見的解決方案都是在尾部加一個紅點提示。
那么問題來了,如何判斷某一個Tab是否可見呢,翻看了源碼,可惜并沒有提供相應的api,那只能我們自己實現了。
我們前面添加小紅點是根據Tab添加的,Tab內部實現也是一個view,那view就可以判斷其是否可見。
private fun isShowDot(): Boolean {var showIndex = 0var tipCount = 0companyMap.keys.forEachIndexed { index, _ ->mBinding.tabLayout7.getTabAt(index)?.let { tab ->val tabView = tab.view as LinearLayoutval 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 = showIndexreturn 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.INVISIBLEmBinding.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 itreturn requestedTabMinWidth;}// Else, we'll use the default valuereturn (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的兩種方式:
1. public Tab setCustomView(@Nullable View view)
2. 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 = stab.customView = viewmBinding.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.LottieAnimationViewandroid:id="@+id/lav_tab_img"android:layout_width="30dp"android:layout_height="30dp"app:lottie_colorFilter="@color/black"app:lottie_rawRes="@raw/anim_confetti" /><TextViewandroid: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?) {} })這里通過兩個擴展方法分別處理不同的狀態。
1. 選中狀態,播放動畫并設置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)}}2. 未選中狀態,停止動畫并還原初始狀態,然后設置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.blackval 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。
https://lordicon.com/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) {// 步驟1this.pageChangeListener = new TabLayout.TabLayoutOnPageChangeListener(this);}this.pageChangeListener.reset();viewPager.addOnPageChangeListener(this.pageChangeListener);// 步驟2this.currentVpSelectedListener = new TabLayout.ViewPagerOnTabSelectedListener(viewPager);// 步驟3this.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);// 步驟4viewPager.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已經廢棄了,官方推薦使用viewpager2 和 FragmentStateAdapter 代替。
Deprecated Switch to androidx.viewpager2.widget.ViewPager2 and use androidx.viewpager2.adapter.FragmentStateAdapter instead.16.常用API整理
16.1 TabLayout
16.2 TabLayout.Tab
16.3 BadgeDrawable
Github:
https://github.com/yechaoa/MaterialDesign總結
以上是生活随笔為你收集整理的Android 原生 TabLayout 使用全解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ANN:Asymmetric Non-l
- 下一篇: 谷歌Android无障碍套件,andro