Android - bar chart using custom layout

This is a specific example of using custom layout in Android.

I will begin with showing the usage of the layout class (the example). The whole code for layout class BarChartLayout is listed at the end of this post.

Firstly, we will create a list of items we want to show in the chart:

data class Item(
    val name: String,
    val value : Double
)

val items = listOf(
    Item("April", 20.0),
    Item("May", 25.0),
    Item("June", 18.0),
    Item("July", 1.0),
    Item("August", 0.0)
)

Then we will prepare the BarCharLayout and add it to our activity (in this case I had a parent ViewGroup with ID main_layout):

val barChart = BarChartLayout(this)
findViewById<ViewGroup>(R.id.main_layout).addView(barChart)

For each item, we add its bar to the BarChartLayout. For this, three views are needed:

  • nameView - will show the name of the item
  • barView - will show the bar (the rectangle)
  • labelView - will show the value of the item
for (item in items) {
    val nameView = TextView(this)
    nameView.text = item.name
    nameView.setTypeface(nameView.typeface, Typeface.BOLD)
    nameView.setPadding(defaultPaddingH, defaultPaddingV, defaultPaddingH, defaultPaddingV)

    val barView = View(this)
    barView.setBackgroundColor(Color.rgb(116, 197, 237))
    barView.setPadding(defaultPaddingH, defaultPaddingV, defaultPaddingH, defaultPaddingV)
    barView.setOnClickListener{
        Toast.makeText(this, item.name, Toast.LENGTH_SHORT).show()
    }

    val labelView = TextView(this)
    labelView.text = item.value.toString()
    labelView.setTypeface(nameView.typeface, Typeface.BOLD)
    labelView.setPadding(defaultPaddingH, defaultPaddingV, defaultPaddingH, defaultPaddingV)

    barChart.add(BarChartLayout.Bar(nameView, barView, labelView, item.value))
}

We can use any view types, but obviously TextView is suitable for the views showing text. The barView can be simple View with filled background.

The views can be styled and configured as needed (note the OnClickListener for the barView). This is the main advantage of this approach.

And, finally, the BarChartLayout. Its only purpose is to properly lay out the child views (the 3 mentioned views for each item). This includes calculating the correct size of the bars according to the item values.

import android.content.Context
import android.view.View
import android.view.ViewGroup

class BarChartLayout(context: Context) : ViewGroup(context) {

    data class Bar(
        val nameView : View,
        val barView : View,
        val labelView : View,
        val value : Double
    )

    private val bars = mutableListOf<Bar>()

    private val barMarginH = dpToPx(context, 4)
    private val barMarginV = dpToPx(context, 2)

    fun add(bar: Bar) {
        bars.add(bar)

        addChildInternal(bar.nameView)
        addChildInternal(bar.barView)
        addChildInternal(bar.labelView)
    }

    private fun addChildInternal(child: View) {
        addView(child)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val captionMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
        val labelMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)

        for (bar in bars) {
            bar.nameView.measure(captionMeasureSpec, captionMeasureSpec)
            bar.labelView.measure(labelMeasureSpec, labelMeasureSpec)
        }

        val maxCaptionHeight = bars.map{it.nameView.measuredHeight}.max() ?: 0

        setMeasuredDimension(
            MeasureSpec.getSize(widthMeasureSpec),
            maxCaptionHeight * bars.size
        )
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        if (bars.size == 0) {
            return
        }

        val itemHeight = (b - t) / bars.size

        val nameRight = bars.map{it.nameView.measuredWidth}.max() ?: 0
        val maxValue = bars.map{it.value}.max() ?: 0.0

        for ((index, bar) in bars.withIndex()) {
            val nameLeft = nameRight - bar.nameView.measuredWidth
            val nameTop = index * itemHeight
            val nameBottom = nameTop + itemHeight

            bar.nameView.layout(
                nameLeft,
                nameTop,
                nameRight,
                nameBottom
            )

            var barWidth = 0
            if (maxValue > 0.0) {
                barWidth = (bar.value * (r - nameRight - barMarginH * 2) / maxValue).toInt()
            }

            val barLeft = nameRight
            val barTop = index * itemHeight
            val barRight = barLeft + barWidth + barMarginH * 2
            val barBottom = barTop + itemHeight
            bar.barView.layout(
                barLeft + barMarginH,
                barTop + barMarginV,
                barRight - barMarginH,
                barBottom - barMarginV
            )

            val labelWidth = bar.labelView.measuredWidth
            val spaceLeftForLabel = barWidth - 2 * barMarginH

            val labelLeft = if (spaceLeftForLabel >= labelWidth) {
                barRight - labelWidth - barMarginH
            } else {
                barRight
            }

            val labelTop = barTop;
            val labelRight = labelLeft + labelWidth
            var labelBottom = barBottom;

            bar.labelView.layout(
                labelLeft,
                labelTop,
                labelRight,
                labelBottom
            )
        }
    }
}

The result looks like this:

Bar Chart Layout Demo

Written on January 2, 2020