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: