How to Draw Line Graph in Android
Android draw Line Chart with Canvas
For Android Development the Canvas Framework is the one of most important technology that you can learn, however is one of the most underestimated too. In this article I will explain how to create a simple Line Chart with Canvas, and how you can simplify the drawing process.
When you create a View and override the onDraw(…) you earn all power of that View, and all responsibilities too. An important responsibility is not put much process in this method, because it is called in every state of a view, every invalidade(…) , requestLayout(…), onSizeChanged(..), etc… If it have a lot of process like calculations, loops or object instantiation, then your application will have Main Thread problems, and lost of performance.
I will show below a strategy for separate your view in the data responsibility, and drawing responsibility. In this way it's easy to pass and update the content of your chart.
For the Data content we need to create an Adapter that you pass and update the Chart data, and notify when the data has changed.
I create an abstract class with some common implementations that call's ChartAdapter:
open val dataBounds: RectF
get() {
val count = count
val hasBaseLine = hasBaseLine()
var minY = Float.MAX_VALUE
var maxY = -Float.MAX_VALUE
var minX = Float.MAX_VALUE
var maxX = -Float.MAX_VALUE
for (i in 0 until count) {
val x = getX(i)
minX = minX.coerceAtMost(x)
maxX = maxX.coerceAtLeast(x)
val y = getY(i)
minY = minY.coerceAtMost(y)
maxY = maxY.coerceAtLeast(y)
}return createRectF(minX, minY, maxX, maxY)
}protected fun createRectF(
open fun hasBaseLine(): Boolean =
left: Float,
top: Float,
right: Float,
bottom: Float
): RectF = RectF(left, top, right, bottom)
false
This class has some functions to be implemented by your child:
abstract val count : Int abstract fun getItem(index: Int): Any
abstract fun getY(index: Int): Float
abstract fun getX(index: Int): Float
To notify the content change I used the DataSetObservable, but you can use any observer you want like LiveData an RX frameworks.
private val observable = DataSetObservable() fun notifyDataSetChanged() {
observable.notifyChanged()
}fun notifyDataSetInvalidated() {
observable.notifyInvalidated()
}fun registerDataSetObserver(observer: DataSetObserver) {
observable.registerObserver(observer)
}fun unregisterDataSetObserver(observer: DataSetObserver) {
observable.unregisterObserver(observer)
}
For the LineChart the adapter implementation is:
open class LineChartAdapter(protected var yData: FloatArray = floatArrayOf()) : ChartAdapter() { override val count: Int
get() = yData.size open fun
setData(yData: FloatArray) {
this.yData = yData
notifyDataSetChanged()
}
fun setDataWithoutNotify(yData: FloatArray) {
this.yData = yData
}
override fun hasBaseLine(): Boolean =
containsNegativeValue()
private fun containsNegativeValue(): Boolean {
for (value in yData) {
if (value < 0)
return true
}
return false
}
override fun getItem(index: Int): Any =
yData[index]
override fun getY(index: Int): Float =
yData[index]
fun clearData(){
setData(floatArrayOf())
}
}
Note that we receive a FloatArray as parameter that represents your line chart(Y columns). The abstract methods getItem(…), getY(…) was implemented returning the Y data. The other methods is just to set new data and notify the super.
Now we have the data layer created, and need to consume it in the view. I will not explain in this post how to get and set attrs in a View, but will create an article talking about that.
To convert data in pixel I use an class called ScaleHelper, that the implementation will help a lot.
class ScaleHelper(adapter: ChartAdapter, contentRect: RectF, lineWidth: Float, fill: Boolean) {
val width: Float
val height: Float
val size: Int
private val xScale: Float
private val yScale: Float
private val xTranslation: Float
private val yTranslation: Float init {
val leftPadding = contentRect.left
val topPadding = contentRect.top var
lineWidthOffset = 0.0f
if (!fill)
lineWidthOffset = lineWidth
this.width = contentRect.width() - lineWidthOffset
this.height = contentRect.height() - lineWidthOffset
this.size = adapter.count val
bounds = adapter.dataBoundsbounds.inset((if (bounds.width() == 0f) -1 else 0).toFloat(), (if (bounds.height() == 0f) -1 else 0).toFloat())
val minX = bounds.left
val maxX = bounds.right
val minY = bounds.top
val maxY = bounds.bottom this
.xScale = width / (maxX - minX)
this.xTranslation = leftPadding - minX * xScale + lineWidthOffset / 2
this.yScale = height / (maxY - minY)
this.yTranslation = minY * yScale + topPadding + lineWidthOffset / 2
}
fun getX(rawX: Float): Float =
rawX * xScale + xTranslation fun
getY(rawY: Float): Float =
height - rawY * yScale + yTranslation
}
This class will return the respective pixel value of the Chart Data based on the view content rect. Note that contentRect represents the view size, and with this information the Helper calculate the View width and heigth.
Now we have the View's Adapter, then we need to create the View. The first thing we need to do is calculate where canvas will draw our data and the space in view that we can use. We need to override two View Methods to know it.
open class LineChartView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.LineChartStyle,
defStyleRes: Int = R.style.LineChartView
) : View(context, attrs, defStyleAttr) { protected val contentRect = RectF() override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
super.onSizeChanged(w, h, oldW, oldH)
updateContentRect()
populatePath() /*I will show later*/
}override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
private fun updateContentRect() {
super.setPadding(left, top, right, bottom)
updateContentRect()
populatePath() /*I will show later*/
}
contentRect.set(
paddingStart.toFloat(),
paddingTop.toFloat(),
(width - paddingEnd).toFloat(),
(height - paddingBottom).toFloat()
)
} }
In this two methods we receive a size changed delegation, then we set the View limits calculated with View paddings and dimensions. Every time when this methods are called we need to calculate and redraw our canvas.
The next step is use the Rect to create a Canvas Path with all points to draw. Using our ScaleHelper we can get the x and y point in View to our Adapter Data.
private fun populatePath() {scaleHelper = ScaleHelper(adapter, contentRect, lineWidth, isFillInternal)
renderPath.reset()for (i in 0 until adapter.count) {
val x = scaleHelper.getX(adapter!!.getX(i))
val y = scaleHelper.getY(adapter!!.getY(i))if (i == 0)
override fun onDraw(canvas: Canvas) {
renderPath.moveTo(x, y)
else
renderPath.lineTo(x, y)
}
invalidate()
}
super.onDraw(canvas)
canvas.drawPath(renderPath, renderPaint)
}
We are using two Path methods moveTo(…) to put our path in the first position, and lineTo(…) to create a line between the first point to second. Then we call invalidate() method to call View redrawing. In onDraw(…) method we call Canvas drawPath(…) method to put in View our path.
We need to note some that all callculations are done before onDraw(…). It's important to know that this method is called after all View change (Size, padding, lifecycle), then put heavy processes here will result in main thread performance problems.
How to Draw Line Graph in Android
Source: https://medium.com/gustavo-santorio/android-draw-line-chart-with-canvas-106120579d96
0 Response to "How to Draw Line Graph in Android"
Post a Comment