morristech
7/28/2016 - 9:54 AM

A custom TabLayout with badge support for Tabs

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