android customview overriden onDraw(): canvas draws all rectangles black - android

I've got a problem with a custom view i use. It draws a grid that i use to represent a floorplan, with a start and current position on it (colored rectangles).
(Code here: https://pastebin.com/8SExmtAp).
In short, i initialize different paints like this:
private void initPaints()
{
waypointPaint = new Paint(Color.parseColor("#800080"));
currentCoordinatePaint = new Paint(Color.RED);
linePaint = new Paint(Color.BLACK);
startCoordinatePaint = new Paint(Color.BLUE);
}
and use them in onDraw() like this:
// color the current coordinates
Coordinates currentCoords = Model.getCurrentCoordinates();
if (currentCoords != null)
{
canvas.drawRect((float) currentCoords.getX() * cellWidth, (float) currentCoords.getY() * cellHeight,
(float) (currentCoords.getX() + 1) * cellWidth, (float) (currentCoords.getY() + 1) * cellHeight,
currentCoordinatePaint);
}
Coordinates startCoordinate = Model.startCoordinate;
if (startCoordinate != null && startCoordinate != currentCoords)
{
canvas.drawRect((float) startCoordinate.getX() * cellWidth, (float) startCoordinate.getY() * cellHeight,
(float) (startCoordinate.getX() + 1) * cellWidth, (float) (startCoordinate.getY() + 1) * cellHeight,
startCoordinatePaint);
}
However, instead of getting a blue one for the startposition and a red one for the current position, both of them are black, see:
Screenshot of app
The documentation on the drawRect(...) Method i use just states the following:
Draw the specified Rect using the specified paint. The rectangle will be filled or framed based on the Style in the paint.
So..i don't really see where the code is wrong and why i am getting the result i get. Maybe someone of you knows why?

Paint constructor you are using expects int flags as a parameter, not the fill color.
Try:
currentCoordinatePaint = new Paint();
currentCoordinatePaint.setStyle(Paint.Style.FILL);
currentCoordinatePaint.setColor(Color.RED);

Like josef.adamcik statet, i was wrong about the constructors i used for the paint objects. Changing the code to
private void initPaints()
{
waypointPaint = new Paint();
waypointPaint.setColor(Color.GREEN);
waypointPaint.setStyle(Paint.Style.FILL);
currentCoordinatePaint = new Paint();
currentCoordinatePaint.setColor(Color.RED);
currentCoordinatePaint.setStyle(Paint.Style.FILL);
linePaint = new Paint();
linePaint.setColor(Color.BLACK);
linePaint.setStyle(Paint.Style.STROKE);
startCoordinatePaint = new Paint();
startCoordinatePaint.setColor(Color.BLUE);
startCoordinatePaint.setStyle(Paint.Style.FILL);
}
did the trick.

Related

Item drawn in Custom view onDraw() keeps disappearing

I'm drawing 3 things in my custom view in the onDraw() method: a vector drawable, a simple line and a triangle (made from 4 Points and a Path). This custom view is displayed in a tab.
If I swipe to go to another tab I see that the system calls onDraw(). When I return to the tab holding my custom view the vector drawable and simple line are still visible but the triangle has disappeared. If I now swipe to another tab, onDraw() runs again and back in the tab with the custom view, all items (including the triangle) are now visible. This disappearing/appearing continues to happen as I swipe back and forth. Why is my triangle disappearing?
UPDATE 1 (hacky fix):
I've tried experimenting and notice that when I move my triangle Path object creation out of my init() method and put it directly in the onDraw() method - then all works well, nothing disappears. But, I now get the 'Avoid object allocations during draw' warning as I'm creating this object in onDraw();
UPDATE 2 (better fix?):
After more experimenting, it's definitely the Path causing this problem. Another solution to this - which doesn't incur the 'Avoid object allocations during draw' warning is: keep Path creation in init() and remove the line of code 'myPath.setFillType(Path.FillType.EVEN_ODD)'. It solves my problem, but I've no idea why.
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Co-ordinates
int width = getWidth();
int halfWidth = width/2;
int left = 0;
int top = 0;
int centreX = left + halfWidth;
int centreY = top + halfWidth;
int baseSize = Math.round((float)(halfWidth * 0.05));
// Vector drawable - always draws fine!
myVectorDrawable.setBounds(left, top, left + width, top + width);
myVectorDrawable.draw(canvas);
// Simple line - always draws fine!
canvas.drawLine(left, top, 20, 20, paint);
// Triangle - sometimes visible, sometimes disappears!
Point myTriangleBottomMiddle = new Point(centreX, centreY);
Point myTriangleBottomLeft = new Point(centreX, centreY + baseSize);
Point myTriangleBottomRight = new Point(centreX, centreY - baseSize);
Point myTriangleTopMiddle = new Point(centreX + halfWidth, centreY);
myPath.moveTo(myTriangleBottomMiddle.x, myTriangleBottomMiddle.y);
myPath.lineTo(myTriangleBottomLeft.x, mTriangleBottomLeft.y);
myPath.lineTo(myTriangleTopMiddle.x, myTriangleTopMiddle.y);
myPath.lineTo(myTriangleBottomRight.x, myTriangleBottomRight.y);
mPath.close();
canvas.drawPath(myPath, myPaint);
}
Below the code where I set up stuff so as not to burden the onDraw() method.
private void init() {
// Vector drawable
myVectorDrawable = r.getDrawable(R.drawable.gauge_dial);
// Triangle path - ** THIS BEING HERE SEEMS TO BE THE PROBLEM **
myPath = new Path();
myPath.setFillType(Path.FillType.EVEN_ODD);
// Triangle Paint
myPaint = new Paint();
myPaint.setColor(r.getColor(R.color.black));
myPaint.setStrokeWidth(2);
myPaint.setAntiAlias(true);
myPaint.setStyle(Paint.Style.FILL);
// Simple line paint
Paint paint = new Paint();
paint.setColor(Color.BLACK);
}
Add a call to reset() on the path.
// Triangle - sometimes visible, sometimes disappears!
Point myTriangleBottomMiddle = new Point(centreX, centreY);
Point myTriangleBottomLeft = new Point(centreX, centreY + baseSize);
Point myTriangleBottomRight = new Point(centreX, centreY - baseSize);
Point myTriangleTopMiddle = new Point(centreX + halfWidth, centreY);
myPath.reset();
myPath.moveTo(myTriangleBottomMiddle.x, myTriangleBottomMiddle.y);
myPath.lineTo(myTriangleBottomLeft.x, mTriangleBottomLeft.y);
myPath.lineTo(myTriangleTopMiddle.x, myTriangleTopMiddle.y);
myPath.lineTo(myTriangleBottomRight.x, myTriangleBottomRight.y);
mPath.close();
canvas.drawPath(myPath, myPaint);
Also, I would recommend putting the vector drawable into a separate view so it's not redrawn everytime you need to animate the triangle (assuming this is going to be an animated guage dial).

