Skip to content

Instantly share code, notes, and snippets.

@eneim
Last active June 10, 2022 11:27
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save eneim/55df01e092520a6d33a4 to your computer and use it in GitHub Desktop.
Save eneim/55df01e092520a6d33a4 to your computer and use it in GitHub Desktop.
A custom TabLayout with badge support for Tabs
package im.ene.lab.android.widgets;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import im.ene.lab.android.R;
/**
* Created by eneim on 9/2/15.
* <p/>
* A custom TabLayout with Builder support for customizing Tabs.
* <p/>
* Since every Tab must be attached to a parent TabLayout, it's reasonable to have an inner
* Builder for every Tab in TabLayout, but not a global TabLayout#Builder. Builder is not strictly
* follow Builder design pattern.
*/
public class BadgeTabLayout extends TabLayout {
private final SparseArray<Builder> mTabBuilders = new SparseArray<>();
public BadgeTabLayout(Context context) {
super(context);
}
public BadgeTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BadgeTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public Builder with(int position) {
Tab tab = getTabAt(position);
return with(tab);
}
/**
* Apply a builder for this tab.
*
* @param tab for which we create a new builder or retrieve its builder if existed.
* @return the required Builder.
*/
public Builder with(Tab tab) {
if (tab == null) {
throw new IllegalArgumentException("Tab must not be null");
}
Builder builder = mTabBuilders.get(tab.getPosition());
if (builder == null) {
builder = new Builder(this, tab);
mTabBuilders.put(tab.getPosition(), builder);
}
return builder;
}
public static final class Builder {
/**
* This badge widget must not support this value.
*/
private static final int INVALID_NUMBER = Integer.MIN_VALUE;
@Nullable final View mView;
final Context mContext;
final TabLayout.Tab mTab;
@Nullable TextView mBadgeTextView;
@Nullable ImageView mIconView;
Drawable mIconDrawable;
Integer mIconColorFilter;
int mBadgeCount = Integer.MIN_VALUE;
boolean mHasBadge = false;
/**
* This construct take a TabLayout parent to have its context and other attributes sets. And
* the tab whose icon will be updated.
*
* @param parent
* @param tab
*/
private Builder(TabLayout parent, @NonNull TabLayout.Tab tab) {
super();
this.mContext = parent.getContext();
this.mTab = tab;
// initialize current tab's custom view.
if (tab.getCustomView() != null) {
this.mView = tab.getCustomView();
} else {
this.mView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.tab_custom_icon, parent, false);
}
if (mView != null) {
this.mIconView = (ImageView) mView.findViewById(R.id.tab_icon);
this.mBadgeTextView = (TextView) mView.findViewById(R.id.tab_badge);
}
if (this.mBadgeTextView != null) {
this.mHasBadge = mBadgeTextView.getVisibility() == View.VISIBLE;
try {
this.mBadgeCount = Integer.parseInt(mBadgeTextView.getText().toString());
} catch (NumberFormatException er) {
er.printStackTrace();
this.mBadgeCount = INVALID_NUMBER;
}
}
if (this.mIconView != null) {
mIconDrawable = mIconView.getDrawable();
}
}
/**
* The related Tab is about to have a badge
*
* @return this builder
*/
public Builder hasBadge() {
mHasBadge = true;
return this;
}
/**
* The related Tab is not about to have a badge
*
* @return this builder
*/
public Builder noBadge() {
mHasBadge = false;
return this;
}
/**
* Dynamically set the availability of tab's badge
*
* @param hasBadge
* @return this builder
*/
// This method is used for DEBUG purpose only
/*hide*/
public Builder badge(boolean hasBadge) {
mHasBadge = hasBadge;
return this;
}
/**
* Set icon custom drawable by Resource ID;
*
* @param drawableRes
* @return this builder
*/
public Builder icon(int drawableRes) {
mIconDrawable = getDrawableCompat(mContext, drawableRes);
return this;
}
/**
* Set icon custom drawable by Drawable Object
*
* @param drawable
* @return this builder
*/
public Builder icon(Drawable drawable) {
mIconDrawable = drawable;
return this;
}
/**
* Set drawable color. Use this when user wants to change drawable's color filter
*
* @param color
* @return this builder
*/
public Builder iconColor(Integer color) {
mIconColorFilter = color;
return this;
}
/**
* increase current badge by 1
*
* @return this builder
*/
public Builder increase() {
mBadgeCount =
mBadgeTextView == null ?
INVALID_NUMBER
:
Integer.parseInt(mBadgeTextView.getText().toString()) + 1;
return this;
}
/**
* decrease current badge by 1
*
* @return
*/
public Builder decrease() {
mBadgeCount =
mBadgeTextView == null ?
INVALID_NUMBER
:
Integer.parseInt(mBadgeTextView.getText().toString()) - 1;
return this;
}
/**
* set badge count
*
* @param count expected badge number
* @return this builder
*/
public Builder badgeCount(int count) {
mBadgeCount = count;
return this;
}
/**
* Build the current Tab icon's custom view
*/
public void build() {
if (mView == null) {
return;
}
// update badge counter
if (mBadgeTextView != null) {
mBadgeTextView.setText(formatBadgeNumber(mBadgeCount));
if (mHasBadge) {
mBadgeTextView.setVisibility(View.VISIBLE);
} else {
// set to View#INVISIBLE to not screw up the layout
mBadgeTextView.setVisibility(View.INVISIBLE);
}
}
// update icon drawable
if (mIconView != null && mIconDrawable != null) {
mIconView.setImageDrawable(mIconDrawable.mutate());
// be careful if you modify this. make sure your result matches your expectation.
if (mIconColorFilter != null) {
mIconDrawable.setColorFilter(mIconColorFilter, PorterDuff.Mode.MULTIPLY);
}
}
mTab.setCustomView(mView);
}
}
private static Drawable getDrawableCompat(Context context, int drawableRes) {
Drawable drawable = null;
try {
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
drawable = context.getResources().getDrawable(drawableRes);
} else {
drawable = context.getResources().getDrawable(drawableRes, context.getTheme());
}
} catch (NullPointerException ex) {
ex.printStackTrace();
}
return drawable;
}
/**
* This format must follow User's badge policy.
*
* @param value of current badge
* @return corresponding badge number. TextView need to be passed by a String/CharSequence
*/
private static String formatBadgeNumber(int value) {
if (value < 0) {
return "-" + formatBadgeNumber(-value);
}
if (value <= 10) {
// equivalent to String#valueOf(int);
return Integer.toString(value);
}
// my own policy
return "10+";
}
}
@eneim
Copy link
Author

