Android Custom Dynamic Chart

[UPDATE] To complete this question, I completed my schedule using the following two methods (see below). drawCurve() gets a drawCurve() and an array of float . The array is correctly filled (timestamps are accepted by the index of values ​​in the array) and range from 0.0 to 1.0. The array is sent to prepareWindowArray() , which takes a piece of the array from the windowStart position for windowSize values ​​in a circular manner.

The array used by GraphView and the data provider (Bluetooth device) is the same. The middle class ensures that GraphView will not read data that is being written by the Bluetooth device. Since GraphView always cycles through the array and redraws it at each iteration, it is updated according to the data recorded by the Bluetooth device, and forcing the recording speed of the Bluetooth device to update the graph frequency, I get a smooth animation of my signal.

The GraphView invalidate() method is called by Activity , which runs Timer to update the graph every x milliseconds. The refresh rate is updated dynamically, so it adapts to the data stream from the Bluetooth device (which determine the frequency of its signal in the header of its packet).

Find the full code of my GraphView in the answer I wrote below (in the answer section). If you guys find errors or a way to optimize them, let me know; that would be 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 implements 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 am making an application that reads data from a Bluetooth device with a fixed speed. Every time I have new data, I want them to be plotted on the graph on the right, and to translate the rest of the graph to the left in real time. In principle, like an oscilloscope.

So, I made my own view, with the xy axis, header and units. For this, I just draw these things on the canvas "View". Now I want to draw a curve. I managed to create 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.

My custom graphview

There are still things on my chart (the tray doesn’t scale correctly), but these are details that I can fix later.

Now I want to change this static graph (which gets a non-dynamic matrix of values) with something dynamic that will redraw the curve every 40 ms, pushing the old data to the left and displaying the new data to the right, so I could visualize in real time the information provided by the Bluetooth device .

I know that there is already some kind of GUI package, but I do not do anything with it, and I would prefer to use this graph myself. In addition, most of my GraphView class runs, except for the part of the curve.

The second question, I wonder how I should send new values ​​to the chart. Should I use something like a FIFO stack, or can I achieve what I want with a simple matrix of paired?

On the other hand, the 4 fields below are already dynamically updated. Well, they kind of pretend to be β€œdynamic”, they go over and over again through the same double matrix, they actually don't take fresh values.

Thank you for your time! If something is unclear about my question, let me know and I will update it in more detail.

+6
source share
2 answers

As mentioned in my question, here is a class that I developed 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 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; } } 
+7
source

Well, I would start by simply redrawing everything with the code you have and real dynastic data. Only if it is not fast enough, you need to try anything like scrolling ...

If you need a fantasy, I would try something like this.

I would draw the dynamic part of the graph in the secondary bitmap that you hold between frames, and not directly in the banks. I would not use any dynamic part of the graph in another bitmap that only gets stretch when scaling, etc.

In this secondary dynamic bitmap, when printing new data, you first need to clear the old data that you are replacing by doing this, by drawing a suitable fragment of the static background image at the top of the obsolete data, thereby clearing it and getting the background nice and fresh again. Then you just need to draw a new bit of dynamic data. The trick is that you draw this second bitmap from left to right, and then just wrap it to the left at the end and start.

To get a coder from a bitmap into your canse, draw a bitmap on the canvas in two parts. The previous data to the right of what you just added, you need to draw on the left side of your last canvas, and the new data should be facing to the right of it.

To send data, a circular buffer will be a common thing for this kind of data, where, when it will be on the chart, you are not interested.

+4
source

Source: https://habr.com/ru/post/898908/


All Articles