Real-time drawing graph using Android custom view

I want to make a simple line plot with real-time graphics in my application. I know that there are many different libraries, but they are too large or do not have the correct functions or licenses.

My idea is to create a custom view and just extend the View class. Using OpenGL in this case would be like shooting a duck with a canon. I already have a view that draws static data - first of all, I put all the data in the float array of my Plot object, and then using the loop to draw everything in the onDraw() method of the PlotView class.

I also have a stream that will provide new data to my plot. But now the problem is how to draw it while new data is added. The first thought was simply to add a new point and attract. Add again and again. But I'm not sure what will happen at 100 or 1000 points. I add a new paragraph, please take a look at the invalidity, but still some points are not drawn. In this case, even using some queue can be difficult, since onDraw() will start from the beginning, so the number of queue elements will simply increase.

What do you recommend to achieve this?

+5
source share
6 answers

Let me try to outline the problem a little more.

  • You have a surface on which you can draw dots and lines, and you know how to make it look the way you want it to look.
  • You have a data source that provides points for drawing and that the data source changes on the fly.
  • You want the surface to accurately reflect the incoming data as close as possible.

The first question is, how about your situation slowly? Do you know where your delays come from? First, make sure you have a problem to solve; secondly, make sure you know where your problem comes from.

Let's say your problem is with the size of the data, as you mean. How to solve this is a difficult question. It depends on the properties of the graphic data - which invariants you can accept and so on. You talked about storing data in float[] , so I'm going to assume that you have a fixed number of data points that change in value. I’m also going to suggest that for 100 or 1000 what you had in mind was “many and many,” because frankly 1000 floats are just not a lot of data.

When you have a really large drawing array, your performance limit will ultimately come from looping through the array. Then your performance improvement will reduce the number of elements that you loop. This is where data properties come into play.

One way to reduce the scope of the redraw operation is to maintain a dirty list that acts like a Queue<Int> . Each time a cell in your array changes, you queue that array index, marking it as dirty. Each time the drawing method goes back, it deletes a fixed number of entries in the dirty list and only updates a piece of your displayed image corresponding to these entries - you will probably have to scale a little and / or smooth or something, because with so many data points you probably got more data than screen pixels. the number of records that you redraw in any given frame update should be limited by the required frame rate - you can do this adaptive, based on the metric of how long the previous draw operations were performed and how deep this dirty list is, to maintain a good balance between frame rate and time of visible data.

This is especially convenient if you are trying to simultaneously draw all the data on the screen. If you are viewing only a piece of data (for example, in a scrollable form), and there is some kind of correspondence between the positions of the array and the window size, then you can “finish” the data - in each callback, only the subset of data that is actually on the screen is taken into account. If you also have a “scale,” you can mix the two methods — this can get complicated.

If your data is terminated in such a way that the value in each element of the array determines whether the data point is on or off, consider using a sorted list of pairs, where the sort key is the value. This will allow you to perform the window optimization described above in this situation. If windowing occurs in both dimensions, you will most likely only need to perform this or that optimization, but there are two range query structures that can also give you this.

Let's say my assumption about a fixed amount of data was wrong; instead, you add data to the end of the list, but existing data points do not change. In this case, you are probably better off with a related structure like Queue that discards old data points rather than an array, because increasing your array will tend to stutter the application unnecessarily.

In this case, your optimization consists of preliminary drawing into the buffer following your turn - since new elements enter the queue, shift the entire buffer to the left and draw only the area containing the new elements.

If this is / rate / data input, that is the problem, then use the structure in the queue and skip the elements - either collapse them as they are added to the queue, save / draw each element n th, or something similar.

If instead, the rendering process, which takes all your time, considers rendering in the background stream and saving the rendered image. This will allow you to take as much time as you want to redraw - the frame rate inside the diagram itself will drop, but not your overall application reaction.

+1
source

