Sunday, April 5, 2015

MAKING NICE LOOKING BAR CHARTS IN ANDROID

We wanted to make a simple but nice looking bar chart for one of our apps, but we couldn't find a graph library that looked the way we wanted it to and that supported older versions of Android. So instead we created our own custom Bar Chart view, that we can use in our XML layouts just like the standard Android views.

Below I set out how you can produce a simple bar chart which takes user input from within the app, performs a calculation and presents the results in a Custom View:


If you have any questions about this, you can contact us on support@stonekick.com.

CREATE CUSTOM VIEW CLASS

First we need to create our view class - this needs to extend the standard Android View class:
public class BarChart extends View {
    public BarChart(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialise();
    }
}

CREATE PAINT OBJECTS

We're going to use the Paint class to control the appearance of what we draw - the colours, text size & line width. We create a few of these in our initialise method - one for each drawing style that we will need.
void initialise() {
    textPaint = new Paint();
    linePaint = new Paint();
    boxPaint1 = new Paint();
    linePaint.setStrokeWidth(1);
    linePaint.setColor(0xFFC5C5C5);
    textPaint.setColor(0xFFC5C5C5);
    textPaint.setTextSize(14*scaleFactor);
    boxPaint1.setColor(0xFFFFBB33);
}

SCALE FACTOR

You will notice in the code above that the text size is multiplied by a scale factor. This is to take account of different display densities. All of our drawing is done in pixels, but the different screens on Android devices have different numbers of pixels per inch (ppi). So if we set our text to be 14 pixels high, it will be drawn a different size on different devices. We don't want that, so we increase the text size in proportion to the device's ppi. You can use the Android display metrics class to work out the scale factor:
DisplayMetrics metrics = getResources().getDisplayMetrics();
scaleFactor = metrics.density;

OVERRIDE DRAW METHOD

Every time Android wants to draw your custom view, it will call the onDraw method, so we need to override that:
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
}
Android passes in a Canvas object when it calls onDraw. This has various methods that you can use to create your graphics, for example:
To draw a line segment with the specified start and stop x,y coordinates:
canvas.drawLine(float startX, float startY, float stopX , float stopY, Paint paint);

To draw a rectangle using a specified paint:
canvas.drawRect(float left, float top, float right , float bottom, Paint paint);

To draw text, with origin t (xy), using the specified paint:
canvas.drawText(String text, float x, float y, Paint paint);
Again, remember that by default all of the drawing is done in pixels, so your co-ordinate system is in pixels starting from the top left hand corner of the canvas.
The easiest way to position the objects within your canvas is to use the getWidth and getHeight methods to determine the width and height of your canvas. You can then use multiples of this to position each object.
In the below example we used the width and height values along with the scale factor previously calculated to draw the line underneath the bar chart. This has the same amount of padding to the left and right of the line, with a larger amount below the line to allow for the text labelling each bar:
int fullWidth = getWidth();
int fullHeight = getHeight();
int padding = (int) (10*scaleFactor);
canvas.drawLine(padding, fullHeight-2.5*padding, fullWidth-padding , fullHeight-2.5*padding,
linePaint);
The other elements within our bar chart view were then positioned in a similar manner, using multiples of the width, height, and padding.
                               Android Bar Chart Positioning

VARYING THE HEIGHT OF THE BARS

It is likely that you will want the height of the bars within the chart to vary depending on user input to the app. We've included a couple of methods to set the heights of the bars in our custom view - you can tailor this to meet your needs. The important part here is we need to call invalidate() after changing the heights of the bars - this tells Android that it needs to redraw the view.
// Earnings and cost are calculated values based on user inputs.
// The Main app Activity calculates these and calls the below methods
// when the user inputs a new value

public void setCost (double value) {
    cost = value;
    invalidate();
}
public void setEarnings (double value) {
    earnings = value;
    invalidate();
}
The user inputs can then be used to proportion the height of one bar against the other:
int fullWidth = getWidth();
int fullHeight = getHeight();
int maxBarHeight = fullHeight-80;
float bar1height;
float bar2height;
int padding = (int) (10*scaleFactor);
float middle = (float) (fullWidth*0.5);
float quarter = (float) (fullWidth*0.25);
float threequarter = (float) (fullWidth*0.75);


