How to customize a ProgressBar in Android xml?

148 Views Asked by At

I need to create a ProgressBar in android like this: enter image description here

I have the figma UI ready and already converted the drawables from svg to vector xml. But now I am stuck here to create the look of my ProgressBar like this. What I have done so far is

class CustomProgressBar(context: Context, attrs: AttributeSet) : View(context, attrs) {
    private var progress: Int = 0 // Progress in percentage
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val batteryIcon: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_battery)
    private val batteryBackground: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.progress_bar_grey)
    private val batteryProgress: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.progress_bar_green)
    private val rect = Rect()

    fun setProgress(value: Int) {
        progress = value.coerceIn(0, 100) // Ensure progress is within 0-100%
        invalidate() // Request a redraw
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        // Draw battery background
        canvas.drawBitmap(batteryBackground, null, rect, paint)

        // Calculate width of the progress based on current progress
        val progressWidth = (width * progress / 100f).roundToInt()

        // Draw battery progress
        rect.right = progressWidth
        canvas.save()
        canvas.clipRect(rect)
        canvas.drawBitmap(batteryProgress, null, this.rect, paint)
        canvas.restore()

        // Draw the battery icon on top
        // Adjust the position as per your design requirements
        val iconLeft = width / 2 - batteryIcon.width / 2
        val iconTop = height / 2 - batteryIcon.height / 2
        canvas.drawBitmap(batteryIcon, iconLeft.toFloat(), iconTop.toFloat(), paint)

        // Draw the percentage text
        paint.color = Color.BLACK // Change as per your design
        paint.textSize = 40f // Change as per your design
        val text = "$progress%"
        val textWidth = paint.measureText(text)
        val textX = width / 2 - textWidth / 2
        val textY = height / 2 - (paint.descent() + paint.ascent()) / 2
        canvas.drawText(text, textX, textY, paint)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        // Update the rect for drawing background and progress
        rect.set(0, 0, w, h)
    }
}

then I created layer-list like this:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/background">
        <bitmap android:src="@drawable/progress_bar_grey" />
    </item>
    <item android:id="@+id/progress">
        <clip
            android:clipOrientation="horizontal"
            android:gravity="left">
            <bitmap android:src="@drawable/progress_bar_green" />
        </clip>
    </item>
</layer-list>

when I take this progressbar in my Activity I am getting nullpointer exception in

private val batteryIcon: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_battery)

the error report is:

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.volarious.customviews/com.volarious.customviews.MainActivity}: android.view.InflateException: Binary XML file line #13 in com.volarious.customviews:layout/activity_main: Binary XML file line #13 in com.volarious.customviews:layout/activity_main: Error inflating class com.volarious.customviews.CustomProgressBar
                                                                                                            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3822)
                                                                                                            at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3963)
                                                                                                            at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
                                                                                                            at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
                                                                                                            at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
                                                                                                            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2468)
                                                                                                            at android.os.Handler.dispatchMessage(Handler.java:106)
                                                                                                            at android.os.Looper.loopOnce(Looper.java:205)
                                                                                                            at android.os.Looper.loop(Looper.java:294)
                                                                                                            at android.app.ActivityThread.main(ActivityThread.java:8248)
                                                                                                            at java.lang.reflect.Method.invoke(Native Method)
                                                                                                            at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
                                                                                                            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
                                                                                                        Caused by: android.view.InflateException: Binary XML file line #13 in com.volarious.customviews:layout/activity_main: Binary XML file line #13 in com.volarious.customviews:layout/activity_main: Error inflating class com.volarious.customviews.CustomProgressBar
                                                                                                        Caused by: android.view.InflateException: Binary XML file line #13 in com.volarious.customviews:layout/activity_main: Error inflating class com.volarious.customviews.CustomProgressBar
                                                                                                        Caused by: java.lang.reflect.InvocationTargetException
                                                                                                            at java.lang.reflect.Constructor.newInstance0(Native Method)
                                                                                                            at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
                                                                                                            at android.view.LayoutInflater.createView(LayoutInflater.java:866)
                                                                                                            at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1018)
                                                                                                            at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:973)
                                                                                                            at android.view.LayoutInflater.rInflate(LayoutInflater.java:1135)
                                                                                                            at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1096)
                                                                                                            at android.view.LayoutInflater.inflate(LayoutInflater.java:694)
                                                                                                            at android.view.LayoutInflater.inflate(LayoutInflater.java:538)
                                                                                                            at android.view.LayoutInflater.inflate(LayoutInflater.java:485)
                                                                                                            at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:775)
                                                                                                            at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:197)
                                                                                                            at com.volarious.customviews.MainActivity.onCreate(MainActivity.kt:11)
                                                                                                            at android.app.Activity.performCreate(Activity.java:8621)
                                                                                                            at android.app.Activity.performCreate(Activity.java:8599)
                                                                                                            at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456)
                                                                                                            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3804)
                                                                                                            at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3963)
                                                                                                            at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
                                                                                                            at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
                                                                                                            at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
                                                                                                            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2468)
                                                                                                            at android.os.Handler.dispatchMessage(Handler.java:106)
                                                                                                            at android.os.Looper.loopOnce(Looper.java:205)
                                                                                                            at android.os.Looper.loop(Looper.java:294)
                                                                                                            at android.app.ActivityThread.main(ActivityThread.java:8248)
                                                                                                            at java.lang.reflect.Method.invoke(Native Method)
                                                                                                            at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
                                                                                                            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
    2024-01-31 16:18:48.259 13110-13110 AndroidRuntime          com.volarious.customviews            E  Caused by: java.lang.NullPointerException: decodeResource(...) must not be null
                                                                                                            at com.volarious.customviews.CustomProgressBar.<init>(CustomProgressBar.kt:24)
                                                                                                            ... 29 more

