TabLayout not filling width when tabMode set to 'scrollable'
AndroidAndroid Support-LibraryAndroid TablayoutAndroid Problem Overview
I have added TabLayout
(from support library v22.2.1) to my Fragment as:
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
style="@style/MyColorAccentTabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable"/>
The issue is that when the Fragment's orientation is landscape (before or after the initial creation of the fragment), the TabLayout
doesn't match the width of the Fragment
(yes the parent has its width set to match_parent
as well).
When screen width is small (i.e not all tabs can be shown at same time):
When screen width is big enough to show all tabs (see the blank space at the right):
If I change tabMode
to fixed, width is filled but tabs are too small. Is there any proper solution out there?
Android Solutions
Solution 1 - Android
androidx version:
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" >
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMaxWidth="0dp"
app:tabGravity="fill"
app:tabMode="fixed"
/>
</androidx.viewpager.widget.ViewPager>
***** BELOW IS OLD ANSWER *****
Try this one, it's a workaround which sets tabMaxWidth="0dp"
, tabGravity="fill"
and tabMode="fixed"
.
<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMaxWidth="0dp"
app:tabGravity="fill"
app:tabMode="fixed"/>
</android.support.v4.view.ViewPager>
Screenshot on a 10 inch tablet:
Solution 2 - Android
Instead of creating custom TabLayout and hacking around or creating more layouts which acts as wrapper around TabLayout only for background. Try this,
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
style="@style/MyColorAccentTabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
<!-- Instead of setting app:tabBackground -->
android:background="@color/colorAccent"
app:tabGravity="fill"
app:tabMode="scrollable"/>
This will set background to behind tabLayout instead of setting background behind every tab.
Solution 3 - Android
I guess this is the simpliest way to achieve what you want.
public class CustomTabLayout extends TabLayout {
public CustomTabLayout(Context context) {
super(context);
}
public CustomTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
try {
if (getTabCount() == 0)
return;
Field field = TabLayout.class.getDeclaredField("mTabMinWidth");
field.setAccessible(true);
field.set(this, (int) (getMeasuredWidth() / (float) getTabCount()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
Solution 4 - Android
Here is my 2021 Kotlin solution. It achieves the following which I couldn't do from other answers:
- If not many tabs they expand to fill the whole width.
- If lots of tabs they are scrollable so they aren't squished.
- Works correctly even if tabLayout is not the full width of the screen.
To achieve this I created a subclass of TabLayout
that overrides onMeasure
class ScalableTabLayout : TabLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val tabLayout = getChildAt(0) as ViewGroup
val childCount = tabLayout.childCount
if (childCount > 0) {
val widthPixels = MeasureSpec.getSize(widthMeasureSpec)
val tabMinWidth = widthPixels / childCount
var remainderPixels = widthPixels % childCount
tabLayout.forEachChild {
if (remainderPixels > 0) {
it.minimumWidth = tabMinWidth + 1
remainderPixels--
} else {
it.minimumWidth = tabMinWidth
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
And then used it in my layout file:
<com.package.name.ScalableTabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMaxWidth="0dp"
app:tabMode="scrollable" />
both tabMaxWidth="0dp"
and tabMode="scrollable"
are required
Solution 5 - Android
please use this it will solve this problem definitely
<android.support.design.widget.TabLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMaxWidth="0dp"
app:tabGravity="fill"
app:tabMode="fixed" />
Solution 6 - Android
I have tried almost all answeres, atlast found this one works like a charm.
public void dynamicSetTabLayoutMode(TabLayout tabLayout) {
int tabWidth = calculateTabWidth(tabLayout);
int screenWidth = getApplicationContext().getResources().getDisplayMetrics().widthPixels;
if (tabWidth <= screenWidth) {
tabLayout.setTabMode(TabLayout.MODE_FIXED);
} else {
tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
}
}
private int calculateTabWidth(TabLayout tabLayout) {
int tabWidth = 0;
for (int i = 0; i < tabLayout.getChildCount(); i++) {
final View view = tabLayout.getChildAt(i);
view.measure(0, 0);
tabWidth += view.getMeasuredWidth();
}
return tabWidth;
}
Solution 7 - Android
This solution works by letting android create the tabs then calculating the total width of them. Then checks if the tabs will fit the screen width, if it will fit then sets the tabMode to "fixed" which scales all the tabs to fit the screen width.
The tab layout xml, parent doesn't matter:
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable"/>
Then in your activity onCreate or fragment onCreateView:
var totalWidth = 0
var maxWidth = 0
for (i in 0 until tabLayout.tabCount) {
val tabWidth = (tabLayout.getChildAt(0) as ViewGroup).getChildAt(i)!!.width
totalWidth += tabWidth
maxWidth = max(maxWidth, tabWidth)
}
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
if (totalWidth < screenWidth&& screenWidth/ tabLayout.tabCount >= maxWidth) {
tabLayout.tabMode = TabLayout.MODE_FIXED
}
If you are using TabLayout with Viewpager, then you have to set a layoutChangeListener since the tabs are not inflated at the start.
In your activity onCreate or fragment onCreateView:
tabLayout.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
var totalWidth = 0
var maxWidth = 0
for (i in 0 until tabLayout.tabCount) {
val tabWidth = (tabLayout.getChildAt(0) as ViewGroup).getChildAt(i)!!.width
totalWidth += tabWidth
maxWidth = max(maxWidth, tabWidth)
}
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
if (totalWidth < screenWidth && screenWidth / tabLayout.tabCount >= maxWidth) {
tabLayout.tabMode = TabLayout.MODE_FIXED
}
}
Note: If you want all of your tabs to have same text size when the tabMode is "fixed", then you have to set the textSize manually from styles.xml.
Solution 8 - Android
Here's an easier way to make sure that the width of tab is equal to tab's string length
<androidx.viewpager.widget.ViewPager
android:id="@+id/vpTabs"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMaxWidth="0dp"
app:tabMode="scrollable"/>
</androidx.viewpager.widget.ViewPager>
Following code is used:-
app:tabMaxWidth="0dp"
app:tabMode="scrollable"
Solution 9 - Android
try with this changes
<android.support.design.widget.TabLayout
android:id="@+id/sliding_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="fixed"
app:tabGravity="fill" />
Solution 10 - Android
I was struggling as well and most options above did not work for me, so I created a version on the answer of DTX12.
I have a boolean to check for tablet or not and make a check on landscape and portrait. Then set the layout based on amount of tabs.
You might need to make the method public and call it on rotation / configurationchange.
In the CustomTabLayout Code i replaced the mintabswidth method
private void initTabMinWidth()
{
boolean isTablet = getContext().getResources().getBoolean(R.bool.device_is_tablet);
boolean isLandscape = (wh[WIDTH_INDEX] > wh[HEIGHT_INDEX]) ? true : false;
if (isTablet)
{
if (isLandscape)
{
if (this.getTabCount() > 8)
{
this.setTabGravity(TabLayout.GRAVITY_FILL);
this.setTabMode(TabLayout.MODE_SCROLLABLE);
} else
{
this.setTabGravity(TabLayout.GRAVITY_FILL);
this.setTabMode(TabLayout.MODE_FIXED);
}
} else
{
if (this.getTabCount() > 6)
{
this.setTabGravity(TabLayout.GRAVITY_FILL);
this.setTabMode(TabLayout.MODE_SCROLLABLE);
} else
{
this.setTabGravity(TabLayout.GRAVITY_FILL);
this.setTabMode(TabLayout.MODE_FIXED);
}
}
} else
{
if (isLandscape)
{
if (this.getTabCount() > 6)
{
this.setTabGravity(TabLayout.GRAVITY_FILL);
this.setTabMode(TabLayout.MODE_SCROLLABLE);
} else
{
this.setTabGravity(TabLayout.GRAVITY_FILL);
this.setTabMode(TabLayout.MODE_FIXED);
}
} else
{
if (this.getTabCount() > 4)
{
this.setTabGravity(TabLayout.GRAVITY_FILL);
this.setTabMode(TabLayout.MODE_SCROLLABLE);
} else
{
this.setTabGravity(TabLayout.GRAVITY_FILL);
this.setTabMode(TabLayout.MODE_FIXED);
}
}
}
}
Solution 11 - Android
After spending 2 days to figure out the issue, now finally it's working for me. Please see @Tyler V's answer check here From Tyler's answer 'It was key to set the minimum width before calling super.onMeasure().' is very important
Solution 12 - Android
This is my solution with tabMode set to: app:tabMode="scrollable"
class MyTabLayout(
context: Context,
attrs: AttributeSet?
) : TabLayout(context, attrs) {
override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int
) {
val equalTabWidth= (MeasureSpec.getSize(widthMeasureSpec) / tabCount.toFloat()).toInt()
for (index in 0..tabCount) {
val tab = getTabAt(index)
val tabMeasuredWidth = tab?.view?.measuredWidth ?: equalTabWidth
if (tabMeasuredWidth < equalTabWidth) {
tab?.view?.minimumWidth = equalTabWidth
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
Solution 13 - Android
This method will work if you want to use it as scrollable tabs with custom tabs width
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="48dp"
app:tabMaxWidth="48dp"
app:tabGravity="fill"
app:tabMode="scrollable"/>
Solution 14 - Android
Please check this i think it works
public class MainActivity extends AppCompatActivity {
private TextView mTxv_Home, mTxv_News, mTxv_Announcement;
private View mView_Home, mView_News, mView_Announcements;
private HorizontalScrollView hsv;
private ViewPager viewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTxv_Home = (TextView) findViewById(R.id.txv_home);
mTxv_News = (TextView) findViewById(R.id.txv_news);
mTxv_Announcement = (TextView) findViewById(R.id.txv_announcements);
mView_Home = (View) findViewById(R.id.view_home);
mView_News = (View) findViewById(R.id.view_news);
mView_Announcements = (View) findViewById(R.id.view_announcements);
hsv = (HorizontalScrollView) findViewById(R.id.hsv);
viewPager = (ViewPager) findViewById(R.id.viewpager);
setupViewPager(viewPager);
DisplayMetrics displaymetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
int wt = displaymetrics.widthPixels/3;
mTxv_Home.setWidth(wt);
mTxv_News.setWidth(wt);
// mTxv_Announcement.setWidth(wt);
mTxv_Home.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mTxv_Home.setTextColor(Color.parseColor("#3F51B5"));
mTxv_News.setTextColor(Color.parseColor("#808080"));
mTxv_Announcement.setTextColor(Color.parseColor("#808080"));
mView_Home.setBackgroundColor(Color.parseColor("#3F51B5"));
mView_News.setBackgroundColor(Color.parseColor("#E8E8E8"));
mView_Announcements.setBackgroundColor(Color.parseColor("#E8E8E8"));
hsv.post(new Runnable() {
public void run() {
hsv.fullScroll(HorizontalScrollView.FOCUS_LEFT);
}
});
viewPager.setCurrentItem(0);
}
});
mTxv_News.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mTxv_Home.setTextColor(Color.parseColor("#808080"));
mTxv_News.setTextColor(Color.parseColor("#3F51B5"));
mTxv_Announcement.setTextColor(Color.parseColor("#808080"));
mView_Home.setBackgroundColor(Color.parseColor("#E8E8E8"));
mView_News.setBackgroundColor(Color.parseColor("#3F51B5"));
mView_Announcements.setBackgroundColor(Color.parseColor("#E8E8E8"));
hsv.post(new Runnable() {
public void run() {
int centerX = hsv.getChildAt(0).getWidth()/2;
hsv.scrollTo(centerX, 0);
}
});
viewPager.setCurrentItem(1);
}
});
mTxv_Announcement.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mTxv_Home.setTextColor(Color.parseColor("#808080"));
mTxv_News.setTextColor(Color.parseColor("#808080"));
mTxv_Announcement.setTextColor(Color.parseColor("#3F51B5"));
mView_Home.setBackgroundColor(Color.parseColor("#E8E8E8"));
mView_News.setBackgroundColor(Color.parseColor("#E8E8E8"));
mView_Announcements.setBackgroundColor(Color.parseColor("#3F51B5"));
hsv.post(new Runnable() {
public void run() {
hsv.fullScroll(HorizontalScrollView.FOCUS_RIGHT);
}
});
viewPager.setCurrentItem(2);
}
});
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
if (position == 0) {
mTxv_Home.setTextColor(Color.parseColor("#3F51B5"));
mTxv_News.setTextColor(Color.parseColor("#808080"));
mTxv_Announcement.setTextColor(Color.parseColor("#808080"));
mView_Home.setBackgroundColor(Color.parseColor("#3F51B5"));
mView_News.setBackgroundColor(Color.parseColor("#E8E8E8"));
mView_Announcements.setBackgroundColor(Color.parseColor("#E8E8E8"));
hsv.post(new Runnable() {
public void run() {
hsv.fullScroll(HorizontalScrollView.FOCUS_LEFT);
}
});
} else if (position == 1) {
mTxv_Home.setTextColor(Color.parseColor("#808080"));
mTxv_News.setTextColor(Color.parseColor("#3F51B5"));
mTxv_Announcement.setTextColor(Color.parseColor("#808080"));
mView_Home.setBackgroundColor(Color.parseColor("#E8E8E8"));
mView_News.setBackgroundColor(Color.parseColor("#3F51B5"));
mView_Announcements.setBackgroundColor(Color.parseColor("#E8E8E8"));
hsv.post(new Runnable() {
public void run() {
int centerX = hsv.getChildAt(0).getWidth()/2;
hsv.scrollTo(centerX, 0);
}
});
} else if (position == 2) {
mTxv_Home.setTextColor(Color.parseColor("#808080"));
mTxv_News.setTextColor(Color.parseColor("#808080"));
mTxv_Announcement.setTextColor(Color.parseColor("#3F51B5"));
mView_Home.setBackgroundColor(Color.parseColor("#E8E8E8"));
mView_News.setBackgroundColor(Color.parseColor("#E8E8E8"));
mView_Announcements.setBackgroundColor(Color.parseColor("#3F51B5"));
hsv.post(new Runnable() {
public void run() {
hsv.fullScroll(HorizontalScrollView.FOCUS_RIGHT);
}
});
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
private void setupViewPager(ViewPager viewPager) {
ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager());
adapter.addFragment(new HomeFragment(), "Home");
adapter.addFragment(new NewsFragment(), "News");
adapter.addFragment(new AnnouncementsFragment(), "Announcements");
viewPager.setAdapter(adapter);
}
class ViewPagerAdapter extends FragmentPagerAdapter {
private final List<Fragment> mFragmentList = new ArrayList<>();
private final List<String> mFragmentTitleList = new ArrayList<>();
public ViewPagerAdapter(FragmentManager manager) {
super(manager);
}
@Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}
@Override
public int getCount() {
return mFragmentList.size();
}
public void addFragment(Fragment fragment, String title) {
mFragmentList.add(fragment);
mFragmentTitleList.add(title);
}
@Override
public CharSequence getPageTitle(int position) {
return mFragmentTitleList.get(position);
}
}
}
<HorizontalScrollView
android:id="@+id/hsv"
android:layout_width="fill_parent"
android:layout_height="56dp"
android:layout_weight="0"
android:fillViewport="true"
android:measureAllChildren="false"
android:scrollbars="none" >
<LinearLayout
android:id="@+id/innerLay"
android:layout_width="fill_parent"
android:layout_height="50dp"
android:orientation="horizontal" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/txv_home"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:text="Home"
android:singleLine="true"
android:paddingLeft="35dp"
android:paddingRight="35dp"
android:gravity="center"
android:textSize="15sp"/>
<View
android:id="@+id/view_home"
android:layout_width="match_parent"
android:layout_height="3dp"
android:background="@color/colorPrimary"
/>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:visibility="gone"
android:background="#e8e8e8"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/txv_news"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:text="News"
android:singleLine="true"
android:paddingLeft="35dp"
android:paddingRight="35dp"
android:gravity="center"
android:textSize="15sp"/>
<View
android:id="@+id/view_news"
android:layout_width="match_parent"
android:layout_height="3dp"
android:background="#e8e8e8"/>
</LinearLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:visibility="gone"
android:background="#e8e8e8"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/txv_announcements"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:singleLine="true"
android:text="Announcements"
android:paddingLeft="35dp"
android:paddingRight="35dp"
android:gravity="center"
android:textSize="15sp"/>
<View
android:id="@+id/view_announcements"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#e8e8e8"/>
</LinearLayout>
</LinearLayout>
</HorizontalScrollView>
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/hsv" />
Solution 15 - Android
You should set app:tabMode="fixed" when you want to show tabs 'fill'.
**<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/toolbar"
android:background="#353535"
app:tabMode="fixed"
android:minHeight="?attr/actionBarSize"
app:tabIndicatorColor="@color/red"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />**
Solution 16 - Android
You may used app:tabMaxWidth="0dp" in tablayout
Solution 17 - Android
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:tabMode="scrollable"/>
Solution 18 - Android
I didn't care about the tabs filling the width, but I cared that the background color wasn't expanding to the full width. So I thought of this solution, where I put a FrameLayout behind it with the same background color as the tabs.
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/MyColor">
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
style="@style/MyCustomTabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable"/>
</FrameLayout>
Solution 19 - Android
@dtx12 answer doesn't work if any tab title is bigger than (measuredWidth/ tabCount).
There is my TabLayout
subclass for this situation (in Kotlin). I hope this will help someone.
class FullWidthTabLayout : TabLayout {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
if (changed) {
val widths = mutableListOf<Int>()
for (i in 0..this.tabCount - 1) {
widths.add(this.getTabAt(i)!!.customView!!.width)
}
if (widths.sum() < this.measuredWidth) {
var equalPart: Long = this.measuredWidth.toLong() / this.tabCount.toLong()
var biggerWidths = widths.filter { it > equalPart }
var smallerWidths = widths.filter { it <= equalPart }
var rest: Long = this.measuredWidth.toLong() - biggerWidths.sum()
while (biggerWidths.size != 0) {
equalPart = rest / smallerWidths.size
biggerWidths = smallerWidths.filter { it >= equalPart }
smallerWidths = smallerWidths.filter { it < equalPart }
rest -= biggerWidths.sum()
}
val minWidth = (rest / smallerWidths.size) + 10 //dont know why there is small gap on the end, thats why +10
for (i in 0..this.tabCount - 1) {
this.getTabAt(i)!!.customView!!.minimumWidth = minWidth.toInt()
}
}
}
}
}