if(earnings>cost) {
bar2height = (float) maxBarHeight;
    bar1height = (float) (cost/earnings*maxBarHeight);
}else{
    bar1height = (float) maxBarHeight;
    bar2height = (float) (earnings/cost*maxBarHeight);
}

int bar1bottom = fullHeight-padding*3;
float bar1top = bar1bottom-bar1height;
canvas.drawRect(padding*2, bar1top, middle-padding , bar1bottom, boxPaint1);

int bar2bottom = fullHeight-padding*3;
float bar2top = bar2bottom-bar2height;
canvas.drawRect(middle+padding, bar2top, fullWidth-padding*2, bar2bottom, boxPaint2);

BRINGING CUSTOM VIEW INTO LAYOUT FILE

Once you have created your BarChart custom view class you can then start to use it within your layout files:
<com.stonekick.pvassistant.BarChart
    android:layout_width="match_parent"
    android:layout_height="250dp"
    android:id="@+id/bcResults"
    android:background="#ffffff">

FULL CODE

That's it. If you put all of the pieces together you end up with this:
package com.stonekick.pvassistant;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;



public class BarChart extends View {
    Paint textPaint;
    Paint linePaint;
    Paint boxPaint1;
    Paint boxPaint2;
    Paint textPaint1;
    Paint textPaint2;
    double cost;
    double earnings;
    float scaleFactor;

    public BarChart(Context context) {
        super(context);
        initialise();
    }
    public BarChart(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialise();
    }
    public BarChart(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initialise();
    }
    void initialise() {

        DisplayMetrics metrics = getResources().getDisplayMetrics();
        scaleFactor = metrics.density;

        textPaint = new Paint();
        linePaint = new Paint();
        boxPaint1 = new Paint();
        boxPaint2 = new Paint();
        textPaint1 = new Paint();
        textPaint2 = new Paint();
        linePaint.setStrokeWidth(1);
        linePaint.setColor(0xFFC5C5C5);
        textPaint.setColor(0xFFC5C5C5);
        textPaint.setTextSize(14*scaleFactor);
        boxPaint1.setColor(0xFFFFBB33);
        boxPaint2.setColor(0xFF218b21);
        textPaint1.setColor(0xFFFFBB33);
        textPaint2.setColor(0xFF218b21);
        textPaint1.setTextSize(14*scaleFactor);
        textPaint2.setTextSize(14*scaleFactor);

    }

    // Earnings and cost are calculated values based on user inputs.
    // The Main app Activity calculates these and calls the below methods
    // when the user inputs a new value

    public void setCost (double value) {
        cost = value;
        invalidate();
    }

    public void setEarnings (double value) {
        earnings = value;
        invalidate();
    }

    protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int fullWidth = getWidth();
            int fullHeight = getHeight();
            int padding = (int) (10*scaleFactor);
            int maxBarHeight = fullHeight-5*padding;
            float bar1height;
            float bar2height;

            if(earnings>cost) {
                bar2height = (float) maxBarHeight;
                bar1height = (float) (cost/earnings*maxBarHeight);
            }else{
                bar1height = (float) maxBarHeight;
                bar2height = (float) (earnings/cost*maxBarHeight);
            }

            canvas.drawLine(padding, fullHeight-25*scaleFactor, fullWidth-padding , fullHeight-25*scaleFactor, linePaint);

            float middle = (float) (fullWidth*0.5);
            float quarter = (float) (fullWidth*0.25);
            float threequarter = (float) (fullWidth*0.75);

            int bar1bottom = fullHeight-padding*3;
            float bar1top = bar1bottom-bar1height;
            float val1pos = bar1top-padding;
            canvas.drawRect(padding*2, bar1top, middle-padding , bar1bottom, boxPaint1);
            canvas.drawText("Cost", quarter-padding, fullHeight-padding, textPaint1);
            canvas.drawText("$" + cost/1000 + "K", quarter-padding, val1pos, textPaint);

            int bar2bottom = fullHeight-padding*3;
            float bar2top = bar2bottom-bar2height;
            float val2pos = bar2top-padding;
            canvas.drawRect(middle+padding, bar2top, fullWidth-padding*2, bar2bottom, boxPaint2);
            canvas.drawText("Earnings", threequarter-padding*3, fullHeight-padding, textPaint2);
            canvas.drawText("$" + earnings/1000 + "K", threequarter-padding*2, val2pos, textPaint);
        }

}
Nguồn : http://www.stonekick.com/techblog/creating_nice_looking_bar_charts.html