Skip to content

Instantly share code, notes, and snippets.

@Skyyo
Last active August 16, 2020 11:22
Show Gist options
  • Save Skyyo/bfca7e7b6e8725b61df283bd0f78620d to your computer and use it in GitHub Desktop.
Save Skyyo/bfca7e7b6e8725b61df283bd0f78620d to your computer and use it in GitHub Desktop.
Custom badge drawable that can be attached to views #view #badge
<dimen name="default_badge_text_size">10sp</dimen>
<dimen name="default_badge_radius">8dp</dimen>
<dimen name="default_badge_long_text_horizontal_padding">10dp</dimen>
<dimen name="default_badge_size">16dp</dimen>
<attr name="badgeDrawableStyle"/>
<declare-styleable name="CustomBadgeDrawable">
<attr format="color" name="badge_backgroundColor"/>
<attr format="color" name="badge_textColor"/>
<attr format="color" name="badge_textSize"/>
<attr format="integer" name="badge_maxNumber"/>
<attr format="integer" name="badge_number"/>
<attr format="dimension" name="badge_radii"/>
<attr format="boolean" name="sizeAdjustRadii"/>
<attr format="dimension" name="badge_size"/>
<attr format="dimension" name="badge_width"/>
<attr format="dimension" name="badge_height"/>
<attr format="dimension" name="badge_horizontalPadding"/>
<attr format="dimension" name="badge_horizontalOffset"/>
<attr format="dimension" name="badge_verticalOffset"/>
<attr format="integer" name="badge_alpha"/>
<attr name="badge_shape">
<enum name="rectangle" value="0"/>
<enum name="dot" value="1"/>
</attr>
<attr name="badge_gravity">
<!-- Gravity.TOP | Gravity.END -->
<enum name="TOP_END" value="8388661"/>
<!-- Gravity.TOP | Gravity.START -->
<enum name="TOP_START" value="8388659"/>
<!-- Gravity.BOTTOM | Gravity.END -->
<enum name="BOTTOM_END" value="8388693"/>
<!-- Gravity.BOTTOM | Gravity.START -->
<enum name="BOTTOM_START" value="8388691"/>
</attr>
</declare-styleable>
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.graphics.*
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.text.TextPaint
import android.text.TextUtils
import android.util.AttributeSet
import android.util.Xml
import android.view.Gravity
import android.view.View
import androidx.annotation.*
import androidx.annotation.IntRange
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
import java.lang.ref.WeakReference
class CustomBadgeDrawable private constructor(context: Context) : Drawable() {
@IntDef(
TOP_END,
TOP_START,
BOTTOM_END,
BOTTOM_START
)
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
annotation class BadgeGravity
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@IntDef(STYLE_DOT, STYLE_RECTANGLE)
annotation class ShapeStyle
private val badgeBounds: RectF
@ColorInt
private var backgroundColor = DEFAULT_BADGE_BACKGROUND_COLOR
private val shapeDrawable: GradientDrawable
private val badgeTextPaint: TextPaint
private var anchorViewRef: WeakReference<View>? = null
private val anchorBounds = Rect()
private var alpha1 = 255
private var badgeRadii1: Float
private var badgeSize: Int
private var badgeWidth1 = 0
private var badgeHeight1 = 0
private var sizeAdjustRadius1 = false
private var number1 = 0
private var maxNumber1 = DEFAULT_MAX_BADGE_NUMBER
@ShapeStyle
private var shapeStyle1 = STYLE_DOT
private var horizontalPadding1: Int
private var horizontalOffset1 = 0
private var verticalOffset1 = 0
private var gravity1 = TOP_END
private val numberText: String
get() {
var numberText = number1.toString()
if (number1 > maxNumber1) {
numberText =
maxNumber1.toString() + DEFAULT_EXCEED_MAX_BADGE_NUMBER_SUFFIX
}
return numberText
}
init {
val resources = context.resources
badgeRadii1 = resources.getDimensionPixelOffset(R.dimen.default_badge_radius).toFloat()
horizontalPadding1 =
resources.getDimensionPixelOffset(R.dimen.default_badge_long_text_horizontal_padding)
badgeSize = resources.getDimensionPixelSize(R.dimen.default_badge_size)
badgeBounds = RectF()
shapeDrawable = GradientDrawable()
badgeTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
badgeTextPaint.textAlign = Paint.Align.CENTER
badgeTextPaint.color = DEFAULT_BADGE_TEXT_COLOR
badgeTextPaint.typeface = Typeface.DEFAULT_BOLD
setTextSize(resources.getDimensionPixelSize(R.dimen.default_badge_text_size).toFloat())
}
override fun draw(canvas: Canvas) {
if (!isVisible) {
return
}
shapeDrawable.alpha = alpha1
shapeDrawable.setColor(backgroundColor)
shapeDrawable.setBounds(
badgeBounds.left.toInt(),
badgeBounds.top.toInt(),
badgeBounds.right.toInt(),
badgeBounds.bottom.toInt()
)
when (shapeStyle1) {
STYLE_DOT -> shapeDrawable.shape = GradientDrawable.OVAL
STYLE_RECTANGLE -> shapeDrawable.shape = GradientDrawable.RECTANGLE
else -> {
}
}
var radii = badgeRadii1
if (sizeAdjustRadius1) {
radii = Math.min(badgeBounds.width(), badgeBounds.height()) / 2f
}
shapeDrawable.cornerRadius = radii
shapeDrawable.draw(canvas)
if (hasNumber()) {
drawText(canvas)
}
}
/**
* Specify an alpha value for the drawable. 0 means fully transparent, and
* 255 means fully opaque.
*
* @param alpha set the alpha component [0..255] of the paint's color.
*/
override fun setAlpha(
@IntRange(
from = 0,
to = 255
) alpha: Int
) {
if (alpha1 != alpha) {
alpha1 = alpha
invalidateSelf()
}
}
override fun setColorFilter(colorFilter: ColorFilter?) {
// Intentionally empty.
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
public override fun onStateChange(state: IntArray): Boolean {
return super.onStateChange(state)
}
private fun calculateTextWidth(charSequence: CharSequence?): Float {
return if (charSequence == null) {
0f
} else badgeTextPaint.measureText(charSequence, 0, charSequence.length)
}
private fun loadDefaultStateFromAttributes(
context: Context,
attrs: AttributeSet?,
@AttrRes defStyleAttr: Int,
@StyleRes defStyleRes: Int
) {
val ta = context.obtainStyledAttributes(
attrs, R.styleable.CustomBadgeDrawable,
defStyleAttr, defStyleRes
)
setBackgroundColor(
ta.getColor(
R.styleable.CustomBadgeDrawable_badge_backgroundColor,
DEFAULT_BADGE_BACKGROUND_COLOR
)
)
setBadgeRadii(ta.getDimension(R.styleable.CustomBadgeDrawable_badge_radii, badgeRadii1))
setBadgeSize(
ta.getDimensionPixelSize(
R.styleable.CustomBadgeDrawable_badge_size,
badgeSize
)
)
setBadgeWidth(ta.getDimensionPixelOffset(R.styleable.CustomBadgeDrawable_badge_width, -1))
setBadgeHeight(ta.getDimensionPixelOffset(R.styleable.CustomBadgeDrawable_badge_height, -1))
setSizeAdjustRadius(ta.getBoolean(R.styleable.CustomBadgeDrawable_sizeAdjustRadii, false))
setBadgeGravity(
ta.getInt(
R.styleable.CustomBadgeDrawable_badge_gravity,
TOP_END
)
)
setTextSize(
ta.getDimension(
R.styleable.CustomBadgeDrawable_badge_textSize,
badgeTextPaint.textSize
)
)
setTextColor(
ta.getColor(
R.styleable.CustomBadgeDrawable_badge_textColor,
DEFAULT_BADGE_TEXT_COLOR
)
)
setHorizontalOffset(
ta.getDimensionPixelOffset(
R.styleable.CustomBadgeDrawable_badge_horizontalOffset,
0
)
)
setVerticalOffset(
ta.getDimensionPixelOffset(
R.styleable.CustomBadgeDrawable_badge_verticalOffset,
0
)
)
setHorizontalPadding(
ta.getDimensionPixelOffset(
R.styleable.CustomBadgeDrawable_badge_horizontalPadding,
horizontalPadding1
)
)
setNumber(ta.getInt(R.styleable.CustomBadgeDrawable_badge_number, 0))
setMaxNumber(
ta.getInt(
R.styleable.CustomBadgeDrawable_badge_maxNumber,
DEFAULT_MAX_BADGE_NUMBER
)
)
setShapeStyle(ta.getInt(R.styleable.CustomBadgeDrawable_badge_shape, shapeStyle1))
alpha = ta.getInt(R.styleable.CustomBadgeDrawable_badge_alpha, alpha1)
ta.recycle()
}
private fun updateBadgeBounds() {
val anchorView =
(if (anchorViewRef == null) null else anchorViewRef!!.get())
?: return
anchorView.getDrawingRect(anchorBounds)
var width = if (badgeWidth1 > 0) badgeWidth1 else badgeSize
val height = if (badgeHeight1 > 0) badgeHeight1 else badgeSize
if (badgeWidth1 <= 0 && hasNumber() && number1 > MAX_CIRCULAR_BADGE_NUMBER_COUNT) {
width = (calculateTextWidth(numberText) + horizontalPadding1).toInt()
}
val left: Int
val top: Int
left = when (gravity1) {
TOP_START, BOTTOM_START -> anchorBounds.left + horizontalOffset1
TOP_END, BOTTOM_END -> anchorBounds.right - width - 48 + horizontalOffset1
else -> anchorBounds.right - width - 48 + horizontalOffset1
}
top = when (gravity1) {
BOTTOM_START, BOTTOM_END -> anchorBounds.bottom - height + verticalOffset1
TOP_START, TOP_END -> anchorBounds.top + 16 + verticalOffset1
else -> anchorBounds.top + 16 + verticalOffset1
}
badgeBounds[left.toFloat(), top.toFloat(), left + width.toFloat()] = top + height.toFloat()
}
private fun hasNumber(): Boolean {
return number1 > 0
}
private fun drawText(canvas: Canvas) {
val bounds = badgeBounds
val textPaint: Paint = badgeTextPaint
textPaint.alpha = alpha1
val numberText = numberText
textPaint.measureText(numberText, 0, numberText.length)
val fontMetrics = textPaint.fontMetrics
val centerY =
bounds.centerY() - fontMetrics.descent + (fontMetrics.descent - fontMetrics.ascent) / 2f
canvas.drawText(numberText, bounds.centerX(), centerY, textPaint)
}
fun setBadgeWidth(badgeWidth: Int) {
if (badgeWidth1 != badgeWidth) {
badgeWidth1 = badgeWidth
updateBadgeBounds()
invalidateSelf()
}
}
fun setBadgeHeight(badgeHeight: Int) {
if (badgeHeight1 != badgeHeight) {
badgeHeight1 = badgeHeight
updateBadgeBounds()
invalidateSelf()
}
}
fun setBadgeGravity(@BadgeGravity gravity: Int) {
if (gravity1 != gravity) {
gravity1 = gravity
updateBadgeBounds()
invalidateSelf()
}
}
fun setHorizontalPadding(horizontalPadding: Int) {
if (horizontalPadding1 != horizontalPadding) {
horizontalPadding1 = horizontalPadding
updateBadgeBounds()
invalidateSelf()
}
}
fun setHorizontalOffset(horizontalOffset: Int) {
if (horizontalOffset1 != horizontalOffset) {
horizontalOffset1 = horizontalOffset
updateBadgeBounds()
invalidateSelf()
}
}
fun setVerticalOffset(verticalOffset: Int) {
if (verticalOffset1 != verticalOffset) {
verticalOffset1 = verticalOffset
updateBadgeBounds()
invalidateSelf()
}
}
fun setSizeAdjustRadius(sizeAdjustRadius: Boolean) {
if (sizeAdjustRadius1 != sizeAdjustRadius) {
sizeAdjustRadius1 = sizeAdjustRadius
invalidateSelf()
}
}
/**
* Set badge background shape style
*
* @param shapeStyle background shape style
*/
fun setShapeStyle(@ShapeStyle shapeStyle: Int) {
if (shapeStyle1 != shapeStyle) {
shapeStyle1 = shapeStyle
invalidateSelf()
}
}
/**
* Set or clear the typeface object.
*
* @param typeface May be null. The typeface to be installed in the paint
*/
fun setTypeface(typeface: Typeface) {
val paint: Paint = badgeTextPaint
if (paint.typeface !== typeface) {
paint.typeface = typeface
invalidateSelf()
}
}
fun setBadgeSize(newBadgeSize: Int) {
if (badgeSize != newBadgeSize) {
badgeSize = newBadgeSize
updateBadgeBounds()
invalidateSelf()
}
}
/**
* Set badge radius, badge size radius
*
* @param badgeRadii badge radius
*/
fun setBadgeRadii(badgeRadii: Float) {
if (badgeRadii1 != badgeRadii) {
badgeRadii1 = badgeRadii
updateBadgeBounds()
invalidateSelf()
}
}
fun setMaxNumber(maxNumber: Int) {
var res = maxNumber
res = Math.max(0, res)
if (maxNumber1 != res) {
maxNumber1 = res
updateBadgeBounds()
invalidateSelf()
}
}
/**
* Set badge number
*
* @param number number
*/
fun setNumber(newNumber: Int) {
var number = newNumber
number = Math.max(0, number)
if (number != number1) {
number1 = number
updateBadgeBounds()
invalidateSelf()
}
}
/**
* Set badge text size
*
* @param textSize text size
*/
fun setTextSize(textSize: Float) {
if (badgeTextPaint.textSize != textSize) {
badgeTextPaint.textSize = textSize
updateBadgeBounds()
invalidateSelf()
}
}
/**
* Set badge text color, regardless of the values of r,g,b
*
* @param color text color
*/
fun setTextColor(@ColorInt color: Int) {
val textPaint: Paint = badgeTextPaint
if (textPaint.color != color) {
textPaint.color = color
invalidateSelf()
}
}
/**
* Set badge background color
*
* @param color background color
*/
fun setBackgroundColor(@ColorInt color: Int) {
if (backgroundColor != color) {
backgroundColor = color
invalidateSelf()
}
}
fun setVisible(visible: Boolean) {
setVisible(visible, /* restart= */false)
}
fun updateBadgeCoordinates(anchorView: View) {
if (anchorViewRef == null || anchorViewRef!!.get() !== anchorView) {
anchorViewRef = WeakReference(anchorView)
}
updateBadgeBounds()
invalidateSelf()
}
companion object {
private const val DEFAULT_STYLE = R.style.DefaultBadge
private const val DEFAULT_THEME_ATTR = R.attr.badgeDrawableStyle
private const val MAX_CIRCULAR_BADGE_NUMBER_COUNT = 9
private const val DEFAULT_MAX_BADGE_NUMBER = 99
private const val DEFAULT_EXCEED_MAX_BADGE_NUMBER_SUFFIX = "+"
private const val DEFAULT_BADGE_BACKGROUND_COLOR = -0xab8f
private const val DEFAULT_BADGE_TEXT_COLOR = Color.WHITE
const val TOP_END = Gravity.TOP or Gravity.END
const val TOP_START = Gravity.TOP or Gravity.START
const val BOTTOM_END = Gravity.BOTTOM or Gravity.END
const val BOTTOM_START = Gravity.BOTTOM or Gravity.START
const val STYLE_RECTANGLE = 0
const val STYLE_DOT = 1
fun create(context: Context): CustomBadgeDrawable {
return createFromAttributes(
context, /* attrs= */
null,
DEFAULT_THEME_ATTR,
DEFAULT_STYLE
)
}
fun create(context: Context, @StyleRes id: Int): CustomBadgeDrawable {
return createFromAttributes(
context, /* attrs= */
null,
DEFAULT_THEME_ATTR,
id
)
}
@SuppressLint("RestrictedApi")
fun createFromResource(
context: Context,
@XmlRes id: Int
): CustomBadgeDrawable {
val attrs =
parseDrawableXml(context, id, "badge")
@StyleRes var style = attrs.styleAttribute
if (style == 0) {
style = DEFAULT_STYLE
}
return createFromAttributes(
context,
attrs,
DEFAULT_THEME_ATTR,
style
)
}
private fun createFromAttributes(
context: Context,
attrs: AttributeSet?,
@AttrRes defStyleAttr: Int,
@StyleRes defStyleRes: Int
): CustomBadgeDrawable {
val badge = CustomBadgeDrawable(context)
badge.loadDefaultStateFromAttributes(context, attrs, defStyleAttr, defStyleRes)
return badge
}
fun attachBadgeDrawable(
customBadgeDrawable: CustomBadgeDrawable,
anchor: View
) {
if (customBadgeDrawable.callback == null) {
anchor.overlay.add(customBadgeDrawable)
anchor.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
customBadgeDrawable.updateBadgeCoordinates(anchor)
}
}
}
fun detachBadgeDrawable(
customBadgeDrawable: CustomBadgeDrawable,
anchor: View
) {
val overlay = anchor.overlay
overlay.remove(customBadgeDrawable)
}
fun parseDrawableXml(
context: Context,
@XmlRes id: Int,
startTag: CharSequence
): AttributeSet {
return try {
val parser: XmlPullParser = context.resources.getXml(id)
var type: Int
do {
type = parser.next()
} while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT)
if (type != XmlPullParser.START_TAG) {
throw XmlPullParserException("No start tag found")
}
if (!TextUtils.equals(parser.name, startTag)) {
throw XmlPullParserException("Must have a <$startTag> start tag")
}
Xml.asAttributeSet(parser)
} catch (e: XmlPullParserException) {
val exception = Resources.NotFoundException(
"Can't load badge resource ID #0x" + Integer.toHexString(id)
)
exception.initCause(e)
throw exception
} catch (e: IOException) {
val exception = Resources.NotFoundException(
"Can't load badge resource ID #0x" + Integer.toHexString(id)
)
exception.initCause(e)
throw exception
}
}
}
}
<style name="DefaultBadge">
<item name="badge_gravity">TOP_END</item>
</style>
<style name="BadgeTextLong" parent="DefaultBadge">
<item name="badge_number">999</item>
<item name="badge_maxNumber">1000</item>
<item name="badge_size">14dp</item>
<item name="badge_height">16dp</item>
<item name="badge_radii">4dp</item>
<item name="badge_shape">rectangle</item>
<item name="badge_horizontalOffset">8dp</item>
<item name="badge_verticalOffset">-2dp</item>
</style>
<style name="BadgeTextDot" parent="DefaultBadge">
<item name="badge_number">9</item>
<item name="badge_size">16dp</item>
<item name="badge_shape">dot</item>
<item name="badge_horizontalOffset">2dp</item>
<item name="badge_verticalOffset">-2dp</item>
</style>
<style name="BadgeDot" parent="DefaultBadge">
<item name="badge_size">6dp</item>
<item name="badge_shape">dot</item>
<item name="badge_horizontalOffset">2dp</item>
<item name="badge_verticalOffset">-2dp</item>
</style>
val notificationBadge = CustomBadgeDrawable.create(this, R.style.BadgeTextDot).apply { setNumber(count)}
attachBadgeDrawable(notificationBadge,yourView)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment