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

7

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

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

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

您有什么建议可以实现这个目标吗?


1
当有很多点要绘制时,您希望如何绘制图形?它会是可滚动的吗?还是图形将向左移动,旧点将被隐藏?实际上,在任何情况下,您只能重新绘制可见部分的图形,因此不会有很多绘图。 - esentsov
所有这些点是否可以同时显示? - Rahul Tiwari
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - sebap123
如果您担心性能问题,请使用由Bitmap/BitmapDrawable支持的ImageView。当出现新数据时,将其绘制在位图上,但不要清除旧数据,这样您就不需要每帧重新绘制所有内容。 - pelya
6个回答

4

这应该能解决问题。

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

3
让我更详细地描述一下问题。
  • 您有一个可以绘制点和线条的表面,并且您知道如何使其看起来符合您的要求。
  • 您有一个数据源,提供要绘制的点,并且该数据源会随时更改。
  • 您希望表面尽可能准确地反映传入的数据。

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

假设您的问题在于数据大小,这个问题如何解决是一个复杂的问题。这取决于正在绘制的数据的属性-您可以假设什么不变量等等。您已经谈到了在float[]中存储数据,因此我将假设您拥有一定数量的数据点,其值会发生变化。我还假设通过“100或1000”,您想表达的是“很多很多”,因为老实说,1000个浮点数并不是很多的数据。

当您有一个非常大的数组要绘制时,性能限制最终将来自于遍历数组。然后,您的性能增强将是减少您要遍历的数组量。这就是数据属性发挥作用的地方。

减少重绘操作量的一种方法是保留一个“脏列表”,它类似于Queue<Int>。每当数组中的单元格更改时,将该数组索引入队,标记为“脏”。每次绘制方法回到时,出队脏列表中的固定数量条目,并仅更新与这些条目对应的渲染图像块-由于有那么多数据点,您可能拥有比屏幕像素更多的数据,因此您可能需要进行一些缩放和/或抗锯齿处理等。在任何给定的帧更新中重新绘制的条目数应受所需帧速率的限制-您可以根据先前绘制操作花费的时间以及脏列表的深度来使其自适应,以保持帧速率和可见数据年龄之间的良好平衡。

如果您尝试在屏幕上同时绘制所有数据,则此方法特别适用。如果您只查看数据的一部分(例如在可滚动视图中),并且数组位置与窗口大小之间存在某种对应关系,则可以“窗口化”数据-在每个绘制调用中,仅考虑实际在屏幕上的数据子集。如果您还有一个“缩放”功能,则可以混合使用这两种方法-这可能会变得复杂。

如果你的数据是窗口化的,每个数组元素的值决定了数据点是否在屏幕上,考虑使用一对排序列表,其中排序键是该值。这将让您在这种情况下执行上面概述的窗口化优化。如果窗口处理发生在两个维度中,您很可能只需要执行一个优化,但也有两个维度范围查询结构可以为您提供此功能。
假设我的固定数据大小的假设是错误的;相反,您正在向列表末尾添加数据,但现有数据点不会更改。在这种情况下,您最好使用类似于链接队列的结构,删除旧数据点而不是数组,因为增加数组大小往往会不必要地引入应用程序中的卡顿。
在这种情况下,您的优化是预先绘制一个跟随您的队列的缓冲区 - 当新元素进入队列时,将整个缓冲区向左移动并仅绘制包含新元素的区域。
如果问题是数据输入速率,则使用排队结构并跳过元素 - 在将它们添加到队列中时将其折叠,存储/绘制每个第n个元素或类似的内容。
如果渲染过程占用了所有时间,请考虑在后台线程上进行渲染并存储渲染图像。这将让您花费任意时间进行重绘 - 图表本身的帧率会降低,但整个应用程序的响应性不会下降。

0

如果您已经有一个绘制静态数据的视图,那么您离目标很近了。 然后您需要做的唯一事情是:

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


