Android TabLayout custom indicator width
Asked Answered
F

5

14

I want to reduce width of indicator - for example, make it's width not whole tab width, but 1/2 of tab width.
That's all I need to do, so I don't want to download some custom library and search where I can do this.

Is it a way to do this or I should write such view by myself?

Fornax answered 16/8, 2017 at 14:4 Comment(1)
this answer working for me very well https://mcmap.net/q/827075/-how-to-customize-tab-indicator-widthClotheshorse
G
3

Try this.

public void setIndicator (TabLayout tabs,int leftDip,int rightDip){  
   Class<?> tabLayout = tabs.getClass();  
   Field tabStrip = null;  
   try {  
       tabStrip = tabLayout.getDeclaredField("mTabStrip");  
   } catch (NoSuchFieldException e) {  
       e.printStackTrace();  
   }  

   tabStrip.setAccessible(true);  
   LinearLayout llTab = null;  
   try {  
       llTab = (LinearLayout) tabStrip.get(tabs);  
   } catch (IllegalAccessException e) {  
       e.printStackTrace();  
   }  

   int left = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, leftDip, Resources.getSystem().getDisplayMetrics());  
   int right = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rightDip, Resources.getSystem().getDisplayMetrics());  

   for (int i = 0; i < llTab.getChildCount(); i++) {  
       View child = llTab.getChildAt(i);  
       child.setPadding(0, 0, 0, 0);  
       LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1);  
       params.leftMargin = left;  
       params.rightMargin = right;  
       child.setLayoutParams(params);  
       child.invalidate();  
   }  
}

And then

tab.post(new Runnable() {  
       @Override  
       public void run() {  
           setIndicator(tab,60,60);  
       }  
});  


My modification w/o reflection (custom view should be set!).

for (int i = 0; i < tabs.getTabCount(); i++) {
    TabLayout.Tab tab = tabs.getTabAt(i);
    if (tab != null) {
        View customView = tab.getCustomView();
        if (customView != null) {
            View targetViewToApplyMargin = (View) customView.getParent();
            ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) targetViewToApplyMargin.getLayoutParams();

            layoutParams.rightMargin = totalTabMargin;
                    targetViewToApplyMargin.setLayoutParams(layoutParams);
        }
    }
}
Gatlin answered 16/8, 2017 at 14:11 Comment(7)
Thanks a lot! I've modified your answer a bit (with custom view, without reflection). But direction was definitely correct!Fornax
Thanks to your modification.I will try it tomorrow.@GoltsevEugeneGatlin
gre8-> Works fine for me. Thanks.Trail
@GoltsevEugene May I know how do you set the custom view and what do you set it to?Canopus
yourTabs.getTabAt(tabIndex).setCustomView(yourView).Fornax
@GoltsevEugene What is the value of 'totalTabMargin' varibale?Reverso
@codeWorm, actually, its calculated margin; in my case it is tab width - desired indicator width, but you might calculate it in some other way.Fornax
W
26

if a tab indicator with a fixed size is what you want, to achieve you can create an indicator shape with a fixed size by drawable:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:gravity="center">
        <shape android:shape="rectangle">
            <solid android:color="@color/colorAccent" />
            <corners
                android:topLeftRadius="2dp"
                android:topRightRadius="2dp" />
            <size
                android:width="16dp"
                android:height="2dp" />
        </shape>
    </item>
</layer-list>

Then on the TabLayout, you can then just set the tab indicator drawable. at the same time, you also have to set the tabIndicatorColor

 app:tabIndicatorColor="@color/tabIndicatorColor"
 app:tabIndicator="@drawable/tab_indicator"

Because the drawable itself centers the shape inside its bounds, the indicator will have a fixed size.

if you want the indicator to match the width of the label, you can remove the tabIndicator android:gravity.

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="@color/colorAccent" />
            <corners
                radius="2dp" />
            <size
                android:width="16dp"
                android:height="2dp" />
        </shape>
    </item>
</layer-list>
Worrell answered 13/11, 2019 at 10:41 Comment(1)
This the cleanest solution! I had solved with a 9-patch PNG, but this one is surely better. I had tried a similar solution but was missing that the default gravity value is fill...Merchantable
A
15

For anyone that ends up on this issue, I found a simple solution with vector drawables for a ~90% of the tab width indicator:

ic_tab_indicator_24dp:

<vector
android:height="24dp"
android:width="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
    android:strokeWidth="4"
    android:fillColor="@android:color/white"
    android:pathData="M2,0 L22,0 L22,24 L2,24 z"/>

And then set tabIndicator in the layout:

app:tabIndicator="@drawable/ic_tab_indicator_24dp"

Or in styles.xml:

<item name="tabIndicator">@drawable/ic_tab_indicator_24dp</item>

Example

Edit: As my solution is getting attention, I'd add that you can play with the width just modifying the pathData ("M2,0 L22,0 L22,24 L2,24 z") changing 2 and 22 values. The amount added to 2 should be subtracted from 22. i.e.: "M4,0 L20,0 L20,24 L4,24 z" or "M6,0 L18,0 L18,24 L6,24 z"...

Angel answered 17/10, 2018 at 12:51 Comment(1)
This logic is awesome!Boxing
G
3

Try this.

