2011-10-09 48 views
6

[UPDATE] 为了总结这个问题,我使用以下两种方法实现了我的图形(请参阅下文)。 drawCurve()收到Canvasfloat的数组。该数组已被正确填充(时间戳由数组中的值索引承担)并从0.0到1.0变化。该阵列被发送到prepareWindowArray(),其以循环方式从位置windowStart获取windowSize值的数组的块。Android中的自定义动态图形

GraphView和数据提供者(蓝牙设备)使用的数组是相同的。中间的类确保GraphView不读取蓝牙设备正在写入的数据。由于GraphView始终通过阵列循环并在每次迭代时重新绘制,它将根据蓝牙设备写入的数据进行更新,并且通过强制蓝牙设备的写入频率达到Graph的刷新频率,我获得了平滑我的信号动画。

GraphViewinvalidate()方法由Activity,其运行一个Timer刷新图形在每个x毫秒调用。图形刷新的频率是动态设置的,以便它适应来自蓝牙设备的数据流(指定其信息包头中的信号频率)。

在我在下面写的答案(答案部分)中找到我的GraphView的完整代码。如果你们发现错误或优化方法,请告诉我;这将不胜感激!

/** 
* Read a buffer array of size greater than "windowSize" and create a window array out of it. 
* A curve is then drawn from this array using "windowSize" points, from left 
* to right. 
* @param canvas is a Canvas object on which the curve will be drawn. Ensure the canvas is the 
* later drawn object at its position or you will not see your curve. 
* @param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0. 
* A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at 
* the top of the graph. The range is not tested, so you must ensure to pass proper values, or your 
* graph will look terrible. 
*  0.0 : draw at the bottom of the graph 
*  0.5 : draw in the middle of the graph 
*  1.0 : draw at the top of the graph 
*/ 
private void drawCurve(Canvas canvas, float[] data){ 

    // Create a reference value to determine the stepping between each points to be drawn 
    float incrementX = (mRightSide-mLeftSide)/(float) windowSize; 

    float incrementY = (mBottomSide - mTopSide); 

    // Prepare the array for the graph 
    float[] source = prepareWindowArray(data); 

    // Prepare the curve Path 
    curve = new Path(); 
    // Move at the first point. 
    curve.moveTo(mLeftSide, source[0]*incrementY); 
    // Draw the remaining points of the curve 
    for(int i = 1; i < windowSize; i++){ 
     curve.lineTo(mLeftSide + (i*incrementX), source[i] * incrementY); 
    } 

    canvas.drawPath(curve, curvePaint); 

} 

prepareWindowArray()方法实现阵列的圆形行为:

/** 
* Extract a window array from the data array, and reposition the windowStart 
* index for next iteration 
* @param data the array of data from which we get the window 
* @return an array of float that represent the window 
*/ 
private float[] prepareWindowArray(float[] data){ 
    // Prepare the source array for the graph. 
    float[] source = new float[windowSize]; 

    // Copy the window from the data array into the source array 
    for(int i = 0; i < windowSize; i++){ 
     if(windowStart+i < data.length)       // If the windows holds within the data array 
      source[i] = data[windowStart + i];     // Simply copy the value in the source array 
     else{             // If the window goes beyond the data array 
      source[i] = data[(windowStart + 1)%data.length]; // Loop at the beginning of the data array and copy from there 
     } 
    } 
    // Reposition the buffer index 
    windowStart = windowStart + windowSize; 
    // If the index is beyond the end of the array 
    if(windowStart >= data.length){ 
     windowStart = windowStart % data.length; 
    } 

    return source; 
} 

[/ UPDATE]

我正在一个应用程序,在从蓝牙设备读取的数据固定利率。每当我有新的数据时,我都希望它们被绘制在右侧的图上,并将图的其余部分实时转换到左侧。基本上,就像示波器一样。

所以我用xy轴做了一个自定义视图,标题和单位。要做到这一点,我只需在视图画布上绘制这些东西。现在我想绘制曲线。我设法使用这种方法从已填充的阵列中绘制出一条静态曲线:

