Reputation: 2685
I want to make in my app simple line plot with real time drawing. I know there are a lot of various libraries but they are too big or don't have right features or licence.
My idea is to make custom view and just extend View
class. Using OpenGL
in this case would be like shooting to a duck with a canon. I already have view that is drawing static data - that is first I am putting all data in float
array of my Plot
object and then using loop draw everything in onDraw()
method of PlotView
class.
I also have a thread that will provide new data to my plot. But the problem now is how to draw it while new data are added. The first thought was to simply add new point and draw. Add another and again. But I am not sure what will happen at 100 or 1000 points. I am adding new point, ask view to invalidate itself but still some points aren't drawn. In this case even using some queue might be difficult because the onDraw()
will start from the beginning again so the number of queue elements will just increase.
What would you recommend to achieve this goal?
Upvotes: 7
Views: 6602
Reputation: 2910
Let me try to sketch out the problem a bit more.
The first question is--what about your situation is slow? Do you know where your delays are coming from? First, be sure you have a problem to solve; second, be sure you know where your problem is coming from.
Let's say your problem is in the size of the data as you imply. How to address this is a complex question. It depends on properties of the data being graphed--what invariants you can assume and so forth. You've talked about storing data in a float[]
, so I'm going to assume that you've got a fixed number of data points which change in value. I'm also going to assume that by '100 or 1000' what you meant was 'lots and lots', because frankly 1000 floats is just not a lot of data.
When you have a really big array to draw, your performance limit is going to eventually come from looping over the array. Your performance enhancement then is going to be reducing how much of the array you're looping over. This is where the properties of the data come into play.
One way to reduce the volume of the redraw operation is to keep a 'dirty list' which acts like a Queue<Int>
. Every time a cell in your array changes, you enqueue that array index, marking it as 'dirty'. Every time your draw method comes back around, dequeue a fixed number of entries in the dirty list and update only the chunk of your rendered image corresponding to those entries--you'll probably have to do some scaling and/or anti-aliasing or something because with that many data points, you've probably got more data than screen pixels. the number of entries you redraw in any given frame update should be bounded by your desired framerate--you can make this adaptive, based on a metric of how long previous draw operations took and how deep the dirty list is getting, to maintain a good balance between frame rate and visible data age.
This is particularly suitable if you're trying to draw all of the data on the screen at once. If you're only viewing a chunk of the data (like in a scrollable view), and there's some kind of correspondence between array positions and window size, then you can 'window' the data--in each draw call, only consider the subset of data that is actually on the screen. If you've also got a 'zoom' thing going on, you can mix the two methods--this can get complicated.
If your data is windowed such that the value in each array element is what determines whether the data point is on or off the screen, consider using a sorted list of pairs where the sort key is the value. This will let you perform the windowing optimization outlined above in this situation. If the windowing is taking place in both dimensions, you most likely will only need to perform one or the other optimization, but there are two dimensional range query structures that can give you this as well.
Let's say my assumption about a fixed data size was wrong; instead you're adding data to the end of the list, but existing data points don't change. In this case you're probably better off with a linked Queue-like structure that drops old data points rather than an array, because growing your array will tend to introduce stutter in the application unnecessarily.
In this case your optimization is to pre-draw into a buffer that follows your queue along--as new elements enter the queue, shift the whole buffer to the left and draw just the region containing the new elements.
If it's the /rate/ of data entry that's the problem, then use a queued structure and skip elements--either collapse them as they're added to the queue, store/draw every n
th element, or something similar.
If instead it's the rendering process that is taking up all of your time, consider rendering on a background thread and storing the rendered image. This will let you take as much time as you want doing the redraw--the framerate within the chart itself will drop but not your overall application responsiveness.
Upvotes: 3
Reputation: 11190
This 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();
}
}
}
Upvotes: 4
Reputation: 653
I am not sure what will happen at 100 or 1000 points
Nothing, you do not need to worry about it. There are already a lot of points being plot every time there is any activity on the screen.
The first thought was to simply add new point and draw. Add another and again.
This is the way to go I feel. You may want to take a more systematic approach with this:
After this postinvalidate on your view.
I am adding new point, ask view to invalidate itself but still some points aren't drawn.
Probably your points are going off the screen. Do check.
In this case even using some queue might be difficult because the onDraw() will start from the beginning again so the number of queue elements will just increase.
This should not be a problem as the points on the screen will be limited, so the queue will hold only so many points, as previous points will be deleted.
Hope this approach helps.
Upvotes: 0
Reputation: 3481
If you already have a view that draws static data then you're close to your goal. The only thing you then have to do is:
1) Extract the logic that retrieves data 2) Extract the logic that draws this data to screen 3) Within the onDraw() method, first call 1) - then call 2) - then call invalidate() at the end of your onDraw()-method - as this will trigger a new draw and view will update itself with the new data.
Upvotes: 0
Reputation: 15668
Now I would suggest you the GraphView Library. It is open source, don't worry about the license and it's not that big either (<64kB). You can clean up the necessary files if you wish to.
You can find a sample of usages for real time plots
From the 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;
}
}
Upvotes: 0
Reputation: 2416
What I did in a similar situation is to create a custom class, let's call it "MyView" that extends View and add it to my layout XML.
public class MyView extends View {
...
}
In 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>
Within MyView, override method "onDraw(Canvas canv)". onDraw gets a canvas you can draw on. In onDraw, get a Paint object new Paint() and set it up as you like. Then you can use all the Canvas drawing function, e.g., drawLine, drawPath, drawBitmap, drawText and tons more.
As far as performance concerns, I suggest you batch-modify your underlying data and then invalidate the view. I think you must live with full re-draws. But if a human is watching it, updating more than every second or so is probably not profitable. The Canvas drawing methods are blazingly fast.
Upvotes: 0