can anyone help me to provide a clear guidance what I am missing and what I can do?

1

There are 1 best solutions below

0
dardan.g On

Since you are using a custom View to draw, you don't need <layer-list> for progress (your view is not a progress, is custom)

When drawing bitmaps with canvas, it has to be png. Set your bitmaps (ic_battery, progress_bar_grey, progress_bar_green) as pngs

then, in order to clip progress bitmap, you need an arc instead of rect: Modify onDraw() to:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    // Draw battery background
    canvas.drawBitmap(batteryBackground, null, rect, paint)

    val figmaStartAngle = 35f// 35f will be the angle of bitmap from 0 label (get from figma/designer)
    val figmaStartToEndAngle = 70f// 70f will be angle between label:0 to:100 of your bitmap in figma
    // canvas starts from right, rotate+90deg to start from bottom
    val startAngle = 90f + figmaStartAngle
    val endAngle = 360f - figmaStartToEndAngle
    // convert 0,100 to 0deg,360deg
    // in your case to bitmap start percentage to end percentage
    val sweepAngle = (progress / 100f) * endAngle

    val path = Path() // declare in top level class
    path.moveTo(width / 2f, height / 2f)
    path.lineTo(0f, height.toFloat())
    path.arcTo(0f, 0f, width.toFloat(), height.toFloat(), startAngle, sweepAngle, true)
    path.lineTo(width / 2f, height / 2f)

    canvas.save()
    canvas.clipPath(path)
    canvas.drawBitmap(batteryProgress, null, this.rect, paint)
    canvas.restore()

    // if you want to test clip arc
    // canvas.drawPath(path, paint)


    // Draw the battery icon on top
    // Adjust the position as per your design requirements
    val iconLeft = width / 2 - batteryIcon.width / 2
    val iconTop = height / 2 - batteryIcon.height / 2
    canvas.drawBitmap(batteryIcon, iconLeft.toFloat(), iconTop.toFloat(), paint)

    // Draw the percentage text
    paint.color = Color.BLACK // Change as per your design
    paint.textSize = 40f // Change as per your design
    val text = "$progress%"
    val textWidth = paint.measureText(text)
    val textX = width / 2 - textWidth / 2
    val textY = height * .7f - (paint.descent() + paint.ascent()) / 2
    canvas.drawText(text, textX, textY, paint)
}

check your start & end angles in figma, set to figmaStartAngle and figmaStartToEndAngle in order to have an accurate measurement

For the thumb: you can have a separate bitmap and draw into the sweep point