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+";
}
}
@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