使用Android自定义视图实时绘制图表

时间:2015-05-14 18:14:03

标签: android

我想在我的应用程序中使用实时绘图制作简单的线条图。我知道有很多不同的图书馆但它们太大或没有正确的功能或许可证。

我的想法是制作自定义视图并扩展View类。在这种情况下使用OpenGL就像用正典射击鸭子一样。我已经有了绘制静态数据的视图 - 首先我将所有数据放在float对象的Plot数组中,然后使用循环绘制onDraw() PlotView方法中的所有内容}。class。

我还有一个线程可以为我的情节提供新数据。但现在的问题是如何在添加新数据时绘制它。首先想到的是简单地添加新点和绘图。添加另一个。但我不确定在100或1000点会发生什么。我正在添加新点,请求视图使自己无效,但仍然没有绘制一些点。在这种情况下,即使使用某个队列也可能很困难,因为onDraw()将从头开始,因此队列元素的数量将会增加。

您建议您实现这一目标?

6 个答案:

答案 0 :(得分:3)

这应该可以解决问题。

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();
        }
    }
}

答案 1 :(得分:2)

让我尝试更多地勾勒出问题。

  • 你有一个可以绘制点和线的表面,你知道如何让它看起来像你想要的样子。
  • 您有一个数据源,可提供绘制点,并且该数据源会即时更改。
  • 您希望曲面尽可能准确地反映传入的数据。

第一个问题是 - 你的情况怎么样慢?你知道你的延误来自哪里吗?首先,确保你有一个问题需要解决;第二,确保你知道问题的来源。

让我们说你的问题与你所暗示的数据大小有关。如何解决这个问题是一个复杂的问题。它取决于绘制的数据的属性 - 您可以假设的不变量等等。您已经谈到了在float[]中存储数据,因此我假设您已经获得了固定数量的数据点,这些数据点的值会发生变化。我也会假设通过100或1000&#39;你的意思是&#39;很多很多&#39;,因为坦率地说1000浮标并不是很多数据。

当你有一个非常大的数组要绘制时,你的性能限制最终将来自于数组的循环。然后,您的性能提升将减少您循环的阵列数量。这就是数据属性发挥作用的地方。

减少重绘操作量的一种方法是保留一个“脏列表”。其作用类似于Queue<Int>。每次数组中的单元格发生更改时,您都会将该数组索引排入队列,并将其标记为“脏”&#39;。每次你的draw方法回来时,在脏列表中出现固定数量的条目并仅更新与这些条目对应的渲染图像的块 - 你可能需要进行一些缩放和/或反别名或其他因为有很多数据点,你可能得到的数据多于屏幕像素。您在任何给定帧更新中重绘的条目数应受所需帧速率的限制 - 您可以根据先前绘制操作所花费的时间以及脏列表的深度来衡量这种自适应性,以保持良好状态帧率和可见数据年龄之间的平衡。

如果您尝试同时在屏幕上绘制所有数据,则此功能尤其适用。如果您只查看一大块数据(例如在可滚动视图中),并且阵列位置和窗口大小之间存在某种对应关系,那么您可以“查看”窗口&#39;数据 - 在每次绘制调用中,仅考虑屏幕上实际存在的数据子集。如果您还有缩放功能。事情继续下去,你可以混合使用这两种方法 - 这可能会变得复杂。

如果您的数据被窗口化,使得每个数组元素中的值决定数据点是在屏幕上还是在屏幕外,请考虑使用排序键为值的排序列表。这将允许您在这种情况下执行上面列出的窗口优化。如果窗口是在两个维度上进行的,那么您很可能只需要执行一个或另一个优化,但是有两个维度范围查询结构也可以为您提供此功能。

让我们说我对固定数据大小的假设是错误的;相反,您将数据添加到列表末尾,但现有数据点不会发生变化。在这种情况下,你最好使用一个类似于链接的队列结构来删除旧的数据点而不是一个数组,因为增加你的数组会不必要地在应用程序中引入口吃。

在这种情况下,您的优化是预先绘制到跟随队列的缓冲区 - 当新元素进入队列时,将整个缓冲区移到左侧并仅绘制包含新元素的区域。

如果它是/ rate /数据条目的问题,那么使用排队结构并跳过元素 - 要么在它们被添加到队列时折叠它们,存储/绘制每个n元素或类似的元素。

如果相反它是占用所有时间的渲染过程,请考虑在后台线程上渲染并存储渲染图像。这将让您花费尽可能多的时间进行重绘 - 图表本身的帧速率将下降,但不会降低整体应用程序的响应速度。

答案 2 :(得分:0)

我在类似的情况下所做的是创建一个自定义类,让我们称之为&#34; MyView&#34;扩展View并将其添加到我的布局XML。

public class MyView extends View {
  ...
}

布局:

<?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中,覆盖方法&#34; onDraw(Canvas canv)&#34;。 onDraw获取可以绘制的画布。在onDraw中,获取一个Paint对象new Paint()并根据需要进行设置。然后你可以使用所有Canvas绘图功能,例如drawLine,drawPath,drawBitmap,drawText和更多。

就性能问题而言,我建议您批量修改基础数据,然后使视图无效。我认为你必须忍受全面的重新抽奖。但是,如果一个人正在观看它,那么每隔一秒左右更新一次可能就无法获利。 Canvas绘图方法非常快。

答案 3 :(得分:0)

现在我建议你GraphView Library。它是开源的,不用担心许可证,也不是那么大(<64kB)。如果您愿意,可以清理必要的文件。

您可以找到实时图表的使用示例

来自官方样本:

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;
    }
}

答案 4 :(得分:0)

如果您已有一个绘制静态数据的视图,那么您就接近目标了。 你唯一要做的就是:

1)提取检索数据的逻辑 2)提取将此数据绘制到屏幕的逻辑 3)在onDraw()方法中,首先调用1) - 然后调用2) - 然后在onDraw() - 方法结束时调用invalidate() - 因为这将触发新的绘制,视图将使用新的更新自身数据

答案 5 :(得分:0)

  

我不确定100或1000点会发生什么

没什么,你不需要担心它。每当屏幕上有任何活动时,就会有很多要点。

  

首先想到的是简单地添加新点和绘图。添加另一个。

这是我觉得的方式。您可能希望采用更系统的方法:

  1. 检查绘图是在屏幕上还是在屏幕上。
  2. 如果他们将在屏幕上,只需将其添加到您的阵列,这应该是好的。
  3. 如果数据不在屏幕上,请根据以下内容进行适当的坐标计算:从数组中删除第一个元素,在数组中添加新元素并重绘。
  4. 在此视图后验证后验证。

      

    我正在添加新点,请求视图使自身无效,但仍然没有绘制一些点。

    可能你的观点已经消失了。请检查。

      

    在这种情况下,即使使用某个队列也可能很困难,因为onDraw()将从头开始,因此队列元素的数量将会增加。

    这应该不是问题,因为屏幕上的点数将受到限制,因此队列将只保留这么多点,因为之前的点将被删除。

    希望这种方法有所帮助。