public void drawCurve(Canvas canvas){ 

    int left = getPaddingLeft(); 
    int bottom = getHeight()-getPaddingTop(); 
    int middle = (bottom-10)/2 - 10; 

    curvePaint = new Paint(); 
    curvePaint.setColor(Color.GREEN); 
    curvePaint.setStrokeWidth(1f); 
    curvePaint.setDither(true); 
    curvePaint.setStyle(Paint.Style.STROKE); 
    curvePaint.setStrokeJoin(Paint.Join.ROUND); 
    curvePaint.setStrokeCap(Paint.Cap.ROUND); 
    curvePaint.setPathEffect(new CornerPathEffect(10)); 
    curvePaint.setAntiAlias(true); 

    mCurve = new Path(); 
    mCurve.moveTo(left, middle); 
    for(int i = 0; i < mData[0].length; i++) 
     mCurve.lineTo(left + ((float)mData[0][i] * 5), middle-((float)mData[1][i] * 20)); 


    canvas.drawPath(mCurve, curvePaint); 
} 

它给了我类似这样的东西。

My custom GraphView

还有事情要解决我的图形(副轴不能正常缩放),但这些都是细节,我可以稍后再修正。

现在我想要改变这个静态图(它接收一个非动态矩阵的值),并且每隔40ms重新绘制一条曲线,将旧数据向左推并将新数据绘制到右侧,所以我可以实时查看蓝牙设备提供的信息。

我知道有一些图形包已经存在了,但我对这些东西有点小事,我想通过自己实现这个图来实践。此外,我的大部分GraphView类都已完成,曲线部分除外。

第二个问题,我想知道我应该如何将新值发送给图。我应该使用像FIFO堆栈这样的东西,还是可以用简单的双倍矩阵来实现我想要的功能?

在旁注上,底部的4个字段已经动态更新。那么,他们有点伪装“动态”,他们一次又一次地通过同一个双重矩阵,他们实际上并没有采取新的价值。

谢谢你的时间!如果有关于我的问题的内容不清楚,请告诉我,我会通过更多细节进行更新。

+0

你能给所有项目链接吗?这看起来很有趣! –

回答

7

正如我的问题中提到的,这里是我设计的课程来解决我的问题。

/** 
* A View implementation that displays a scatter graph with 
* automatic unit scaling. 
* 
* Call the <i>setupGraph()</i> method to modify the graph's 
* properties. 
* @author Antoine Grondin 
* 
*/ 

public class GraphView extends View { 

    ////////////////////////////////////////////////////////////////// 
    // Configuration 
    ////////////////////////////////////////////////////////////////// 

    // Set to true to impose the graph properties 
    private static final boolean TEST = false; 

    // Scale configuration 
    private float minX = 0;   // When TEST is true, these values are used to 
    private float maxX = 50;  // Draw the graph 
    private float minY = 0; 
    private float maxY = 100; 

    private String titleText = "A Graph..."; 
    private String xUnitText = "s"; 
    private String yUnitText = "Volts"; 

    // Debugging variables 
    private boolean D = true; 
    private String TAG = "GraphView"; 

    ////////////////////////////////////////////////////////////////// 
    // Member fields 
    ////////////////////////////////////////////////////////////////// 

    // Represent the borders of the View 
    private int mTopSide = 0; 
    private int mLeftSide = 0; 
    private int mRightSide = 0; 
    private int mBottomSide = 0; 
    private int mMiddleX = 0; 
    // Size of a DensityIndependentPixel 
    private float mDips = 0; 

    // Hold the position of the axis in regard to the range of values 
    private int positionOfX = 0; 
    private int positionOfY = 0; 

    // Index for the graph array window, and size of the window 
    private int windowStart = 0; 
    private int windowSize = 128; 
    private float[] dataSource; 

    // Painting tools 
    private Paint xAxisPaint; 
    private Paint yAxisPaint; 
    private Paint tickPaint; 
    private Paint curvePaint; 
    private Paint backgroundPaint; 

    private TextPaint unitTextPaint; 
    private TextPaint titleTextPaint; 

    // Object to be drawn 

    private Path curve; 
    private Bitmap background; 