Fix text mirroring on rear view of Object3D

My Class extends Renderer(org.rajawali3d.renderer.Renderer)
Following function in the class is called from activity :
public void makeText(float[] openGLPpoint, int color, String text,float depth)
{
double adjust_angle_o = 0;
Material mSphereMaterial = new Material();
Bitmap bitmap= setTextInImage(text);
Texture t = new Texture("text",bitmap);
mSphereMaterial.addTexture(t);
Object3D object3D = new Plane(0.3f,0.1f,100,100);
object3D.setMaterial(mSphereMaterial);
object3D.setDoubleSided(true);
object3D.setColor(color);
if(!depth >= 1)
{
adjust_angle_o = 0.2;
}
object3D.setPosition(openGLPpoint[0],
openGLPpoint[1],
openGLPpoint[2]-adjust_angle_o);
getCurrentScene().addChild(object3D);
}
public Bitmap setTextInImage(String text){
Bitmap src =
BitmapFactory.decodeResource(mContext.getResources(),R.drawable.label_bg_sm);
Bitmap dest =
Bitmap.createBitmap(src.getWidth(),
src.getHeight(),
Bitmap.Config.ARGB_8888);
Canvas cs = new Canvas(dest);
Paint tPaint = new Paint();
tPaint.setTextSize(50 * mContext.getResources().getDisplayMetrics().density);
tPaint.setColor(Color.GREEN);
tPaint.setStyle(Paint.Style.FILL_AND_STROKE);
tPaint.setFakeBoldText(true);
cs.drawBitmap(src, 0f, 0f, null);
float height = tPaint.measureText("yY");
float width_o = tPaint.measureText(text+" m");
float x_coord = (src.getWidth() - width_o)/2;
// 15f is to put space between top edge and the text
cs.drawText(text+" m", x_coord, height+15f, tPaint);
return dest;
}
I need the factor object3D.setDoubleSided(true); , which is working fine .
The text is correct when seen from front view but when the object is seen from the rear side , the text naturally mirrors .
Following is a snap :
Mirrored text observed from rear view
How can the text be corrected , as it should be readable from both front and rear views ? Any articles to fix text-mirroring ?
I figured it out , object3D.setDoubleSided(false) created a duplicate object and rotated it 180 at y axis . It worked fine now .
Edit : The rotated side is showing dark then the original . Increasing alpha isnt working. Can anyone guide futher ?

Display multiple lines of text in Oxyplot TextAnnotation