public void setIndicator (TabLayout tabs,int leftDip,int rightDip){  
   Class<?> tabLayout = tabs.getClass();  
   Field tabStrip = null;  
   try {  
       tabStrip = tabLayout.getDeclaredField("mTabStrip");  
   } catch (NoSuchFieldException e) {  
       e.printStackTrace();  
   }  

   tabStrip.setAccessible(true);  
   LinearLayout llTab = null;  
   try {  
       llTab = (LinearLayout) tabStrip.get(tabs);  
   } catch (IllegalAccessException e) {  
       e.printStackTrace();  
   }  

   int left = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, leftDip, Resources.getSystem().getDisplayMetrics());  
   int right = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rightDip, Resources.getSystem().getDisplayMetrics());  

   for (int i = 0; i < llTab.getChildCount(); i++) {  
       View child = llTab.getChildAt(i);  
       child.setPadding(0, 0, 0, 0);  
       LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1);  
       params.leftMargin = left;  
       params.rightMargin = right;  
       child.setLayoutParams(params);  
       child.invalidate();  
   }  
}

And then

tab.post(new Runnable() {  
       @Override  
       public void run() {  
           setIndicator(tab,60,60);  
       }  
});  


My modification w/o reflection (custom view should be set!).

for (int i = 0; i < tabs.getTabCount(); i++) {
    TabLayout.Tab tab = tabs.getTabAt(i);
    if (tab != null) {
        View customView = tab.getCustomView();
        if (customView != null) {
            View targetViewToApplyMargin = (View) customView.getParent();
            ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) targetViewToApplyMargin.getLayoutParams();

            layoutParams.rightMargin = totalTabMargin;
                    targetViewToApplyMargin.setLayoutParams(layoutParams);
        }
    }
}
Gatlin answered 16/8, 2017 at 14:11 Comment(7)
Thanks a lot! I've modified your answer a bit (with custom view, without reflection). But direction was definitely correct!Fornax
Thanks to your modification.I will try it tomorrow.@GoltsevEugeneGatlin
gre8-> Works fine for me. Thanks.Trail
@GoltsevEugene May I know how do you set the custom view and what do you set it to?Canopus
yourTabs.getTabAt(tabIndex).setCustomView(yourView).Fornax
@GoltsevEugene What is the value of 'totalTabMargin' varibale?Reverso
@codeWorm, actually, its calculated margin; in my case it is tab width - desired indicator width, but you might calculate it in some other way.Fornax
K
3

Tried all those solutions,but not satisfied, and finally I found a solution myself, we just need to modify the indicatorLeft and indicatorRight fileds of SlidingTabIndicator, extends the tablayout and call setIndicaotrWidth() when need, for example:

setIndicatorWidth(70);

The whole code:

import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.google.android.material.tabs.TabLayout;

import java.lang.reflect.Field;

import androidx.annotation.NonNull;

public class TabLayoutEx extends TabLayout {

    public TabLayoutEx(Context context) {
        this(context, null);
    }

    public TabLayoutEx(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TabLayoutEx(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    private void init(AttributeSet attrs) {
        setTabIndicatorFullWidth(false);
        setIndicatorWidth(70);
    }



    private class DefPreDrawListener implements ViewTreeObserver.OnPreDrawListener {

        private LinearLayout tabStrip = null;
        private int tabWidth;
        private Field fieldLeft;
        private Field fieldRight;

        public void setTabStrip(LinearLayout tabStrip, int width) {
            try {
                this.tabStrip = tabStrip;
                this.tabWidth = width;
                Class cls = tabStrip.getClass();
                fieldLeft = cls.getDeclaredField("indicatorLeft");
                fieldLeft.setAccessible(true);
                fieldRight = cls.getDeclaredField("indicatorRight");
                fieldRight.setAccessible(true);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public boolean onPreDraw() {
            try {
                if (tabWidth > 0) {
                    int left = fieldLeft.getInt(this.tabStrip);
                    int right = fieldRight.getInt(this.tabStrip);
                    //根据目标宽度及现在的宽度调整为合适的left和right
                    int diff = right - left - tabWidth;
                    left = left + diff / 2;
                    right = right - diff / 2;
                    fieldLeft.setInt(this.tabStrip, left);
                    fieldRight.setInt(this.tabStrip, right);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return true;
        }
    }

    private DefPreDrawListener defPreDrawListener = new DefPreDrawListener();

    public void setIndicatorWidth(int widthDp) {
        Class<?> tabLayout = TabLayout.class;
        Field tabStrip = null;
        try {
            tabStrip = tabLayout.getDeclaredField("slidingTabIndicator");
            tabStrip.setAccessible(true);
            LinearLayout tabIndicator = (LinearLayout) tabStrip.get(this);
            int width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, widthDp, Resources.getSystem().getDisplayMetrics());
            //avoid add preDrawListener multi times
            tabIndicator.getViewTreeObserver().removeOnPreDrawListener(defPreDrawListener);
            tabIndicator.getViewTreeObserver().addOnPreDrawListener(defPreDrawListener);
            defPreDrawListener.setTabStrip(tabIndicator, width);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

Killian answered 14/10, 2019 at 16:56 Comment(1)
after using this code, tab indicator width is located at center but i want it left side, want its size half of the text from last side.. then what to do ?Bladdernose
G
1

you could copy TabLayout, and modify the logic of calculating the width of Indicator. See updateIndicatorPosition() and animateIndicatorToPosition(). A simple demo https://github.com/xybean/CustomTabLayout

Grandparent answered 3/11, 2017 at 7:40 Comment(4)
It looks more like recommendation but not full answer. Can you provide the example of implementation of your idea? It would be great if you add some details. Thank you!Heathenry
What ideally I tried to do is to copy all those classes - ViewPager, TabLayout etc. to have fully customizable behaviour. The problem is that when copied, there are a lot of private/native fields which I cannot import into the project. Moreover, all these classes have very strong connections to other classes in support package. So it's was really hard for me to do that.Fornax
@GoltsevEugene Just copy TabLayout.Grandparent
@Heathenry I upload a demo to github see github.com/xybean/CustomTabLayoutGrandparent

© 2022 - 2024 — McMap. All rights reserved.