eneim commented Nov 30, 2015

Custom tab layout design:

tab_custom_icon.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="@dimen/tab_width"
    android:layout_height="@dimen/tab_height"
    android:background="@android:color/transparent"
    android:gravity="center"
    android:paddingBottom="4dp"
    android:paddingLeft="12dp"
    android:paddingRight="12dp"
    android:paddingTop="4dp">

    <ImageView
        android:id="@+id/tab_icon"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:adjustViewBounds="true"
        android:background="@android:color/transparent"/>

    <TextView
        android:id="@+id/tab_badge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="top|end"
        android:layout_marginTop="4dp"
        android:background="@drawable/icon_badge"
        android:paddingBottom="1dp"
        android:paddingLeft="6dp"
        android:paddingRight="6dp"
        android:paddingTop="1dp"
        android:textAppearance="@style/TextAppearance.AppCompat.Small"
        android:textColor="@android:color/white"
        android:textSize="12dp"/>
</FrameLayout>

@eneim
Copy link
Author

eneim commented Nov 30, 2015

Badge background (red circle)

icon_badge.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

    <solid android:color="#ea5444"/>
    <corners android:radius="12dp"/>

    <stroke
        android:width="@dimen/0.5dp"
        android:color="#f5f5f5"/>
</shape>

@eneim
Copy link
Author

eneim commented Nov 30, 2015

Sample result (one Selected Badged tab and one Unselected original Tab)

badge_sample

Copy link

ghost commented Mar 31, 2016

how can i have text instead icon under Badged?

@behelit
Copy link

behelit commented May 2, 2016

Are you able to include the definitions of:

android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:layout_width="@dimen/tab_width"
android:layout_height="@dimen/tab_height"

or maybe even how to use this class at all?

@glalwani2
Copy link

