How Can We Configure TabLayout For a Fixed Visible Number of Scrollable Tabs?

Viewed 962

In a project, we are using the Material Design Components' TabLayout with a ViewPager. There are 14 tabs, and we want 7 of those tabs to be visible at a time in the TabLayout. The tab content is narrow enough that we are sure that 7 will not be too many, and the design team wants a consistent number of tabs showing up regardless of screen width (tabs represent days of the week).

None of the pre-defined tab modes seem to match this:

  • MODE_FIXED and MODE_AUTO control the number of visible tabs... by showing all of them
  • MODE_SCROLLABLE allows the tabs to scroll... but then we do not have control over the number of visible tabs

Is there a way of accomplishing this that does not involve non-maintainable hacks, such as using reflection to tinker with tabPaddingStart at runtime, or iterating over the tab widgets and adjusting their LayoutParams?

I have seen this question, but the explanation is lacking — in particular, it is unclear how to use app:tabMaxWidth for what should be a dynamic value at runtime. Also, that question is about the older Design Support Library, which may differ somewhat with MDC's implementation.

2 Answers

There several ways to show a fixed number of tabs irrespective of screen width that could work, but the desired functionality is really locked down. Most notably, if getTabMinWidth() in TabLayout were not private, an easy solution would be to override that method in a custom TabLayout view.

The following is along that lines of, and maybe exactly, what Eugen Pechanec suggested in a comment above which involves a custom view for the tabs.

First the base layout.

activity_main.xml

tabMinWidth, tabPaddingEnd and tabPaddingStart are all set to 0dp. tabMinWidth has a default value that is probably too large for our needs. The padding could be set to other than zero, but I would rather deal with that in the custom views for the tabs.

Nothing really happens with the ViewPager.

<androidx.appcompat.widget.LinearLayoutCompat 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity">

    <androidx.viewpager.widget.ViewPager
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:tabMinWidth="0dp"
            app:tabMode="scrollable"
            app:tabPaddingEnd="0dp"
            app:tabPaddingStart="0dp" />
    </androidx.viewpager.widget.ViewPager>
</androidx.appcompat.widget.LinearLayoutCompat>

custom_tab.xml

<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@android:id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Tab x"
        android:textAppearance="@style/TextAppearance.AppCompat.Body2" />

<!--    If an icon is needed. -->
<!--    <ImageView-->
<!--        android:id="@android:id/icon"-->
<!--        android:layout_width="48dp"-->
<!--        android:layout_height="48dp"-->
<!--        android:scaleType="centerCrop"-->
<!--        android:src="@drawable/ic_launcher_foreground" />-->
</LinearLayout>

MainActivity.kt

Tabs are loaded into the TabLayout one-by-one setting the custom views as we go. The minimum width of the custom views is set to 1/7 of the width of the TabLayout. Setting the minimum width suffices since it is given that the width needed will always be less than or equal to 1/7 of the total width.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val tabLayout = findViewById<TabLayout>(R.id.tabs)
        tabLayout.doOnLayout {
            val tabWidth = tabLayout.width / 7
            for (i in 1..14) {
                tabLayout.newTab().run {
                    setCustomView(R.layout.custom_tab)
                    customView?.minimumWidth = tabWidth
                    setText("Tab $i")
                    tabLayout.addTab(this)
                }
            }
        }

    }
}

If custom tabs are used anyway, I think that this is a reasonable solution. However, it is little better (IMO) than iterating over the TabLayout children and setting widths.

Finally, a couple of pictures:

Portrait Orientation

Landscape Orientation

I had to do the same and I investigated the problem with the special attention. Spoiler: I failed - currently what you want to achieve is not possible via stock TabLayout and clean Android way.

There are hacks though)

  • The most adequate one is to use library that allows that - for example this

  • Although you mentioned it and do not wan't to do it - iterating TabLayout children changing their LayoutParams is the next by adequacy way to do it. This method utilizes the public methods Android provides and does not require usage of some not very good techniques. Speaking of which...

*Reflection start

  • Change tabPaddingStart or requestedTabMinWidth along with requestedTabMaxWidth. It is bad. I won't even start why. It is the most inadequate one way in this list. It is still a way to fix it though.

  • This one is the combination of clean Android way and TabLayout parameters app:tabMaxWidth and app:tabMinWidth and reflection. The reflection is a bit different in this case - it is almost normal(as much as a reflection can be). I propose to create an integer android resource in integers.xml called tabWidth or something (I'm sure you already know where I am going) and on application start you replace it via reflection with the value of your screenWidth divided by 7(or less depending on your paddings).

*Reflection finish

  • There is one more way and it is Android clean but quite inadequate nevertheless. You can use a combination of ViewPagers and/or fragments. The easiest one is a ViewPager of two ViewPagers and two TabLayouts each with 7 pages and 7 tabs accordingly. You might need to adjust gestures handler though. And there can be many more combinations.

I will not include a way of you writing the TabLayout by yourself with help of RecyclerView and SnapHelper because it always is an option to write something by yourself in Android... and waste tremendous amount of time.

I know that it is not an answer. Moreover it is rather list of how NOT to do stuff in Android but sometimes we need to choose the dark side...

Hope it helps though.

Related