Skip to content

Instantly share code, notes, and snippets.

@programmerr47
Created May 26, 2019 04:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save programmerr47/3d363516f5e15cbf910dfde309ef24b7 to your computer and use it in GitHub Desktop.
Save programmerr47/3d363516f5e15cbf910dfde309ef24b7 to your computer and use it in GitHub Desktop.
Floating action button with counting badge
class CounterFloatingActionButton @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FloatingActionButton(context, attrs, defStyleAttr) {
private val textPaint = TextPaint(ANTI_ALIAS_FLAG).apply {
textAlign = Paint.Align.LEFT
}
private val tintPaint = Paint(ANTI_ALIAS_FLAG)
private var countStr: String
private var countMaxStr: String
private var counterBounds: RectF = RectF()
private var counterTextBounds: Rect = Rect()
private var counterMaxTextBounds: Rect = Rect()
private var counterPossibleCenter: PointF = PointF()
private var fabBounds: Rect = Rect()
var counterTextColor: Int
get() = textPaint.color
set(value) {
val was = textPaint.color
if (was != value) {
textPaint.color = value
invalidate()
}
}
var counterTint: Int
get() = tintPaint.color
set(value) {
val was = tintPaint.color
if (was != value) {
tintPaint.color = value
invalidate()
}
}
var counterTextSize: Float
get() = textPaint.textSize
set(value) {
val was = textPaint.textSize
if (was != value) {
textPaint.textSize = value
invalidate()
requestLayout()
}
}
var counterTypeface: Typeface?
get() = textPaint.typeface
set(value) {
val was = textPaint.typeface
if (was != value) {
textPaint.typeface = value
invalidate()
requestLayout()
}
}
var counterTextPadding: Float = 0f
set(value) {
if (field != value) {
field = value
invalidate()
requestLayout()
}
}
var maxCount: Int = 9
set(@IntRange(from = 1) value) {
if (field != value) {
field = value
countMaxStr = "$value+"
requestLayout()
}
}
var count: Int = 0
set(@IntRange(from = 0) value) {
if (field != value) {
field = value
countStr = countStr(value)
textPaint.getTextBounds(countStr, counterTextBounds)
invalidate()
}
}
init {
countStr = countStr(count)
textPaint.getTextBounds(countStr, counterTextBounds)
countMaxStr = "$maxCount+"
attrs?.let { initAttrs(attrs) }
}
@SuppressWarnings("ResourceType", "Recycle")
private fun initAttrs(attrs: AttributeSet) {
context.obtainStyledAttributes(attrs, R.styleable.CounterFloatingActionButton).use {
counterTextPadding = getDimension(R.styleable.CounterFloatingActionButton_counterTextPadding, 0f)
counterTint = getColor(R.styleable.CounterFloatingActionButton_counterTint, 0)
context.obtainStyledAttributes(getResourceId(R.styleable.CounterFloatingActionButton_counterTextAppearance, R.style.Text_Primary), TEXT_APPEARANCE_SUPPORTED_ATTRS).use {
counterTextSize = getDimension(0, 0f)
counterTextColor = getColor(1, 0)
counterTypeface = getTypeface(context, 2) ?: getTypeface(context, 3)
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
calculateCounterBounds(counterBounds)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (count > 0) {
canvas.drawCircle(counterBounds, tintPaint)
val textX = counterBounds.centerX() - counterTextBounds.width() / 2f - counterTextBounds.left
val textY = counterBounds.centerY() + counterTextBounds.height() / 2f - counterTextBounds.bottom
canvas.drawText(countStr, textX, textY, textPaint)
}
}
private fun calculateCounterBounds(outRect: RectF) {
getMeasuredContentRect(fabBounds)
calculateCounterCenter(fabBounds, counterPossibleCenter)
textPaint.getTextBounds(countMaxStr, counterMaxTextBounds)
val counterDiameter = max(counterMaxTextBounds.width(), counterMaxTextBounds.height()) + 2 * counterTextPadding
val counterRight = min(counterPossibleCenter.x + counterDiameter / 2, fabBounds.right.toFloat())
val counterTop = max(counterPossibleCenter.y - counterDiameter / 2, fabBounds.top.toFloat())
outRect.set(counterRight - counterDiameter, counterTop, counterRight, counterTop + counterDiameter)
}
private fun calculateCounterCenter(inBounds: Rect, outPoint: PointF) {
val radius = min(inBounds.width(), inBounds.height()) / 2f
calculateCounterCenter(radius, outPoint)
outPoint.x = inBounds.centerX() + outPoint.x
outPoint.y = inBounds.centerY() - outPoint.y
}
private fun calculateCounterCenter(radius: Float, outPoint: PointF) =
calculateCounterCenter(radius, (PI / 4).toFloat(), outPoint)
private fun calculateCounterCenter(radius: Float, angle: Float, outPoint: PointF) {
outPoint.x = radius * cos(angle)
outPoint.y = radius * sin(angle)
}
private fun countStr(count: Int) = if (count > maxCount) "$maxCount+" else count.toString()
companion object {
val TEXT_APPEARANCE_SUPPORTED_ATTRS = intArrayOf(android.R.attr.textSize, android.R.attr.textColor, androidx.appcompat.R.attr.fontFamily, android.R.attr.fontFamily)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment