I'm trying to draw a line graph using Canvas and Paint on an Android App
First I generate some data points with generateData()
which creates random values for the Y
data point and i
times 50 for the X
data point.
I expected each X
point to be seperated by 50 pixels (as a scale) and thus draw a similar graph like this:
The aplication class
public class Plotter extends View {
private List<Float> xPosList, yPosList;
private List<Path> pathList;
private Path path;
private Paint paint;
private ConstraintLayout cl;
private TextView stockPriceView;
public Plotter(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.xPosList = new ArrayList<>();
this.yPosList = new ArrayList<>();
this.pathList = new ArrayList<>();
this.paint = new Paint();
this.paint.setStrokeWidth(20);
this.paint.setColor(Color.GREEN);
generateData();
}
/***
* Generates random float data points from 5 to 100 and creates a path to plot
*/
private void generateData() {
int min = 5;
int max = 100;
double random = 0;
float xPos = 0;
float yPos = 0;
for (int i = 1; i <= 20; i++) {
random = min + Math.random() * (max - min);
xPos = 50 * i; //50 pixels
yPos = (float)random;
this.xPosList.add(xPos);
this.yPosList.add(yPos);
path = new Path(); //Create path
path.moveTo(xPos, yPos); //Add values to path
this.pathList.add(path); //Add path to pathList
}
}
/***
* Clears the points list
*/
private void clearData() {
this.xPosList.clear();
this.yPosList.clear();
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
clearData();
generateData();
break;
case MotionEvent.ACTION_UP:
invalidate(); //Refresh canvas
break;
}
return true; //Activate event
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint p = new Paint();
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
p.setColor(Color.GREEN);
p.setStrokeWidth(10);
p.setStyle(Paint.Style.FILL);
/***
* Better use 50 by 50 pixels
*/
float startX = 0; //Start graph at bottom left
float startY = canvas.getHeight(); //Start at the bottom (max height)
float nextX = 0;
float nextY = 0;
for (int i = 0; i < this.xPosList.size(); i++){
nextX = this.xPosList.get(i);
//TODO: Find a better function
nextY = (canvas.getHeight() - this.yPosList.get(i));
canvas.drawLine(startX, startY, nextX, nextY, p); //Draw segment
startX = nextX; //Save previous X point
startY = nextY; //Save previous Y point
}
//TODO: Find a better way to manage this
cl = (ConstraintLayout) ((ViewGroup)this.getParent()); //get parent Layout
this.stockPriceView = cl.findViewById(R.id.stockPriceText); //access the sibling
if (this.stockPriceView != null) {
this.stockPriceView.setText((nextY)+""); //Write number
}
}
}
Although my current output is not far from desired, it is not correct.
It looks like your max y value will be 100px and that is not very much and pales in comparison to the 1000px max x value. You need to convert the y values to dps or some other scaling value to fill up more of your view.
In detail, within generateData()
x will be set to a range of 50..1,000 in increments of 50 while y values will be randomly assigned values between 5 and 100. In the drawing code, you use these values for drawLine()
which takes pixels for its arguments. 1,000 pixels in the x-direction will get you some distance on most devices (333.3 dps on a device with a density of 3 pixels/dp), but a maximum value of 100 for the y-value will get you at most 33.3 dps on the same device - that is 1/10 the distance.
Let's say that you want the x values to span the width of the Plotter view and, likewise for the y-axis, the values should span the height. So, when x == 0, the x value for drawLine()
should also be 0. When the x-value is 1,000, the x value for drawLine()
should be with width of the Plotter view or
xview = viewWidth * x/1,000
Likewise, for the y value for drawLine()
:
yview = viewHeight * y/100
Change the drawing for loop to something like this:
float viewWidth = getWidth();
float viewHeight = getHeight();
for (int i = 0; i < this.xPosList.size(); i++) {
nextX = this.xPosList.get(i);
nextY = this.yPosList.get(i);
canvas.drawLine(viewWidth * startX / 1000, viewHeight - (viewHeight * startY / 100),
viewWidth * nextX / 1000, viewHeight - (viewHeight * nextY / 100), p); //Draw segment
startX = nextX; //Save previous X point
startY = nextY; //Save previous Y point
}
and you will get a plot that looks something like this:
Here is another way using the canvas translate()
method:
float viewWidth = getWidth();
float viewHeight = getHeight();
canvas.save();
// Flip the canvas vertically.
canvas.scale(1f, -1f, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
for (int i = 0; i < this.xPosList.size(); i++) {
nextX = this.xPosList.get(i);
nextY = this.yPosList.get(i);
canvas.drawLine(viewWidth * startX / 1000, viewHeight * startY / 100,
viewWidth * nextX / 1000, viewHeight * nextY / 100, p); //Draw segment
startX = nextX; //Save previous X point
startY = nextY; //Save previous Y point
}
canvas.restore();