7

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?

sebap123
  • 2,541
  • 6
  • 45
  • 81
  • 1
    How do you want to draw the plot when there are a lot of points? Will it be scrollable? Or the plot will be moved left and old points will be getting hidden? Actually, in any case you can redraw only visible part of the plot, so there will not be a lot of drawing. – esentsov May 21 '15 at 09:20
  • are all these points visible simultaniouly? – Rahul Tiwari May 21 '15 at 11:22
  • @esentsov I want it to scroll automatically when new points appear. I only need new points so the old ones can be destroyed. – sebap123 May 21 '15 at 11:48
  • If you are worried about performance, use `ImageView` backed by a `Bitmap` / `BitmapDrawable`. When new data appears, draw it on the bitmap, but do not clear old data, this way you do not need to redraw everything each frame. – pelya May 22 '15 at 14:32

6 Answers6

4

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();
        }
    }
}
Simon
  • 10,932
  • 50
  • 49
3

Let me try to sketch out the problem a bit more.

  • You've got a surface that you can draw points and lines to, and you know how to make it look how you want it to look.
  • You have a data source that provides points to draw, and that data source is changed on the fly.
  • You want the surface to accurately reflect the incoming data as closely as possible.

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 nth 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.

Mark McKenna
  • 2,857
  • 1
  • 17
  • 17
0

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.

DontPanic
  • 2,164
  • 5
  • 29
  • 56
  • As I wrote I know how to draw a plot with static data. The problem is to plot a dynamic data. – sebap123 May 16 '15 at 19:09
  • Not sure what you mean. If your data is in an array or database, just modify it there and redraw from it. If your thread changes the data, the plot *is* "dynamic". Or are you trying to get some real-time video animation effect? – DontPanic May 16 '15 at 19:18
  • Further, if you're worried about managing redraws, I'd suggest your update thread set a "redraw required" flag on any change. Start a Timer in your app and check the flag and do an invalidate if set. That way, you'd get timely updates and not suffer an avalanche of redraws. – DontPanic May 16 '15 at 19:29
  • In short - I have a thread that is providing constant stream of data (new data) received from Bluetooth device. I want this data to be drawn. As I wrote I was thinking about adding those values to array and then redrawing but if I'll have 1000 points and then add another one there is possibility that before everything will be drawn the new value will come. – sebap123 May 16 '15 at 20:20
  • I don't see a problem. If new data arrives and is added to your array after onDraw begins, it would get picked up on the next timer tick. You probably would have to protect your data with a synchronized method and lock it during draw and update so your data queue bookkeeping doesn't get confused. – DontPanic May 16 '15 at 20:57
0

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;
    }
}
Bojan Kseneman
  • 15,488
  • 2
  • 54
  • 59
  • Well.. I am fighting with `GraphView` already but it looks like there is a bug - http://stackoverflow.com/questions/30290575/android-graphview-project-get-freeze-with-real-time-updates – sebap123 May 21 '15 at 17:18
  • I have not experienced anything like that. Keep a fixed size buffer, like in the balanduino project does and stuff like that will not happen. I believe he was infinitely adding data and not clearing it. Also stop updating the UI when the app goes to pause or bad stuff might happen. – Bojan Kseneman May 21 '15 at 17:26
  • 1
    Actually I remember I left the phone plugged into AC and with the option to keep screen on while charging through the night and it was still working in the morning. But that was sometime ago and using an older library, at that time it was 3.4 or something. I never updated my code because it was for a school project... once you pass the exams, you don't care about stuff like that any more :))) – Bojan Kseneman May 21 '15 at 17:30
  • Maybe with old library it works, but now you can't add whole array to draw. Also I am using buffer but in best case scenario it isn't moving smoothly. – sebap123 May 21 '15 at 17:45
  • This is what I have used, also it's only 24kBs. Follow the balanduino code, mine is almost the same. https://dl.dropboxusercontent.com/u/91522375/GraphView-3.1.jar – Bojan Kseneman May 21 '15 at 17:51
  • increase the max data points from 40 to 400 and it will crash afetr some time doe to memory leakage – abh22ishek Jun 17 '16 at 05:50
0

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.

DKIT
  • 3,471
  • 2
  • 20
  • 24
0

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:

  1. check if the plot will be on the screen or off the screen.
  2. if they will be on the the screen simply add it to your array and that should be good.
  3. if the data is not on the screen, make appropriate coordinate calculations considering the following: remove the first element from the array, add the new element in the array and redraw.

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.

antonio
  • 18,044
  • 4
  • 45
  • 61
user2520215
  • 603
  • 8
  • 15