I'm working on an Android app utilizing xamarin and the oxyplot library. I ran into a problem where I cannot add multiple lines of text in TextAnnotation.
I tried the following options:
var sb = new StringBuilder();
sb.Append("Line1");
sb.AppendLine(); // which is equal to Append(Environment.NewLine);
sb.Append("\n");
sb.Append("\r\n");
sb.Append(System.Environment.NewLine);
sb.Append("Line2");
and added it like so to the TextAnnotation text object:
var textAnnotation1 = new TextAnnotation();
textAnnotation1.Text = sb.ToString();
textAnnotation1.Background = OxyColors.White;
textAnnotation1.TextColor = OxyColors.Black;
textAnnotation1.FontSize = 18;
textAnnotation1.TextPosition = new DataPoint(4,_vericalLineYaxis);
plotModel.Annotations.Add(textAnnotation1);
but all to avail.
My goal is to have the text appear like so:
Line 1
Line 2
Currently it's appearing as:
Line 1 Line2
Any help would be much appreciated.
Multi-line Annotations is not currently supported on the Android platform.
OxyPlot is invoking Android.Canvas.DrawText and that function does not support text wrapping, it is a fairly low-level primitive drawing routine.
Google's Doc: public void drawText (String text, float x, float y, Paint paint)
If you feel like mod'ing the source, this could be done by using a static layout vs. the current canvas.DrawText.
Something like this would get you started (but is untested):
public void DrawText (string text, float x, float y, Paint paint)
{
TextPaint textPaint = new TextPaint();
StaticLayout textLayout = new StaticLayout(text, textPaint, canvas.getWidth(), Alignment.ALIGN_NORMAL, 1.0f, 0.0f, False);
canvas.Save();
canvas.Translate(x, y);
textLayout.Draw(canvas);
canvas.Restore();
}
FYI: Oxyplot's SVG renderer manually handles multi-line text by string splitting on "\r\n" and rendering a separate element for each line so the same thing could be done for the Android instead of using a StaticLayout (slower performance wise, but easy to mod/test):
var lines = Regex.Split(text, "\r\n");
if (valign == VerticalAlignment.Bottom)
{
for (var i = lines.Length - 1; i >= 0; i--)
{
var line = lines[i];
var size = this.MeasureText(line, fontFamily, fontSize, fontWeight);
this.w.WriteText(p, line, c, fontFamily, fontSize, fontWeight, rotate, halign, valign);
p += new ScreenVector(Math.Sin(rotate / 180.0 * Math.PI) * size.Height, Math.Cos(rotate / 180.0 * Math.PI) * size.Height);
}
}

How to make line with rounded (smooth) corners with AndroidPlot

I have a small problem with ploting my graph. On a picture below is what I have already done.
The graph should represent the actual signal strength of available Wi-Fi network(s). It's a simple XYPlot here data are represented with SimpleXYSeries (values are dynamically created).
Here is a little snippet of code (only for example):
plot = (XYPlot) findViewById(R.id.simplexyPlot);
series1 = new SimpleXYSeries(Arrays.asList(series1Numbers),
SimpleXYSeries.ArrayFormat.Y_VALS_ONLY, "Link 1");
f1 = new LineAndPointFormatter(color.getColor(), null,
Color.argb(60, color.getRed(), color.getGreen(), color.getBlue()), null);
plot.addSeries(series1, f1);
The example in the picture is a dynamic simulation of dB changes. Everything works, I guess, correctly, but what I want to achieve is to have line with "rounded" corners (see the picture to see what I mean).
I already tried to customize LineFormatter:
f1.getFillPaint().setStrokeJoin(Join.ROUND);
f1.getFillPaint().setStrokeWidth(8);
But this didn't work as expected.
Note: The Wifi Analyzer application has a similar graph and its graph has the rounded corners I want. It looks like this:
You can use Path.cubicTo() method. It draws a line using cubic spline algorithm which results in the smoothing effect you want.
Checkout the answer to a similar question here, where a guy is talking about cubic splines. There is a short algorithm showing how to calculate input parameters for Path.cubicTo() method. You can play with divider values to achieve required smoothness. For example, in the picture below I divided by 5 instead of 3. Hope this helps.
I have spent some time and implemented a SplineLineAndPointFormatter class, which does the stuff you need in androidplot library. It uses same technics. Here is how androidplot example applications looks like. You just need to use it instead of LineAndPointFormatter.
Here is code example and the class I wrote.
f1 = new SplineLineAndPointFormatter(color.getColor(), null,
Color.argb(60, color.getRed(), color.getGreen(), color.getBlue()), null);
plot.addSeries(series1, f1);
Here is the class doing the magic. It is based on version 0.6.1 of androidplot library.
package com.androidplot.xy;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import com.androidplot.ui.SeriesRenderer;
import com.androidplot.util.ValPixConverter;
public class SplineLineAndPointFormatter extends LineAndPointFormatter {
public SplineLineAndPointFormatter() { }
public SplineLineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor) {
super(lineColor, vertexColor, fillColor, null);
}
public SplineLineAndPointFormatter(Integer lineColor, Integer vertexColor, Integer fillColor, FillDirection fillDir) {
super(lineColor, vertexColor, fillColor, null, fillDir);
}
#Override
public Class<? extends SeriesRenderer> getRendererClass() {
return SplineLineAndPointRenderer.class;
}
#Override
public SeriesRenderer getRendererInstance(XYPlot plot) {
return new SplineLineAndPointRenderer(plot);
}
public static class SplineLineAndPointRenderer extends LineAndPointRenderer<BezierLineAndPointFormatter> {
static class Point {
public float x, y, dx, dy;
public Point(PointF pf) { x = pf.x; y = pf.y; }
}
private Point prev, point, next;
private int pointsCounter;
public SplineLineAndPointRenderer(XYPlot plot) {
super(plot);
}
#Override
protected void appendToPath(Path path, final PointF thisPoint, PointF lastPoint) {
pointsCounter--;
if (point == null) {
point = new Point(thisPoint);
point.dx = ((point.x - prev.x) / 5);
point.dy = ((point.y - prev.y) / 5);
return;
} else if (next == null) {
next = new Point(thisPoint);
} else {
prev = point;
point = next;
next = new Point(thisPoint);
}
point.dx = ((next.x - prev.x) / 5);
point.dy = ((next.y - prev.y) / 5);
path.cubicTo(prev.x + prev.dx, prev.y + prev.dy, point.x - point.dx, point.y - point.dy, point.x, point.y);
if (pointsCounter == 1) { // last point
next.dx = ((next.x - point.x) / 5);
next.dy = ((next.y - point.y) / 5);
path.cubicTo(point.x + point.dx, point.y + point.dy, next.x - next.dx, next.y - next.dy, next.x, next.y);
}
}
#Override
protected void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAndPointFormatter formatter) {
Number y = series.getY(0);
Number x = series.getX(0);
if (x == null || y == null) throw new IllegalArgumentException("no null values in xyseries permitted");
XYPlot p = getPlot();
PointF thisPoint = ValPixConverter.valToPix(x, y, plotArea,
p.getCalculatedMinX(), p.getCalculatedMaxX(), p.getCalculatedMinY(), p.getCalculatedMaxY());
prev = new Point(thisPoint);
point = next = null;
pointsCounter = series.size();
super.drawSeries(canvas, plotArea, series, formatter);
}
}
}
1- I guess that you only use a few points to draw graphs of signals. All graph/chart applications try to connect points with direct lines and then your chart will be shown. So if you only use three points, your graph will looks like a triangle! If you want your graph to be curved, you have to add more points. Then it comes out like a curve.
2- Or you can find any library that can draw sin graph, for example GraphView Library. Then try to draw this function:
So it looks like to this:
Then translate it to (a,0), so result seems like what you want.
3- And another way, you can use built in Math.sin in Java:
Chose for example 1000 point in range a to b and compute value of above function for each point and finally create a path and show them in a canvas.
You can use quadTo (float x1, float y1, float x2, float y2) that simplify drawing quad curves for you. The documentation says:
Add a quadratic bezier from the last point, approaching control point
(x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
this contour, the first point is automatically set to (0,0).
Parameters
x1 The x-coordinate of the control point on a quadratic curve
y1 The y-coordinate of the control point on a quadratic curve
x2 The x-coordinate of the end point on a quadratic curve
y2 The y-coordinate of the end point on a quadratic curve
Finally, I add a simple class that extends View and can draw a curve that looks like what you want:
public class SinWave extends View {
private float first_X = 50;
private float first_Y = 230;
private float end_X = 100;
private float end_Y = 230;
private float Max = 50;
public SinWave(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint() {
{
setStyle(Paint.Style.STROKE);
setStrokeCap(Paint.Cap.ROUND);
setStrokeWidth(0.7f);
setAntiAlias(true);
setColor(0xFFFF00FF);
}
};
final Path path = new Path();
path.moveTo(first_X, first_Y);
path.quadTo((first_X + end_X)/2, Max, end_X, end_Y);
canvas.drawPath(path, paint);
}
}
The result must look like this:
You can add more methods to the class and change it to increase performance!
There's always been a smooth line renderer in Androidplot: BezierLineAndPointRenderer, which like the implementations above uses Android's built in Bezier drawing routines cubicTo(...) & quadTo(...). The problem is that using Beziers to draw smooth lines in this way creates a false line that overshoots the actual control points by varying amounts, which you can see happening if you look closely at the image above.
The solution is to use the Catmull-Rom spline interpolation, which is now finally supported by Androidplot. Details here: http://androidplot.com/smooth-curves-and-androidplot/
Just use ChartFactory.getCubeLineChartView instead of ChartFactory.getLineChartView using achart engine
In some simple cases, this could help:
mPaint.pathEffect = CornerPathEffect(radius)
even in combination with
path.lineTo(x,y)
try this:
symbol = new Path();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStrokeWidth(2);
paint.setColor(-7829368);
paint.setStrokeJoin(Paint.Join.ROUND); // set the join to round you want
paint.setStrokeCap(Paint.Cap.ROUND); // set the paint cap to round too
paint.setPathEffect(new CornerPathEffect(10) );
paint.setStyle(Paint.Style.STROKE);
symbol.moveTo(50.0F, 230.0F);
symbol.lineTo(75.0F, 100.0F);
symbol.lineTo(100.0F, 230.0F);
most of the info found here

Custom dynamic graph in Android

[UPDATE]
To conclude this question, I implemented my graph using the following two methods (see below). drawCurve() receives a Canvas and an array of float. The array is properly filled (timestamps are assumed by the value index in the array) and varies from 0.0 to 1.0. The array is sent to prepareWindowArray() that takes a chunk of the array from position windowStart for windowSize-values, in a circular manner.
The array used by the GraphView and by the data provider (a Bluetooth device) is the same. A Class in the middle ensures that GraphView is not reading data that are being written by the Bluetooth device. Since the GraphView always loop thru the array and redraw it at every iteration, it will update according to the data written by the Bluetooth device, and by forcing the write frequency of the Bluetooth device to the refresh frequency of the Graph, I obtain a smooth animation of my signal.
The GraphView's invalidate() method is called by the Activity, which run a Timer to refresh the graph at every x milliseconds. The frequency at which the graph is refreshed is dynamically set, so that it adapt to the flow of data from the Bluetooth device (which specify the frequency of its signal in the header of its packet).
Find the complete code of my GraphView in the answer I wrote below (in the answer section). If you guys find errors or way to optimize it, please let me know; it would be greatly appreciated!
/**
* Read a buffer array of size greater than "windowSize" and create a window array out of it.
* A curve is then drawn from this array using "windowSize" points, from left
* to right.
* #param canvas is a Canvas object on which the curve will be drawn. Ensure the canvas is the
* later drawn object at its position or you will not see your curve.
* #param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0.
* A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at
* the top of the graph. The range is not tested, so you must ensure to pass proper values, or your
* graph will look terrible.
* 0.0 : draw at the bottom of the graph
* 0.5 : draw in the middle of the graph
* 1.0 : draw at the top of the graph
*/
private void drawCurve(Canvas canvas, float[] data){
// Create a reference value to determine the stepping between each points to be drawn
float incrementX = (mRightSide-mLeftSide)/(float) windowSize;
float incrementY = (mBottomSide - mTopSide);
// Prepare the array for the graph
float[] source = prepareWindowArray(data);
// Prepare the curve Path
curve = new Path();
// Move at the first point.
curve.moveTo(mLeftSide, source[0]*incrementY);
// Draw the remaining points of the curve
for(int i = 1; i < windowSize; i++){
curve.lineTo(mLeftSide + (i*incrementX), source[i] * incrementY);
}
canvas.drawPath(curve, curvePaint);
}
The prepareWindowArray() method that implement the circular behavior of the array:
/**
* Extract a window array from the data array, and reposition the windowStart
* index for next iteration
* #param data the array of data from which we get the window
* #return an array of float that represent the window
*/
private float[] prepareWindowArray(float[] data){
// Prepare the source array for the graph.
float[] source = new float[windowSize];
// Copy the window from the data array into the source array
for(int i = 0; i < windowSize; i++){
if(windowStart+i < data.length) // If the windows holds within the data array
source[i] = data[windowStart + i]; // Simply copy the value in the source array
else{ // If the window goes beyond the data array
source[i] = data[(windowStart + 1)%data.length]; // Loop at the beginning of the data array and copy from there
}
}
// Reposition the buffer index
windowStart = windowStart + windowSize;
// If the index is beyond the end of the array
if(windowStart >= data.length){
windowStart = windowStart % data.length;
}
return source;
}
[/UPDATE]
I'm making an app that read data from a Bluetooth device at a fixed rate. Everytime that I have new data, I want them to be plotted on the graph to the right, and to translate the remainder of the graph to the left in realtime. Basically, like an oscilloscope would do.
So I made a custom View, with xy axis, a title and units. To do this, I simply draw those things on the View canvas. Now I want to draw the curve. I manage to draw a static curve from an already filled array using this method:
public void drawCurve(Canvas canvas){
int left = getPaddingLeft();
int bottom = getHeight()-getPaddingTop();
int middle = (bottom-10)/2 - 10;
curvePaint = new Paint();
curvePaint.setColor(Color.GREEN);
curvePaint.setStrokeWidth(1f);
curvePaint.setDither(true);
curvePaint.setStyle(Paint.Style.STROKE);
curvePaint.setStrokeJoin(Paint.Join.ROUND);
curvePaint.setStrokeCap(Paint.Cap.ROUND);
curvePaint.setPathEffect(new CornerPathEffect(10) );
curvePaint.setAntiAlias(true);
mCurve = new Path();
mCurve.moveTo(left, middle);
for(int i = 0; i < mData[0].length; i++)
mCurve.lineTo(left + ((float)mData[0][i] * 5), middle-((float)mData[1][i] * 20));
canvas.drawPath(mCurve, curvePaint);
}
It gives me something like this.
There are still things to fix on my graph (the sub-axis are not properly scaling), but these are details I can fix later.
Now I want to change this static graph (that receives a non-dynamic matrice of values) with something dynamic that would redraw the curve every 40ms, pushing the old data to the left and plotting the new data to the right, so I could visualise in real time the information provided by the Bluetooth device.
I know there are some graphing package that exists already, but I'm kinda noob with these things and I'd like to pratice by implementing this graph myself. Also, most of my GraphView class is done, except for the curve part.
Second question, I'm wondering how I should send the new values to the graph. Should I use something like a FIFO stack, or can I achieve what I want with a simple matrice of doubles?
On a side note, the 4 fields at the bottom are already dynamically updated. Well, they are kind of faking the "dynamic", they loop thru the same double matrice again and again, they don't actually take fresh values.
Thanks for your time! If something's unclear about my question, let me know and I'll update it with more details.
As mentioned in my question, here's the class that I designed to solve my problems.
/**
* A View implementation that displays a scatter graph with
* automatic unit scaling.
*
* Call the <i>setupGraph()</i> method to modify the graph's
* properties.
* #author Antoine Grondin
*
*/
public class GraphView extends View {
//////////////////////////////////////////////////////////////////
// Configuration
//////////////////////////////////////////////////////////////////
// Set to true to impose the graph properties
private static final boolean TEST = false;
// Scale configuration
private float minX = 0; // When TEST is true, these values are used to
private float maxX = 50; // Draw the graph
private float minY = 0;
private float maxY = 100;
private String titleText = "A Graph...";
private String xUnitText = "s";
private String yUnitText = "Volts";
// Debugging variables
private boolean D = true;
private String TAG = "GraphView";
//////////////////////////////////////////////////////////////////
// Member fields
//////////////////////////////////////////////////////////////////
// Represent the borders of the View
private int mTopSide = 0;
private int mLeftSide = 0;
private int mRightSide = 0;
private int mBottomSide = 0;
private int mMiddleX = 0;
// Size of a DensityIndependentPixel
private float mDips = 0;
// Hold the position of the axis in regard to the range of values
private int positionOfX = 0;
private int positionOfY = 0;
// Index for the graph array window, and size of the window
private int windowStart = 0;
private int windowSize = 128;
private float[] dataSource;
// Painting tools
private Paint xAxisPaint;
private Paint yAxisPaint;
private Paint tickPaint;
private Paint curvePaint;
private Paint backgroundPaint;
private TextPaint unitTextPaint;
private TextPaint titleTextPaint;
// Object to be drawn
private Path curve;
private Bitmap background;
///////////////////////////////////////////////////////////////////////////////
// Constructors
///////////////////////////////////////////////////////////////////////////////
public GraphView(Context context) {
super(context);
init();
}
public GraphView(Context context, AttributeSet attrs){
super(context, attrs);
init();
}
public GraphView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
init();
}
///////////////////////////////////////////////////////////////////////////////
// Configuration methods
///////////////////////////////////////////////////////////////////////////////
public void setupGraph(String title, String nameOfX, float min_X, float max_X, String nameOfY, float min_Y, float max_Y){
if(!TEST){
titleText = title;
xUnitText = nameOfX;
yUnitText = nameOfY;
minX = min_X;
maxX = max_X;
minY = min_Y;
maxY = max_Y;
}
}
/**
* Set the array this GraphView is to work with.
* #param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0.
* A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at
* the top of the graph. The range is not tested, so you must ensure to pass proper values, or your
* graph will look terrible.
* 0.0 : draw at the bottom of the graph
* 0.5 : draw in the middle of the graph
* 1.0 : draw at the top of the graph
*/
public void setDataSource(float[] data){
this.dataSource = data;
}
///////////////////////////////////////////////////////////////////////////////
// Initialization methods
///////////////////////////////////////////////////////////////////////////////
private void init(){
initDrawingTools();
}
private void initConstants(){
mDips = getResources().getDisplayMetrics().density;
mTopSide = (int) (getTop() + 10*mDips);
mLeftSide = (int) (getLeft() + 10*mDips);
mRightSide = (int) (getMeasuredWidth() - 10*mDips);
mBottomSide = (int) (getMeasuredHeight() - 10*mDips);
mMiddleX = (mRightSide - mLeftSide)/2 + mLeftSide;
}
private void initWindowSetting() throws IllegalArgumentException {
// Don't do anything if the given values make no sense
if(maxX < minX || maxY < minY ||
maxX == minX || maxY == minY){
throw new IllegalArgumentException("Max and min values make no sense");
}
// Transform the values in scanable items
float[][] maxAndMin = new float[][]{
{minX, maxX},
{minY, maxY}};
int[] positions = new int[]{positionOfY, positionOfX};
// Place the X and Y axis in regard to the given max and min
for(int i = 0; i<2; i++){
if(maxAndMin[i][0] < 0f){
if(maxAndMin[i][1] < 0f){
positions[i] = (int) maxAndMin[i][0];
} else{
positions[i] = 0;
}
} else if (maxAndMin[i][0] > 0f){
positions[i] = (int) maxAndMin[i][0];
} else {
positions[i] = 0;
}
}
// Put the values back in their right place
minX = maxAndMin[0][0];
maxX = maxAndMin[0][1];
minY = maxAndMin[1][0];
maxY = maxAndMin[1][1];
positionOfY = mLeftSide + (int) (((positions[0] - minX)/(maxX-minX))*(mRightSide - mLeftSide));
positionOfX = mBottomSide - (int) (((positions[1] - minY)/(maxY-minY))*(mBottomSide - mTopSide));
}
private void initDrawingTools(){
xAxisPaint = new Paint();
xAxisPaint.setColor(0xff888888);
xAxisPaint.setStrokeWidth(1f*mDips);
xAxisPaint.setAlpha(0xff);
xAxisPaint.setAntiAlias(true);
yAxisPaint = xAxisPaint;
tickPaint = xAxisPaint;
tickPaint.setColor(0xffaaaaaa);
curvePaint = new Paint();
curvePaint.setColor(0xff00ff00);
curvePaint.setStrokeWidth(1f*mDips);
curvePaint.setDither(true);
curvePaint.setStyle(Paint.Style.STROKE);
curvePaint.setStrokeJoin(Paint.Join.ROUND);
curvePaint.setStrokeCap(Paint.Cap.ROUND);
curvePaint.setPathEffect(new CornerPathEffect(10));
curvePaint.setAntiAlias(true);
backgroundPaint = new Paint();
backgroundPaint.setFilterBitmap(true);
titleTextPaint = new TextPaint();
titleTextPaint.setAntiAlias(true);
titleTextPaint.setColor(0xffffffff);
titleTextPaint.setTextAlign(Align.CENTER);
titleTextPaint.setTextSize(20f*mDips);
titleTextPaint.setTypeface(Typeface.MONOSPACE);
unitTextPaint = new TextPaint();
unitTextPaint.setAntiAlias(true);
unitTextPaint.setColor(0xff888888);
unitTextPaint.setTextAlign(Align.CENTER);
unitTextPaint.setTextSize(20f*mDips);
unitTextPaint.setTypeface(Typeface.MONOSPACE);
}
///////////////////////////////////////////////////////////////////////////////
// Overridden methods
///////////////////////////////////////////////////////////////////////////////
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
regenerateBackground();
}
public void onDraw(Canvas canvas){
drawBackground(canvas);
if(dataSource != null)
drawCurve(canvas, dataSource);
}
///////////////////////////////////////////////////////////////////////////////
// Drawing methods
///////////////////////////////////////////////////////////////////////////////
private void drawX(Canvas canvas){
canvas.drawLine(mLeftSide, positionOfX, mRightSide, positionOfX, xAxisPaint);
canvas.drawText(xUnitText, mRightSide - unitTextPaint.measureText(xUnitText)/2, positionOfX - unitTextPaint.getTextSize()/2, unitTextPaint);
}
private void drawY(Canvas canvas){
canvas.drawLine(positionOfY, mTopSide, positionOfY, mBottomSide, yAxisPaint);
canvas.drawText(yUnitText, positionOfY + unitTextPaint.measureText(yUnitText)/2 + 4*mDips, mTopSide + (int) (unitTextPaint.getTextSize()/2), unitTextPaint);
}
private void drawTick(Canvas canvas){
// No tick at this time
// TODO decide how I want to put those ticks, if I want them
}
private void drawTitle(Canvas canvas){
canvas.drawText(titleText, mMiddleX, mTopSide + (int) (titleTextPaint.getTextSize()/2), titleTextPaint);
}
/**
* Read a buffer array of size greater than "windowSize" and create a window array out of it.
* A curve is then drawn from this array using "windowSize" points, from left
* to right.
* #param canvas is a Canvas object on which the curve will be drawn. Ensure the canvas is the
* later drawn object at its position or you will not see your curve.
* #param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0.
* A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at
* the top of the graph. The range is not tested, so you must ensure to pass proper values, or your
* graph will look terrible.
* 0.0 : draw at the bottom of the graph
* 0.5 : draw in the middle of the graph
* 1.0 : draw at the top of the graph
*/
private void drawCurve(Canvas canvas, float[] data){
// Create a reference value to determine the stepping between each points to be drawn
float incrementX = (mRightSide-mLeftSide)/(float) windowSize;
float incrementY = mBottomSide - mTopSide;
// Prepare the array for the graph
float[] source = prepareWindowArray(data);
// Prepare the curve Path
curve = new Path();
// Move at the first point.
curve.moveTo(mLeftSide, source[0]*incrementY);
// Draw the remaining points of the curve
for(int i = 1; i < windowSize; i++){
curve.lineTo(mLeftSide + (i*incrementX), source[i] * incrementY);
}
canvas.drawPath(curve, curvePaint);
}
///////////////////////////////////////////////////////////////////////////////
// Intimate methods
///////////////////////////////////////////////////////////////////////////////
/**
* When asked to draw the background, this method will verify if a bitmap of the
* background is available. If not, it will regenerate one. Then, it will draw
* the background using this bitmap. The use of a bitmap to draw the background
* is to avoid unnecessary processing for static parts of the view.
*/
private void drawBackground(Canvas canvas){
if(background == null){
regenerateBackground();
}
canvas.drawBitmap(background, 0, 0, backgroundPaint);
}
/**
* Call this method to force the <i>GraphView</i> to redraw the cache of it's background,
* using new properties if you changed them with <i>setupGraph()</i>.
*/
public void regenerateBackground(){
initConstants();
try{
initWindowSetting();
} catch (IllegalArgumentException e){
Log.e(TAG, "Could not initalize windows.", e);
return;
}
if(background != null){
background.recycle();
}
background = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas backgroundCanvas = new Canvas(background);
drawX(backgroundCanvas);
drawY(backgroundCanvas);
drawTick(backgroundCanvas);
drawTitle(backgroundCanvas);
}
/**
* Extract a window array from the data array, and reposition the windowStart
* index for next iteration
* #param data the array of data from which we get the window
* #return an array of float that represent the window
*/
private float[] prepareWindowArray(float[] data){
// Prepare the source array for the graph.
float[] source = new float[windowSize];
// Copy the window from the data array into the source array
for(int i = 0; i < windowSize; i++){
if(windowStart+i < data.length) // If the windows holds within the data array
source[i] = data[windowStart + i]; // Simply copy the value in the source array
else{ // If the window goes beyond the data array
source[i] = data[(windowStart + 1)%data.length]; // Loop at the beginning of the data array and copy from there
}
}
// Reposition the buffer index
windowStart = windowStart + windowSize;
// If the index is beyond the end of the array
if(windowStart >= data.length){
windowStart = windowStart % data.length;
}
return source;
}
}
Well I would start by just trying to redraw it all with the code you have and real dynalic data. Only if that is not quick enough do you need to try anything fancy like scrolling...
If you need fancy I would try somthing like this.
I would draw the dynamic part of the graph into a secondary Bitmap that you keep between frames rather than directly to the canves. I would have the background none dynamic part of the graph in another bitmap that only gets drawen on rescale etc.
In this secondary dynamic bitmap when ploting new data you first need to clear the old data you are replacing you do this by drawing the apropriate slice of the static background bitmap over the top of the stale data, thus clearing it and geting the background nice and fresh again. You then just need to draw your new bit of dynamic data. The trick is that You draw into this second bitmap left to right then just wrap back to the left at the end and start over.
To get from the soncodary bitmap to your cancas draw the bitmap to the canvas in two parts. The older data to the right of what you just added needs to be drawn onto the left part of your final canvas and the new data needs to be drawn imediatly to the right of it.
For sending the data a circular buffer would be the normal thing for this sort of data where once it's off the graph you don't care about it.

Categories

Resources