    /////////////////////////////////////////////////////////////////////////////// 
    // Constructors 
    /////////////////////////////////////////////////////////////////////////////// 

    public GraphView(Context context) { 
     super(context); 
     init(); 
    } 

    public GraphView(Context context, AttributeSet attrs){ 
     super(context, attrs); 
     init(); 
    } 

    public GraphView(Context context, AttributeSet attrs, int defStyle){ 
     super(context, attrs, defStyle); 
     init(); 
    } 

    /////////////////////////////////////////////////////////////////////////////// 
    // Configuration methods 
    /////////////////////////////////////////////////////////////////////////////// 

    public void setupGraph(String title, String nameOfX, float min_X, float max_X, String nameOfY, float min_Y, float max_Y){ 
     if(!TEST){ 
      titleText = title; 
      xUnitText = nameOfX; 
      yUnitText = nameOfY; 
      minX = min_X; 
      maxX = max_X; 
      minY = min_Y; 
      maxY = max_Y; 
     } 
    } 

    /** 
    * Set the array this GraphView is to work with. 
    * @param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0. 
    * A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at 
    * the top of the graph. The range is not tested, so you must ensure to pass proper values, or your 
    * graph will look terrible. 
    *  0.0 : draw at the bottom of the graph 
    *  0.5 : draw in the middle of the graph 
    *  1.0 : draw at the top of the graph 
    */ 
    public void setDataSource(float[] data){ 
     this.dataSource = data; 
    } 

    /////////////////////////////////////////////////////////////////////////////// 
    // Initialization methods 
    /////////////////////////////////////////////////////////////////////////////// 

    private void init(){ 
     initDrawingTools(); 
    } 

    private void initConstants(){ 
     mDips = getResources().getDisplayMetrics().density; 
     mTopSide = (int) (getTop() + 10*mDips); 
     mLeftSide = (int) (getLeft() + 10*mDips); 
     mRightSide = (int) (getMeasuredWidth() - 10*mDips); 
     mBottomSide = (int) (getMeasuredHeight() - 10*mDips); 
     mMiddleX = (mRightSide - mLeftSide)/2 + mLeftSide; 
    } 

    private void initWindowSetting() throws IllegalArgumentException { 

     // Don't do anything if the given values make no sense 
     if(maxX < minX || maxY < minY || 
       maxX == minX || maxY == minY){ 
      throw new IllegalArgumentException("Max and min values make no sense"); 
     } 
     // Transform the values in scanable items 
     float[][] maxAndMin = new float[][]{ 
       {minX, maxX}, 
       {minY, maxY}}; 
     int[] positions = new int[]{positionOfY, positionOfX}; 

     // Place the X and Y axis in regard to the given max and min 
     for(int i = 0; i<2; i++){ 
      if(maxAndMin[i][0] < 0f){ 
       if(maxAndMin[i][1] < 0f){ 
        positions[i] = (int) maxAndMin[i][0]; 
       } else{ 
        positions[i] = 0; 
       } 
      } else if (maxAndMin[i][0] > 0f){ 
       positions[i] = (int) maxAndMin[i][0]; 
      } else { 
       positions[i] = 0; 
      } 
     } 

     // Put the values back in their right place 
     minX = maxAndMin[0][0]; 
     maxX = maxAndMin[0][1]; 
     minY = maxAndMin[1][0]; 
     maxY = maxAndMin[1][1]; 

     positionOfY = mLeftSide + (int) (((positions[0] - minX)/(maxX-minX))*(mRightSide - mLeftSide));  
     positionOfX = mBottomSide - (int) (((positions[1] - minY)/(maxY-minY))*(mBottomSide - mTopSide)); 
    } 

