Skip to content

Instantly share code, notes, and snippets.

@EmmanuelGuther
Last active December 14, 2022 03:08
Show Gist options
  • Select an option

  • Save EmmanuelGuther/a10cba2caa821b98fa1c965aa3022233 to your computer and use it in GitHub Desktop.

Select an option

Save EmmanuelGuther/a10cba2caa821b98fa1c965aa3022233 to your computer and use it in GitHub Desktop.
Add badge to tab layout
<?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="0.5dp"
android:color="#a6a0a0"/>
</shape>
package es.cityride.mantapp
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
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
/**
* Use:
* tabLayout.with(tabPosition).badge(true).badgeCount(badgeCount).icon(R.drawable.ic_local_shipping_24dp).build()
*/
class BadgeTabLayout : TabLayout {
private val mTabBuilders = SparseArray<Builder>()
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
fun with(position: Int): Builder {
val 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.
*/
fun with(tab: TabLayout.Tab?): Builder {
if (tab == null) {
throw IllegalArgumentException("Tab must not be null")
}
var builder: Builder? = mTabBuilders.get(tab.position)
if (builder == null) {
builder = Builder(this, tab)
mTabBuilders.put(tab.position, builder)
}
return builder
}
class Builder
/**
* 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
*/
(parent: TabLayout, private val mTab: TabLayout.Tab) {
private val mView: View?
private val mContext: Context = parent.context
private var mBadgeTextView: TextView? = null
private var mIconView: ImageView? = null
private var mIconDrawable: Drawable? = null
private var mIconColorFilter: Int? = null
private var mBadgeCount = Integer.MIN_VALUE
private var mHasBadge = false
init {
// initialize current tab's custom view.
if (mTab.customView != null) {
this.mView = mTab.customView
} else {
this.mView = LayoutInflater.from(parent.context)
.inflate(R.layout.tab_custom_icon, parent, false)
}
if (mView != null) {
this.mIconView = mView.findViewById<View>(R.id.tab_icon) as ImageView
this.mBadgeTextView = mView.findViewById<View>(R.id.tab_badge) as TextView
}
if (this.mBadgeTextView != null) {
this.mHasBadge = mBadgeTextView!!.visibility == View.VISIBLE
try {
this.mBadgeCount = Integer.parseInt(mBadgeTextView!!.text.toString())
} catch (er: NumberFormatException) {
er.printStackTrace()
this.mBadgeCount = INVALID_NUMBER
}
}
if (this.mIconView != null) {
mIconDrawable = mIconView!!.drawable
}
}
fun imageContentDescription(contentDescription: String): Builder {
mIconView?.contentDescription = contentDescription
return this
}
/**
* The related Tab is about to have a badge
*
* @return this builder
*/
fun hasBadge(): Builder {
mHasBadge = true
return this
}
/**
* The related Tab is not about to have a badge
*
* @return this builder
*/
fun noBadge(): Builder {
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*/
fun badge(hasBadge: Boolean): Builder {
mHasBadge = hasBadge
return this
}
/**
* Set icon custom drawable by Resource ID;
*
* @param drawableRes
* @return this builder
*/
fun icon(drawableRes: Int): Builder {
mIconDrawable = getDrawableCompat(mContext, drawableRes)
return this
}
/**
* Set icon custom drawable by Drawable Object
*
* @param drawable
* @return this builder
*/
fun icon(drawable: Drawable?): Builder {
mIconDrawable = drawable
return this
}
/**
* Set drawable color. Use this when user wants to change drawable's color filter
*
* @param color
* @return this builder
*/
fun iconColor(color: Int?): Builder {
mIconColorFilter = color
return this
}
/**
* increase current badge by 1
*
* @return this builder
*/
fun increase(): Builder {
mBadgeCount = if (mBadgeTextView == null)
INVALID_NUMBER
else
Integer.parseInt(mBadgeTextView!!.text.toString()) + 1
return this
}
/**
* decrease current badge by 1
*
* @return
*/
fun decrease(): Builder {
mBadgeCount = if (mBadgeTextView == null)
INVALID_NUMBER
else
Integer.parseInt(mBadgeTextView!!.text.toString()) - 1
return this
}
/**
* set badge count
*
* @param count expected badge number
* @return this builder
*/
public fun badgeCount(count: Int): Builder {
mBadgeCount = count
return this
}
/**
* Build the current Tab icon's custom view
*/
fun build() {
if (mView == null) {
return
}
// update badge counter
if (mBadgeTextView != null) {
mBadgeTextView!!.text = formatBadgeNumber(mBadgeCount)
if (mHasBadge) {
mBadgeTextView!!.visibility = View.VISIBLE
} else {
// set to View#INVISIBLE to not screw up the layout
mBadgeTextView!!.visibility = 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.customView = mView
}
companion object {
/**
* This badge widget must not support this value.
*/
private val INVALID_NUMBER = Integer.MIN_VALUE
}
}
companion object {
private fun getDrawableCompat(context: Context, drawableRes: Int): Drawable? {
var drawable: Drawable? = null
try {
drawable = context.resources.getDrawable(drawableRes, context.theme)
} catch (ex: NullPointerException) {
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 fun formatBadgeNumber(value: Int): String {
if (value < 0) {
return "-" + formatBadgeNumber(-value)
}
return if (value <= 10) {
// equivalent to String#valueOf(int);
Integer.toString(value)
} else "10+"
// my own policy
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/actionbar_height"
android:layout_height="@dimen/actionbar_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/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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment