2011-10-09 5 views
6

[MISE À JOUR] Pour conclure cette question, j'ai implémenté mon graphique en utilisant les deux méthodes suivantes (voir ci-dessous). drawCurve() reçoit un Canvas et un tableau de float. Le tableau est correctement rempli (les horodatages sont pris en charge par l'index de valeur dans le tableau) et varie de 0,0 à 1,0. Le tableau est envoyé à prepareWindowArray() qui prend un morceau du tableau de la position windowStart pour windowSize -values, d'une manière circulaire.Graphique dynamique personnalisé sous Android

Le tableau utilisé par GraphView et par le fournisseur de données (un périphérique Bluetooth) est le même. Une classe au centre garantit que GraphView ne lit pas les données écrites par le périphérique Bluetooth. Comme le GraphView boucle toujours à travers le tableau et le redessine à chaque itération, il se met à jour en fonction des données écrites par le périphérique Bluetooth, et en forçant la fréquence d'écriture du périphérique Bluetooth à la fréquence de rafraîchissement du Graph, j'obtiens un animation de mon signal.

La méthode de invalidate()GraphView est appelé par le Activity, qui courent un Timer pour actualiser le graphique à toutes x millisecondes. La fréquence à laquelle le graphique est actualisé est définie dynamiquement, de sorte qu'il s'adapte au flux de données du périphérique Bluetooth (qui spécifie la fréquence de son signal dans l'en-tête de son paquet).

Trouvez le code complet de mon GraphView dans la réponse que j'ai écrite ci-dessous (dans la section réponse). Si vous trouvez des erreurs ou si vous voulez l'optimiser, faites-le moi savoir. Ce serait vivement apprécié!

/** 
* 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); 

} 

Procédé prepareWindowArray() qui mettent en oeuvre le comportement circulaire de la matrice:

/** 
* 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]

je fais une application qui lire des données à partir d'un dispositif Bluetooth à un taux fixe. Chaque fois que j'ai de nouvelles données, je veux qu'elles soient tracées sur le graphique à droite, et traduire le reste du graphique vers la gauche en temps réel. Fondamentalement, comme un oscilloscope ferait l'affaire. J'ai donc fait une vue personnalisée, avec un axe xy, un titre et des unités. Pour ce faire, je dessine simplement ces choses sur le canevas View. Maintenant, je veux dessiner la courbe. Je parviens à dessiner une courbe statique d'un tableau déjà rempli en utilisant cette méthode:

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

Il me donne quelque chose comme ça.

My custom GraphView

Il y a encore des choses à fixer sur mon graphique (les sous-axe ne sont pas correctement mise à l'échelle), mais ce sont des détails que je peux régler plus tard. Maintenant, je veux changer ce graphique statique (qui reçoit une matrice de valeurs non dynamique) avec quelque chose de dynamique qui redessinerait la courbe toutes les 40ms, en poussant les anciennes données vers la gauche et en traçant les nouvelles données vers la droite, afin que je puisse visualiser en temps réel les informations fournies par le périphérique Bluetooth.

Je sais qu'il existe déjà un progiciel de graphisme, mais je ne suis pas très familier avec ce genre de choses et j'aimerais les appliquer en implémentant ce graphique moi-même.De plus, la plus grande partie de ma classe GraphView est terminée, sauf pour la partie courbe.

deuxième question, je me demande comment je devrais envoyer les nouvelles valeurs au graphique. Dois-je utiliser quelque chose comme une pile FIFO, ou puis-je réaliser ce que je veux avec une simple matrice de doubles? D'un côté, les 4 champs en bas sont déjà mis à jour dynamiquement. Eh bien, ils font semblant de simuler la "dynamique", ils bouclent à travers la même double matrice encore et encore, ils ne prennent pas réellement de nouvelles valeurs.

Merci pour votre temps! Si quelque chose n'est pas clair sur ma question, faites le moi savoir et je le mettrai à jour avec plus de détails.

+0

pouvez-vous donner un lien pour tous les projets? ce look est très intéressant! –

Répondre

7

Comme mentionné dans ma question, voici la classe que j'ai conçue pour résoudre mes problèmes.

/** 
* 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

Eh bien, je commencerais par juste essayer de redessiner tout le code que vous avez et les données réelles de dynalic. Seulement si cela ne suffit pas devez-vous essayer quelque chose de fantaisie rapide comme le défilement ...

Si vous avez besoin de fantaisie Je voudrais essayer somthing comme ça.

J'attire la partie dynamique du graphique dans un Bitmap secondaire que vous gardiez entre les cadres plutôt que directement aux canves.

Dans ce bitmap dynamique secondaire lorsque vous tracez de nouvelles données, vous devez d'abord effacer les anciennes données que vous remplacez. dessiner la tranche appropriée du bitmap d'arrière-plan statique sur le haut des données périmées, ce qui l'efface et rend l'arrière-plan agréable et frais à nouveau. Vous avez juste besoin de dessiner votre nouveau bit de données dynamiques. L'astuce est que vous dessinez dans ce deuxième bitmap de gauche à droite, puis retournez à gauche à la fin et recommencez.

Pour obtenir du bitmap soncodary à vos Cancas dessiner le bitmap sur la toile en deux parties. Les anciennes données à la droite de ce que vous venez d'ajouter doivent être dessinées sur la partie gauche de votre dernier canevas et les nouvelles données doivent être dessinées immédiatement à droite de celle-ci.

pour envoyer les données d'une mémoire tampon circulaire serait la chose normale pour ce type de données où une fois qu'il est hors du graphique vous ne vous inquiétez pas.

+0

Merci pour votre idée, cela me donne un bon sentiment sur la façon de résoudre ce problème. – AntoineG