That should do the trick.

 import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.os.Bundle; import android.support.v4.view.ViewCompat; import android.support.v7.app.AppCompatActivity; import android.util.AttributeSet; import android.view.View; import java.io.Serializable; public class MainActivity extends AppCompatActivity { private static final String STATE_PLOT = "statePlot"; private MockDataGenerator mMockDataGenerator; private Plot mPlot; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(savedInstanceState == null){ mPlot = new Plot(100, -1.5f, 1.5f); }else{ mPlot = (Plot) savedInstanceState.getSerializable(STATE_PLOT); } PlotView plotView = new PlotView(this); plotView.setPlot(mPlot); setContentView(plotView); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putSerializable(STATE_PLOT, mPlot); } @Override protected void onResume() { super.onResume(); mMockDataGenerator = new MockDataGenerator(mPlot); mMockDataGenerator.start(); } @Override protected void onPause() { super.onPause(); mMockDataGenerator.quit(); } public static class MockDataGenerator extends Thread { private final Plot mPlot; public MockDataGenerator(Plot plot) { super(MockDataGenerator.class.getSimpleName()); mPlot = plot; } @Override public void run() { try{ float val = 0; while(!isInterrupted()){ mPlot.add((float) Math.sin(val += 0.16f)); Thread.sleep(1000 / 30); } } catch(InterruptedException e){ // } } public void quit() { try{ interrupt(); join(); } catch(InterruptedException e){ // } } } public static class PlotView extends View implements Plot.OnPlotDataChanged { private Paint mLinePaint; private Plot mPlot; public PlotView(Context context) { this(context, null); } public PlotView(Context context, AttributeSet attrs) { super(context, attrs); mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mLinePaint.setStyle(Paint.Style.STROKE); mLinePaint.setStrokeJoin(Paint.Join.ROUND); mLinePaint.setStrokeCap(Paint.Cap.ROUND); mLinePaint.setStrokeWidth(context.getResources() .getDisplayMetrics().density * 2.0f); mLinePaint.setColor(0xFF568607); setBackgroundColor(0xFF8DBF45); } public void setPlot(Plot plot) { if(mPlot != null){ mPlot.setOnPlotDataChanged(null); } mPlot = plot; if(plot != null){ plot.setOnPlotDataChanged(this); } onPlotDataChanged(); } public Plot getPlot() { return mPlot; } public Paint getLinePaint() { return mLinePaint; } @Override public void onPlotDataChanged() { ViewCompat.postInvalidateOnAnimation(this); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); final Plot plot = mPlot; if(plot == null){ return; } final int height = getHeight(); final float[] data = plot.getData(); final float unitHeight = height / plot.getRange(); final float midHeight = height / 2.0f; final float unitWidth = (float) getWidth() / data.length; float lastX = -unitWidth, lastY = 0, currentX, currentY; for(int i = 0; i < data.length; i++){ currentX = lastX + unitWidth; currentY = unitHeight * data[i] + midHeight; canvas.drawLine(lastX, lastY, currentX, currentY, mLinePaint); lastX = currentX; lastY = currentY; } } } public static class Plot implements Serializable { private final float[] mData; private final float mMin; private final float mMax; private transient OnPlotDataChanged mOnPlotDataChanged; public Plot(int size, float min, float max) { mData = new float[size]; mMin = min; mMax = max; } public void setOnPlotDataChanged(OnPlotDataChanged onPlotDataChanged) { mOnPlotDataChanged = onPlotDataChanged; } public void add(float value) { System.arraycopy(mData, 1, mData, 0, mData.length - 1); mData[mData.length - 1] = value; if(mOnPlotDataChanged != null){ mOnPlotDataChanged.onPlotDataChanged(); } } public float[] getData() { return mData; } public float getMin() { return mMin; } public float getMax() { return mMax; } public float getRange() { return (mMax - mMin); } public interface OnPlotDataChanged { void onPlotDataChanged(); } } } 
+2
source

What I did in a similar situation was to create a custom class, let it have “MyView”, which extends the view and adds it to my XML layout.

 public class MyView extends View { ... } 

In the layout:

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <com.yadayada.MyView android:id="@+id/paintme" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout> 

MyView uses the override method "onDraw (Canvas canv)". onDraw gets a canvas to draw on. In onDraw, get the Paint new Paint () object and customize it as you like. Then you can use the entire canvas drawing function like drawLine, drawPath, drawBitmap, drawText, etc.

In terms of performance, I suggest you batch modify your baseline data and then invalidate the view. I think you should live with a full re-draw. But if a person watches him, updating more than every second or so is probably not profitable. Canvas drawing methods are incredibly fast.

0
source

Now I offer you the GraphView Library . This is open source, don’t worry about the license, and it’s not that big (<64 KB). You can clear the necessary files if you want.

You can find a custom pattern for real time charts.

From official samples:

 public class RealtimeUpdates extends Fragment { private final Handler mHandler = new Handler(); private Runnable mTimer1; private Runnable mTimer2; private LineGraphSeries<DataPoint> mSeries1; private LineGraphSeries<DataPoint> mSeries2; private double graph2LastXValue = 5d; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main2, container, false); GraphView graph = (GraphView) rootView.findViewById(R.id.graph); mSeries1 = new LineGraphSeries<DataPoint>(generateData()); graph.addSeries(mSeries1); GraphView graph2 = (GraphView) rootView.findViewById(R.id.graph2); mSeries2 = new LineGraphSeries<DataPoint>(); graph2.addSeries(mSeries2); graph2.getViewport().setXAxisBoundsManual(true); graph2.getViewport().setMinX(0); graph2.getViewport().setMaxX(40); return rootView; } @Override public void onAttach(Activity activity) { super.onAttach(activity); ((MainActivity) activity).onSectionAttached( getArguments().getInt(MainActivity.ARG_SECTION_NUMBER)); } @Override public void onResume() { super.onResume(); mTimer1 = new Runnable() { @Override public void run() { mSeries1.resetData(generateData()); mHandler.postDelayed(this, 300); } }; mHandler.postDelayed(mTimer1, 300); mTimer2 = new Runnable() { @Override public void run() { graph2LastXValue += 1d; mSeries2.appendData(new DataPoint(graph2LastXValue, getRandom()), true, 40); mHandler.postDelayed(this, 200); } }; mHandler.postDelayed(mTimer2, 1000); } @Override public void onPause() { mHandler.removeCallbacks(mTimer1); mHandler.removeCallbacks(mTimer2); super.onPause(); } private DataPoint[] generateData() { int count = 30; DataPoint[] values = new DataPoint[count]; for (int i=0; i<count; i++) { double x = i; double f = mRand.nextDouble()*0.15+0.3; double y = Math.sin(i*f+2) + mRand.nextDouble()*0.3; DataPoint v = new DataPoint(x, y); values[i] = v; } return values; } double mLastRandom = 2; Random mRand = new Random(); private double getRandom() { return mLastRandom += mRand.nextDouble()*0.5 - 0.25; } } 
0
source

If you already have a view that draws static data, you are close to your goal. The only thing you need to do is:

1) Extract the logic that retrieves the data 2) Extract the logic that draws this data on the screen 3) Inside the onDraw () method, the first call 1) - then the call 2) - then call invalidate () at the end of your onDraw () method - so how it will cause a new draw, and the view will be updated with new data.

0
source

I am not sure what will happen with 100 or 1000 points.

Nothing, you do not need to worry about it. There are many dots that are the plot every time any action is visible on the screen.

The first thought was simply to add a new point and attract. Add one more and again.

This is the way that I feel. You can use a more systematic approach:

  • check if the graph will be displayed on the screen or on the screen.
  • if they are on the screen, just add it to your array, and that should be fine.
  • If there is no data on the screen, perform the appropriate coordinate calculations, taking into account the following: delete the first element from the array, add a new element to the array and redraw.

After that postinvalidate in your opinion.

I add a new point, ask how to invalidate, but still some points are not drawn.

Your glasses probably go off the screen. Check it out.

In this case, even using some queue can be difficult, because onDraw () will start from the beginning, so the number of queue elements will simply increase.

This should not be a problem, since the points on the screen will be limited, so the queue will only contain so many points, since the previous points will be deleted.

Hope this approach helps.

0
source

All Articles