Hi Eneim, I was wondering if I could use some of your solution, as I wanted a functionality something like :
I have a scrollable tab and at any particular time the selected tab will be in center and that tab should have two arrows, one poiting from top and one pointing from down, to give it a fvisual eel that its currently selected.Any points as to how to use it, I am making a xamarin Android app. Thank you, if you want I can show you the image.

@creativetrendsapps
Copy link

Is there an example of this code anywhere?

@NimzyMaina
Copy link

Hi creativetrendsapps,
I'm really new to android hence forgive my amature question.
could you perhaps give me a sample of how to use this gist. I'm looking to implement a shopping cart with a badge containing the number of items in the cart.

This is my mainActivity

package tk.nimzymaina.beta;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe;

import java.util.ArrayList;
import java.util.List;

import butterknife.ButterKnife;
import butterknife.OnClick;
import tk.nimzymaina.beta.Events.CluckEvent;
import tk.nimzymaina.beta.fragments.OneFragment;
import tk.nimzymaina.beta.fragments.ThreeFragment;
import tk.nimzymaina.beta.fragments.TwoFragment;
import tk.nimzymaina.beta.infrustructure.EventBus;

public class MainActivity extends AppCompatActivity {

//protected Bus bus  = new Bus();

private Toolbar toolbar;
private TabLayout tabLayout;
private ViewPager viewPager;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show();
        }
    });


    viewPager = (ViewPager) findViewById(R.id.viewpager);
    viewPager.setOffscreenPageLimit(2);
    setupViewPager(viewPager);

    tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(viewPager);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
        return true;
    }

    return super.onOptionsItemSelected(item);
}

// @OnClick(R.id.btn)
// void Deme(){
// EventBus.getBus().post(new CluckEvent("Yahaya"));
// }

@Subscribe
public void Reciever(CluckEvent event){
    Toast.makeText(getApplicationContext(),event.getMessage(),Toast.LENGTH_SHORT).show();
}

@Override
public void onResume(){
    super.onResume();
    EventBus.getBus().register(this);
}

@Override
public void onPause(){
    super.onPause();
    EventBus.getBus().unregister(this);
}


private void setupViewPager(ViewPager viewPager) {
    ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager());
    adapter.addFragment(new OneFragment(), "ONE");
    adapter.addFragment(new TwoFragment(), "TWO");
    adapter.addFragment(new ThreeFragment(), "THREE");
    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);
    }
}

}

and this is my layout

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="tk.nimzymaina.beta.MainActivity">

<android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:popupTheme="@style/AppTheme.PopupOverlay"
        app:layout_scrollFlags="scroll|enterAlways" />

    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:tabMode="fixed"
        app:tabGravity="fill"/>

</android.support.design.widget.AppBarLayout>

<!--<include layout="@layout/content_main" />-->

<android.support.v4.view.ViewPager
    android:id="@+id/viewpager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"  />

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_margin="@dimen/fab_margin"
    android:src="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout>

Thanks in advance

@eneim
Copy link
Author

eneim commented Aug 7, 2016

It seems that I get no notification about these issues above. I will try to read them and figure out some solution later ...

@pishguy
Copy link

pishguy commented Aug 27, 2016

@eneim how can i implementing this class? could you please paste simple code to know about that? i cant implementing that and i get erro, please

@pishguy
Copy link

pishguy commented Aug 27, 2016

@NimzyMaina could you implementing this feature and set badget for some tabs?

@ludmilamm
Copy link

ludmilamm commented Sep 29, 2016

The java implementation could be:
yourBadgetTabLayoutObject.with(desireTabPosition).badge(true).badgeCount(1).build();

@EdgarMP
Copy link

EdgarMP commented Nov 10, 2016

@eneim can you add this part of code to your gist? It adds content description attribute to mIconView for those Tabs as part of the builder.


 /**
   * set Content Description
   *
   * @param contentDescription expected string for content description.
   * @return this builder
   */
  public Builder imageContentDescription(String contentDescription) {
      mIconView.setContentDescription(contentDescription);
      return this;
  }
}

@saeedkg
Copy link

saeedkg commented May 28, 2017

how i can implement this view in XML?

@chnouman
Copy link

@eneim how can i use text instead of icon in this?

@chnouman
Copy link

@ludmilamm thanks how to add text instead of icon?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment