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