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



我想在我的应用程序中使用实时绘图来制作简单的行绘图。我知道有很多各种图书馆,但它们太大或没有正确的功能或许可。

我的想法是进行自定义视图,只扩展View类。在这种情况下,使用OpenGL就像用佳能射击鸭子一样。我已经有了绘制静态数据的视图 - 首先,我将所有数据放在我的Plot对象的float数组中,然后使用LOOP绘制onDraw() PlotView类的所有数据。

我也有一个线程,可以为我的图提供新的数据。但是现在的问题是如何在添加新数据时绘制它。第一个想法是简单地添加新观点并绘制。再次添加。但是我不确定在100或1000点会发生什么。我正在添加新的观点,要求视图使自己无效,但仍未提取一些要点。在这种情况下,即使使用一些队列也可能很困难,因为onDraw()将从开始再次开始,因此队列元素的数量只会增加。

您建议什么来实现此目标?

这应该可以解决问题。

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

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

  • 您有一个可以绘制点和线条的表面,并且您知道如何使其看起来如何看起来。
  • 您有一个数据源可以提供绘制点,并且该数据源被即时更改。
  • 您希望表面尽可能准确地反映传入的数据。

第一个问题是 - 您的情况如何缓慢?你知道延迟来自哪里吗?首先,请确保您有问题要解决;其次,请确保您知道您的问题来自哪里。

假设您的问题是您所暗示的数据大小。如何解决这个问题是一个复杂的问题。这取决于要绘制的数据的属性 - 您可以假设的不变性等等。您已经谈论过将数据存储在float[]中,因此我假设您有一个固定数量的数据点可以变化。我还要假设您的意思是" 100或1000",因为坦率地说,有1000个浮标不是很多数据。

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

减少REDRAW操作体积的一种方法是保持"肮脏列表",该列表像Queue<Int>一样。每当您的数组中的单元格更改时,您就会招募该数组索引,将其标记为"脏"。每当您的绘图方法返回时,肮脏列表中的固定条目都固定数量,并仅更新与这些条目相对应的渲染图像的一部分 - 您可能必须进行一些缩放和/或抗氧化因为有很多数据点,您可能拥有的数据可能比屏幕像素更多。您在任何给定的帧更新中重绘的条目次数应受您所需的帧速率的界限 - 您可以根据以前的抽奖操作多长时间进行了多长时间以及肮脏的列表的深度,以保持适应性帧速率和可见数据年龄之间的平衡。

如果您试图一次绘制屏幕上的所有数据,这特别合适。如果您仅查看一块数据(例如在可滚动视图中),并且在数组位置和窗口大小之间存在某种对应关系,那么您可以"窗口"数据 - 在每个绘制调用中,只考虑屏幕上实际的数据子集。如果您还进行了"缩放"的事情,则可以混合两种方法 - 这种方法可能会变得复杂。

如果您的数据被窗口窗口,以使每个数组元素中的值是确定数据点在屏幕上还是关闭屏幕的原因,请考虑使用排序的对列表,其中sort键为值。这将使您在这种情况下执行上面概述的窗口优化。如果窗口在两个维度上都进行,则您很可能只需要执行一个或另一个优化,但是有两个维度的查询结构也可以为您提供。

假设我对固定数据大小的假设是错误的;取而代之的是,您将数据添加到列表的末尾,但是现有数据点不会改变。在这种情况下,您可能会使用链接的队列式结构来删除旧数据点而不是数组,因为生长数组会不必要地在应用程序中引入口吃。

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

如果是问题的/率/数据输入,则使用排队结构并跳过元素 - 无论将它们添加到队列时,它们都会折叠,存储/绘制每个n元素或类似的内容。

相反,如果是呈现所有时间的渲染过程,请考虑在背景线程上渲染并存储渲染的图像。这将使您花费尽可能多的时间进行重绘 - 图表本身中的帧率将下降,但不会降低您的整体应用响应能力。

我在类似情况下所做的就是创建自定义类,我们将其称为" myview",然后将视图扩展并将其添加到我的布局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中,覆盖方法" OnDraw(Canvas Canv)"。OnDraw获得了您可以绘制的画布。在OnDraw中,获取涂料对象新涂料()并根据需要进行设置。然后,您可以使用所有画布绘图功能,例如drawline,drawpath,drawbitMap,drawtext和tons。

就绩效关注而言,我建议您批量修改基础数据,然后使视图无效。我认为您必须全面绘制。但是,如果一个人正在观看它,那么更新每秒钟左右的更新可能不会盈利。画布图方法的速度非常快。它是开源的,不必担心许可证,也不是那么大(&lt; 64kb)。如果您愿意,您可以清理必要的文件。

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

  • 在官方样本中-https://github.com/jjoe64/graphview-demos/
  • balanduino项目-https://github.com/tkjelectronics/balanduinoandroidapp/blob/blob/master/src/com/tkjelectronics/balanduino/graphfragment.java

来自官方样本:

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

如果您已经有绘制静态数据的视图,则您接近目标。然后,您唯一要做的是:

1)提取检索数据的逻辑2)提取将此数据绘制到屏幕的逻辑3)在OnDraw()方法中,第一个调用1) - 然后调用2) - 然后在您的OnDraw() - 方法末尾调用Invalidate(),因为这将触发新的绘制,并且视图将随着新的方式更新数据。

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

没事,您不必担心它。每次屏幕上有任何活动时,都已经有很多要点。

第一个想法是简单地添加新的点并绘制。再次添加。

这是我的感觉。您可能需要对此采取更系统的方法:

  1. 检查绘图是在屏幕上还是在屏幕上。
  2. 如果它们在屏幕上,只需将其添加到您的数组中,那应该很好。
  3. 如果数据不在屏幕上,请考虑以下情况进行适当的坐标计算:从数组中删除第一个元素,在数组中添加新元素,并在Redraw中添加。

在您的视图上引人入胜。

我正在添加新的观点,要求视图以使其自身无效,但仍未绘制一些要点。

您的观点可能正在脱离屏幕。检查。

在这种情况下,即使使用一些队列也可能很难

这不应该是一个问题,因为屏幕上的要点将受到限制,因此队列只能容纳这么多点,因为先前的几点将被删除。

希望这种方法会有所帮助。

最新更新