    private void initDrawingTools(){ 

     xAxisPaint = new Paint(); 
     xAxisPaint.setColor(0xff888888); 
     xAxisPaint.setStrokeWidth(1f*mDips); 
     xAxisPaint.setAlpha(0xff); 
     xAxisPaint.setAntiAlias(true); 

     yAxisPaint = xAxisPaint; 

     tickPaint = xAxisPaint; 
     tickPaint.setColor(0xffaaaaaa); 

     curvePaint = new Paint(); 
     curvePaint.setColor(0xff00ff00); 
     curvePaint.setStrokeWidth(1f*mDips); 
     curvePaint.setDither(true); 
     curvePaint.setStyle(Paint.Style.STROKE); 
     curvePaint.setStrokeJoin(Paint.Join.ROUND); 
     curvePaint.setStrokeCap(Paint.Cap.ROUND); 
     curvePaint.setPathEffect(new CornerPathEffect(10)); 
     curvePaint.setAntiAlias(true); 

     backgroundPaint = new Paint(); 
     backgroundPaint.setFilterBitmap(true); 

     titleTextPaint = new TextPaint(); 
     titleTextPaint.setAntiAlias(true); 
     titleTextPaint.setColor(0xffffffff); 
     titleTextPaint.setTextAlign(Align.CENTER); 
     titleTextPaint.setTextSize(20f*mDips); 
     titleTextPaint.setTypeface(Typeface.MONOSPACE); 

     unitTextPaint = new TextPaint(); 
     unitTextPaint.setAntiAlias(true); 
     unitTextPaint.setColor(0xff888888); 
     unitTextPaint.setTextAlign(Align.CENTER); 
     unitTextPaint.setTextSize(20f*mDips); 
     unitTextPaint.setTypeface(Typeface.MONOSPACE); 

    } 

