Your First Custom View: Clubhouse ProgressBar!

No doubt about that Clubhouse is the most hottest social media recently. There is no special UI/UX which is never have seen anywhere else. But I’m impressed by the ProgressBar. Even if it is not special something, it’s quite pretty and simple!

It could be a good example of Custom View. Because its view is simple and doesn’t have heavy animation.


So what the heck is Clubhouse ProgressBar?




DotDrawable.class⚫️

class DotDrawable(private val radius: Float,
                  var paint: Paint) : Drawable() {

    override fun draw(canvas: Canvas) {
        canvas.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), radius, paint)
    }

    override fun setAlpha(alpha: Int) {}

    override fun setColorFilter(colorFilter: ColorFilter?) {}

    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}

This is Dot Class which is created in ProgressBar we gonna make. It draw a Circle(dot) at center of bounds which means something like drawing paper. We going to set bounds at ProgressBar Class.
Make sure that declare paint as a public var in Constructor! Not a private val or public val. We will check below why it should be a public var


attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ClubhouseProgressBar">
        <attr name="dotCount" format="integer"/>
        <attr name="dotRadius" format="dimension"/>
        <attr name="dotInterval" format="dimension"/>
        <attr name="activeColor" format="color"/>
        <attr name="inactiveColor" format="color"/>
        <attr name="animationDuration" format="integer" />
    </declare-styleable>
</resources>

Make this at res/values folder. This is Custom Attributes will be used at Layout. After all the work is done, you can specify attributes like below.

<your.package.name.ClubhouseProgressBar
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:dotCount="5"
    app:dotRadius="20dp"
    app:dotInterval="10dp"
    app:inactiveColor="@color/purple_200"
    app:activeColor="@color/purple_700"
    app:animationDuration="500" />




ClubhouseProgressBar.class

init {
        context.theme.obtainStyledAttributes(attrs, R.styleable.ChProgressBar, 0, 0).let {
            try {
                dotCount = it.getInt(R.styleable.ChProgressBar_dotCount, 3)
                dotRadius = it.getDimension(R.styleable.ChProgressBar_dotRadius, 65f)
                dotInterval = it.getDimension(R.styleable.ChProgressBar_dotInterval, 25f)
                activePaint.color = it.getColor(R.styleable.ChProgressBar_activeColor, Color.BLACK)
                inactivePaint.color = it.getColor(
                    R.styleable.ChProgressBar_inactiveColor,
                    Color.WHITE
                )
                animDuration = it.getInt(R.styleable.ChProgressBar_animationDuration, 200).toLong()
            } finally {
                it.recycle()
            }
        }

        for (i in 0 until dotCount) {
            dotList.add(DotDrawable(dotRadius, inactivePaint))
        }
    }

First of all, get Attributes that user set from Layout. And add Dot object into dotList(mutableList).


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)


    adjustDotBounds()
}

private fun adjustDotBounds() {

    val diameter = (dotRadius * 2).toInt()
    val totalDotWidth = (diameter*dotCount + dotInterval*(dotCount-1)).toInt()
    val diameterWithInterval = (diameter + dotInterval).toInt()
    val paddingStart = (measuredWidth - totalDotWidth)/2f

    var left = paddingStart.toInt()
	val top = 0
    var right = left + diameter
	val bottom = measuredHeight

    dotList.forEach {
        it.setBounds(left, top, right, bottom)
        left += diameterWithInterval
        right += diameterWithInterval
    }
}

Now it’s time to set bounds of each dot. totalDotWidth is the sum of every dot’s diameter and every dotInterval. For example, if dotRadius is 30, dotCount is 5 and dotInterval is 10, the totalDotWidth is a 340. paddingStart is for no matter what width of this view user sets, locate at center like gravity center. We don’t have to consider the height, because Clubhouse ProgressBar is only horizontal line.
Finally, set bounds of each dot by giving each coordinate. Dot will be drew center of this bounds.


override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    createAnimation()
}

 private fun createAnimation() {
    val paintEvaluator = TypeEvaluator<Paint> { fraction, _, _ ->

        when(fraction) {
            1f -> inactivePaint
            0f -> inactivePaint
            else -> activePaint
        }
    }

    dotList.forEach { dot ->
        val animator = ObjectAnimator.ofObject(dot, "paint", paintEvaluator, inactivePaint)
        animator.apply {
            duration = animDuration
            addUpdateListener { invalidate() }
        }
        animatorList.add(animator)
    }

    animatorSet.apply {
        playSequentially(animatorList)
        addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                start()
            }
        })
        start()
    }
}

We need animation to change color of dots. So change paint by ObjectAnimator. To ObjectAnimator access to paint property of Dot, we should have set as a public var as mention above. public var makes getter and setter automatically. If you work with Java, you should add setter method like below.

public void setPaint(Paint paint) {
    this.paint = paint;
}


In TypeEvaluator you can manipulate in fine detail animation. Observe fraction at Log when animation is running, you can catch how fraction works.


💫Do you want to see more specific full code? Visit my github repo

If there’s a mistake, always welcome your opinion!

android

Back to Top ↑

kotlin

Back to Top ↑

progressBar

Back to Top ↑

compose

Back to Top ↑

editText

Back to Top ↑