Android原生TabLayout使用全解析,看這篇就夠了

hfhsdgzsdgsdg發表於2023-02-21

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.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" />

1

2

3

4

5

6

7

8

9

10

11

只寫一個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>

1

2

3

4

5

6

7

8

9

10

11

屬於固定寫法,比如我們非常確定item有幾個,可以透過TabItem顯式宣告。


1.3 kotlin/java程式碼寫法

    val tab = mBinding.tabLayout7.newTab()

    tab.text = it.key

    //...

    mBinding.tabLayout7.addTab(tab)

1

2

3

4

這種情況適合Tab的資料是動態的,比如介面資料回來之後,再建立Tab並新增到TabLayout中。


2.新增圖示

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

1

獲取Tab然後設定icon。


Tab內部其實是一個TextView和ImageView,新增圖示就是給ImageView設定icon。


3.字型大小、加粗

透過app:tabTextAppearance給TabLayout設定文字樣式


    <com.google.android.material.tabs.TabLayout

...

        app:tabTextAppearance="@style/MyTabLayout"

/>

1

2

3

4

style:


    <style name="MyTabLayout">

        <item name="android:textSize">20sp</item>

        <item name="android:textStyle">bold</item>

        <item name="android:textAllCaps">false</item>

    </style>

1

2

3

4

5

比如這裡設定了字型大小和加粗。


預設字型大小14sp:


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

1

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 = ""

        }

    }

1

2

3

4

5

6

7

8

9

10

11

可以取消長按事件,在api26以上也可以設定提示文字為空。


5.去掉下劃線indicator

app:tabIndicatorHeight="0dp"

1

設定高度為0即可。


注意,單純設定tabIndicatorColor為透明,其實不準確,預設還是有2dp的,根本瞞不過射雞師的眼睛。


6.下劃線的樣式



透過app:tabIndicator可以設定自定義的樣式,比如透過shape設定圓角和寬度。


    <com.google.android.material.tabs.TabLayout

        ...

        app:tabIndicator="@drawable/shape_tab_indicator"

        app:tabIndicatorColor="@color/colorPrimary"

/>

1

2

3

4

5

注意:Indicator的顏色在shape中設定是無效的,需要透過app:tabIndicatorColor設定才可以


shape:


<?xml version="1.0" encoding="utf-8"?>

<layer-list xmlns: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>

1

2

3

4

5

6

7

8

9

10

11

12

13

7.下劃線的寬度



預設情況下,tabIndicator的寬度是填充整個Tab的,比如上圖中的第一個,我們可以簡單的設定不填充,與文字對齊,即第二個效果


app:tabIndicatorFullWidth="false"

1

也可以像上一節那樣,透過shape自定義tabIndicator的寬度。


8.Tab分割線




  /** A {@link LinearLayout} containing {@link Tab} instances for use with {@link TabLayout}. */

  public final class TabView extends LinearLayout {

  

  }

1

2

3

4

5

透過原始碼可以看到內部實現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

            }

        }

1

2

3

4

5

6

7

8

9

shape_tab_divider:


<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android="

    <solid android:color="@color/colorPrimary" />

    <size android:width="1dp" android:height="10dp" />

</shape>

1

2

3

4

5

9.TabLayout樣式


上圖中的效果其實是TabLayout樣式+tabIndicator樣式形成的一個「整體」的效果。


TabLayout是兩邊半圓的一個長條,這個我們透過編寫shape設定給其背景即可實現。


shape_tab_bg:


<?xml version="1.0" encoding="utf-8"?>

<shape xmlns:android="

    <corners android:radius="999dp" />

    <solid android:color="@color/colorPrimary" />

</shape>

1

2

3

4

5

這個效果的關鍵在於tabIndicator的高度與TabLayout的高度相同,所以二者高度設定一致即可。


shape_full_tab_indicator:


<?xml version="1.0" encoding="utf-8"?>

<layer-list xmlns: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>

1

2

3

4

5

6

7

8

9

10

11

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" />

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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)

        }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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

    }


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

上面的方法中就是判斷是否需要顯示右側提示的小紅點。


計算規則: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

        }

1

2

3

4

5

還有初始化時的判斷邏輯:


    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)

            }

        })

    }

1

2

3

4

5

6

7

8

9

10

12.Tab寬度自適應


細心的同學會發現,這個TabLayout的item左右間距都是一樣的,不管標題是兩個字還是四個字的,左右間距都是相等的,而實際上的效果是兩個字的Tab要比四個字的Tab左右間距要大一些的,那這個效果是怎麼實現的呢?


實際上是我們設定了tabMinWidth:


app:tabMinWidth="50dp"

1

原始碼中預設的是:


  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

2

3

4

5

6

7

8

requestedTabMinWidth是根據xml設定獲取的。

假如xml沒設定tabMinWidth的情況下,且tabMode是scrollable的情況下,會返回預設配置,否則為0,即tabMode為fixed的情況。

系統預設配置scrollableTabMinWidth:


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

1

在兩個字和四個字的標題都存在的情況下,兩個字用這個預設寬度就會有多餘的間距,所以會出現間距不均等的情況,透過設定覆蓋預設即可解決。


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)

        }

1

2

3

4

5

6

7

8

9

10

11

12

13

14.使用Lottie



Lottie是一個可以在多平臺展示動畫的庫,相信很多同學都已經用過了,就不詳細展開了,感興趣的可以檢視Lottie官方檔案。


Lottie依賴:


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

1

上一節中我們實現了自定義TabLayout的Item View,在這個自定義的佈局中,我們用LottieAnimationView來承載動畫的展示。


<?xml version="1.0" encoding="utf-8"?>

<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="

    xmlns:app="

    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>


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

新增的方式也在上一節中講過了,我們只需要控制好選中、未選中的狀態即可。


        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

2

3

4

5

6

7

8

9

10

11

12

13

14

這裡透過兩個擴充套件方法分別處理不同的狀態。


選中狀態,播放動畫並設定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)

        }

    }


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

未選中狀態,停止動畫並還原初始狀態,然後設定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)

        }

    }


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

關於修改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);

    }

1

2

3

4

5

6

7

8

所以直接借鑑即可:


    /**

     * 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)

        }

    }

1

2

3

4

5

6

7

8

9

10

11

12

13

動畫檔案的下載網站推薦: 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]

        }

    }


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

15.2 給ViewPager設定Adapter

mBinding.viewPager.adapter = SimpleFragmentPagerAdapter(supportFragmentManager)

1

15.3 給TabLayout關聯ViewPager

mBinding.tabLayout1.setupWithViewPager(mBinding.viewPager)

1

以上即可把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);

        }


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

先是建立了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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70026759/viewspace-2936114/,如需轉載,請註明出處,否則將追究法律責任。

相關文章