    /////////////////////////////////////////////////////////////////////////////// 
    // Overridden methods 
    /////////////////////////////////////////////////////////////////////////////// 

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ 
     super.onMeasure(widthMeasureSpec, heightMeasureSpec); 
    } 

    protected void onSizeChanged(int w, int h, int oldw, int oldh) { 
     regenerateBackground(); 
    } 

    public void onDraw(Canvas canvas){ 
     drawBackground(canvas); 
     if(dataSource != null) 
      drawCurve(canvas, dataSource); 
    } 

    /////////////////////////////////////////////////////////////////////////////// 
    // Drawing methods 
    /////////////////////////////////////////////////////////////////////////////// 

    private void drawX(Canvas canvas){ 
     canvas.drawLine(mLeftSide, positionOfX, mRightSide, positionOfX, xAxisPaint); 
     canvas.drawText(xUnitText, mRightSide - unitTextPaint.measureText(xUnitText)/2, positionOfX - unitTextPaint.getTextSize()/2, unitTextPaint); 
    } 

    private void drawY(Canvas canvas){ 
     canvas.drawLine(positionOfY, mTopSide, positionOfY, mBottomSide, yAxisPaint); 
     canvas.drawText(yUnitText, positionOfY + unitTextPaint.measureText(yUnitText)/2 + 4*mDips, mTopSide + (int) (unitTextPaint.getTextSize()/2), unitTextPaint); 
    } 

    private void drawTick(Canvas canvas){ 
     // No tick at this time 
     // TODO decide how I want to put those ticks, if I want them 
    } 

    private void drawTitle(Canvas canvas){ 
     canvas.drawText(titleText, mMiddleX, mTopSide + (int) (titleTextPaint.getTextSize()/2), titleTextPaint); 
    } 

    /** 
    * Read a buffer array of size greater than "windowSize" and create a window array out of it. 
    * A curve is then drawn from this array using "windowSize" points, from left 
    * to right. 
    * @param canvas is a Canvas object on which the curve will be drawn. Ensure the canvas is the 
    * later drawn object at its position or you will not see your curve. 
    * @param data is a float array of length > windowSize. The floats must range between 0.0 and 1.0. 
    * A value of 0.0 will be drawn at the bottom of the graph, while a value of 1.0 will be drawn at 
    * the top of the graph. The range is not tested, so you must ensure to pass proper values, or your 
    * graph will look terrible. 
    *  0.0 : draw at the bottom of the graph 
    *  0.5 : draw in the middle of the graph 
    *  1.0 : draw at the top of the graph 
    */ 
    private void drawCurve(Canvas canvas, float[] data){ 

     // Create a reference value to determine the stepping between each points to be drawn 
     float incrementX = (mRightSide-mLeftSide)/(float) windowSize; 

     float incrementY = mBottomSide - mTopSide; 

     // Prepare the array for the graph 
     float[] source = prepareWindowArray(data); 

     // Prepare the curve Path 
     curve = new Path(); 
     // Move at the first point. 
     curve.moveTo(mLeftSide, source[0]*incrementY); 
     // Draw the remaining points of the curve 
     for(int i = 1; i < windowSize; i++){ 
      curve.lineTo(mLeftSide + (i*incrementX), source[i] * incrementY); 
     } 

     canvas.drawPath(curve, curvePaint); 
    } 

    /////////////////////////////////////////////////////////////////////////////// 
    // Intimate methods 
    /////////////////////////////////////////////////////////////////////////////// 

    /** 
    * When asked to draw the background, this method will verify if a bitmap of the 
    * background is available. If not, it will regenerate one. Then, it will draw 
    * the background using this bitmap. The use of a bitmap to draw the background 
    * is to avoid unnecessary processing for static parts of the view. 
    */ 
    private void drawBackground(Canvas canvas){ 
     if(background == null){ 
      regenerateBackground(); 
     } 
     canvas.drawBitmap(background, 0, 0, backgroundPaint); 
    } 

    /** 
    * Call this method to force the <i>GraphView</i> to redraw the cache of it's background, 
    * using new properties if you changed them with <i>setupGraph()</i>. 
    */ 
    public void regenerateBackground(){ 
     initConstants(); 
     try{ 
      initWindowSetting(); 
     } catch (IllegalArgumentException e){ 
      Log.e(TAG, "Could not initalize windows.", e); 
      return; 
     } 
     if(background != null){ 
      background.recycle(); 
     } 
     background = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); 
     Canvas backgroundCanvas = new Canvas(background); 

     drawX(backgroundCanvas); 
     drawY(backgroundCanvas); 
     drawTick(backgroundCanvas); 
     drawTitle(backgroundCanvas); 

    } 

    /** 
    * Extract a window array from the data array, and reposition the windowStart 
    * index for next iteration 
    * @param data the array of data from which we get the window 
    * @return an array of float that represent the window 
    */ 
    private float[] prepareWindowArray(float[] data){ 
     // Prepare the source array for the graph. 
     float[] source = new float[windowSize]; 

     // Copy the window from the data array into the source array 
     for(int i = 0; i < windowSize; i++){ 
      if(windowStart+i < data.length)       // If the windows holds within the data array 
       source[i] = data[windowStart + i];     // Simply copy the value in the source array 
      else{             // If the window goes beyond the data array 
       source[i] = data[(windowStart + 1)%data.length]; // Loop at the beginning of the data array and copy from there 
      } 
     } 
     // Reposition the buffer index 
     windowStart = windowStart + windowSize; 
     // If the index is beyond the end of the array 
     if(windowStart >= data.length){ 
      windowStart = windowStart % data.length; 
     } 

     return source; 
    } 
} 
4

那么我会开始只是试图用你有的代码和真正的代数据重绘它。只有如果这不够快,你需要尝试任何幻想像滚动...

如果你需要花哨,我会尝试像这样的东西。

我会将图形的动态部分绘制到您保留在框架之间而不是直接指向罐子的辅助位图中。我将在另一个位图中的背景上没有动态部分,只在重新缩放等时绘制。

在绘制新数据的第二个动态位图中,首先需要清除要替换的旧数据,然后执行此操作在静态数据的顶部绘制静态背景位图的适当片段,从而清除它并使背景更加美观和新鲜。然后您只需要绘制新的动态数据。诀窍是你从左到右画出第二个位图,然后在最后回到左边并重新开始。

要从soncodary位图获取cancas,将位图绘制为两部分。刚刚添加的旧数据需要绘制到最终画布的左侧,新数据需要直接绘制到右侧。

对于发送数据,一个循环缓冲区对于这类数据来说是正常的事情,一旦它离开图表,你不关心它。

+0

感谢您的想法,它给了我一个良好的感觉,如何解决这个问题。 – AntoineG