0
我不确定在100或1000个点时会发生什么。
没事,你不需要担心。每次屏幕上有任何活动时,已经绘制了很多点。
最初的想法是简单地添加新点并进行绘制。再添加一个,再绘制一次。
我觉得这是正确的方法。您可能希望采用更系统化的方法:
1. 检查绘图是否在屏幕上或屏幕外。 2. 如果它们将出现在屏幕上,请将其添加到数组中,然后就可以了。 3. 如果数据不在屏幕上,请进行适当的坐标计算,考虑以下因素:从数组中删除第一个元素,将新元素添加到数组中并重新绘制。
完成后,在您的视图上进行postinvalidate操作。
我正在添加新点,要求视图使自己无效,但仍然没有绘制一些点。
可能是您的点超出了屏幕范围。请检查一下。
在这种情况下,即使使用某些队列也可能很困难,因为onDraw()将重新开始,因此队列元素的数量只会增加。

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

希望这种方法有所帮助。


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

嗯...我已经在与GraphView进行斗争,但似乎存在一个错误 - http://stackoverflow.com/questions/30290575/android-graphview-project-get-freeze-with-real-time-updates - sebap123
我没有经历过那样的事情。像balanduino项目一样保持固定大小的缓冲区,这样类似的问题就不会发生了。我相信他是无限地添加数据而没有清除它。同时,在应用程序进入暂停状态或出现问题时停止更新UI。 - Bojan Kseneman
1
其实我记得有一次我把手机插在交流电源上,并选择保持屏幕在充电期间常亮,到了早上它仍然能正常工作。但那是很久以前用旧库的情况下,当时版本可能为3.4之类的。因为那只是一个学校项目,我从未更新过代码......一旦你通过了考试,你就不再关心这样的事情了:))) - Bojan Kseneman
也许旧库可以工作,但现在你不能添加整个数组来绘制。此外,我正在使用缓冲区,但在最好的情况下,它也不能平滑移动。 - sebap123
这是我使用的,而且只有24kBs。按照balanduino代码,我的代码几乎相同。https://dl.dropboxusercontent.com/u/91522375/GraphView-3.1.jar - Bojan Kseneman
将最大数据点从40增加到400,但由于内存泄漏它会在一段时间后崩溃。 - abh22ishek

0
在类似的情况下,我所做的是创建一个自定义类,我们称之为“MyView”,它扩展了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中,重写方法“onDraw(Canvas canv)”。onDraw会得到一个Canvas对象,你可以在上面绘制。在onDraw中,获取一个Paint对象new Paint()并将其设置为所需的样式。然后你就可以使用所有的Canvas绘图函数,例如drawLine、drawPath、drawBitmap、drawText等等。
就性能而言,我建议你批量修改底层数据,然后使视图无效。我认为你必须接受完全重新绘制的事实。但是,如果有人在观看它,更新频率超过每秒一次可能不划算。Canvas绘图方法非常快速。

正如我所写的那样,我知道如何绘制静态数据的图表。问题在于如何绘制动态数据的图表。 - sebap123
不确定您的意思。如果您的数据存储在数组或数据库中,只需在那里进行修改并从中重新绘制即可。如果您的线程更改了数据,则绘图“动态”的。或者您是否尝试获得一些实时视频动画效果? - DontPanic
此外,如果您担心管理重绘,我建议您的更新线程在任何更改时设置“需要重绘”标志。在应用程序中启动一个计时器,检查标志并在设置后进行无效操作。这样,您将获得及时的更新,而不会遭受大量的重绘。 - DontPanic
简而言之 - 我有一个线程,它提供了从蓝牙设备接收到的不断流动的数据(新数据)。我想要绘制这些数据。正如我所写的,我考虑将这些值添加到数组中,然后重新绘制,但如果我有1000个点,然后再添加一个点,那么在所有内容被绘制之前,新值可能会出现。 - sebap123
我看不到问题。如果在onDraw开始后新的数据被添加到数组中,它会在下一个计时器滴答声中被捕捉到。你可能需要使用同步方法来保护你的数据,在绘制和更新期间锁定它,以便你的数据队列记录不会混乱。 - DontPanic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接