1 package com.mobileer.oboetester; 2 3 import android.content.Context; 4 import android.content.res.TypedArray; 5 import android.graphics.Canvas; 6 import android.graphics.Color; 7 import android.graphics.Paint; 8 import android.util.AttributeSet; 9 import android.view.View; 10 11 import java.util.ArrayList; 12 13 /** 14 * Draw a chart with multiple traces 15 */ 16 public class MultiLineChart extends View { 17 public static final int NUM_DATA_VALUES = 512; 18 private Paint mWavePaint; 19 private Paint mCursorPaint; 20 private int mBackgroundColor = 0xFFF0F0F0; 21 private int mLineColor = Color.RED; 22 private Paint mBackgroundPaint; 23 float[] mVertices = new float[4]; 24 25 CircularFloatArray mXData = new CircularFloatArray(NUM_DATA_VALUES); 26 private ArrayList<Trace> mTraceList = new ArrayList<>(); 27 MultiLineChart(Context context)28 public MultiLineChart(Context context) { 29 super(context); 30 init(null, 0); 31 } 32 MultiLineChart(Context context, AttributeSet attrs)33 public MultiLineChart(Context context, AttributeSet attrs) { 34 super(context, attrs); 35 init(attrs, 0); 36 } 37 MultiLineChart(Context context, AttributeSet attrs, int defStyle)38 public MultiLineChart(Context context, AttributeSet attrs, int defStyle) { 39 super(context, attrs, defStyle); 40 init(attrs, defStyle); 41 } 42 init(AttributeSet attrs, int defStyle)43 private void init(AttributeSet attrs, int defStyle) { 44 // Load attributes 45 final TypedArray a = getContext().obtainStyledAttributes( 46 attrs, R.styleable.MultiLineChart, defStyle, 0); 47 48 mBackgroundColor = a.getColor( 49 R.styleable.MultiLineChart_backgroundColor, 50 mBackgroundColor); 51 mLineColor = a.getColor( 52 R.styleable.MultiLineChart_backgroundColor, 53 mLineColor); 54 55 a.recycle(); 56 57 mWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 58 mWavePaint.setColor(mLineColor); 59 60 mCursorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 61 mCursorPaint.setColor(Color.RED); 62 mCursorPaint.setStrokeWidth(3.0f); 63 64 mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 65 mBackgroundPaint.setColor(mBackgroundColor); 66 mBackgroundPaint.setStyle(Paint.Style.FILL); 67 } 68 69 @Override onDraw(Canvas canvas)70 protected void onDraw(Canvas canvas) { 71 super.onDraw(canvas); 72 73 canvas.drawRect(0.0f, 0.0f, getWidth(), 74 getHeight(), mBackgroundPaint); 75 76 for (Trace trace : mTraceList) { 77 drawTrace(canvas, trace); 78 } 79 } 80 drawTrace(Canvas canvas, Trace trace)81 void drawTrace(Canvas canvas, Trace trace) { 82 // Determine bounds and XY conversion. 83 int numPoints = mXData.size(); 84 if (numPoints < 2) return; 85 // Allocate array for polyline. 86 int arraySize = (numPoints - 1) * 4; 87 if (arraySize > mVertices.length) { 88 mVertices = new float[arraySize]; 89 } 90 // Setup scaling. 91 float previousX = 0.0f; 92 float previousY = 0.0f; 93 float xMax = getXData(1); 94 float xRange = xMax - getXData(numPoints); 95 float yMin = trace.getMin(); 96 float yRange = trace.getMax() - yMin; 97 float width = getWidth(); 98 float height = getHeight(); 99 float xScaler = width / xRange; 100 float yScaler = height / yRange; 101 // Iterate through the available data. 102 int vertexIndex = 0; 103 for (int i = 1; i < numPoints; i++) { 104 float xData = getXData(i); 105 float yData = trace.get(i); 106 float xPos = width - ((xMax - xData) * xScaler); 107 float yPos = height - ((yData - yMin) * yScaler); 108 if (i > 1) { 109 // Each line segment requires 4 values! 110 mVertices[vertexIndex++] = previousX; 111 mVertices[vertexIndex++] = previousY; 112 mVertices[vertexIndex++] = xPos; 113 mVertices[vertexIndex++] = yPos; 114 } 115 previousX = xPos; 116 previousY = yPos; 117 } 118 canvas.drawLines(mVertices, 0, vertexIndex, trace.paint); 119 } 120 getXData(int i)121 public float getXData(int i) { 122 return mXData.get(i); 123 } 124 createTrace(String name, int color, float min, float max)125 public MultiLineChart.Trace createTrace(String name, int color, float min, float max) { 126 Trace trace = new Trace(name, color, NUM_DATA_VALUES, min, max); 127 mTraceList.add(trace); 128 return trace; 129 } 130 update()131 public void update() { 132 post(new Runnable() { 133 public void run() { 134 postInvalidate(); 135 } 136 }); 137 } 138 addX(float value)139 public void addX(float value) { 140 mXData.add(value); 141 } 142 reset()143 public void reset() { 144 mXData.clear(); 145 for (Trace trace : mTraceList) { 146 trace.reset(); 147 } 148 } 149 150 public static class Trace { 151 private final String mName; 152 public Paint paint; 153 protected float mMin; 154 protected float mMax; 155 protected CircularFloatArray mData; 156 Trace(String name, int color, int numValues, float min, float max)157 private Trace(String name, int color, int numValues, float min, float max) { 158 mName = name; 159 mMin = min; 160 mMax = max; 161 mData = new CircularFloatArray(numValues); 162 163 paint = new Paint(Paint.ANTI_ALIAS_FLAG); 164 paint.setColor(color); 165 paint.setStrokeWidth(3.0f); 166 } 167 reset()168 public void reset() { 169 mData.clear(); 170 } 171 add(float value)172 public void add(float value) { 173 mData.add(value); 174 // Take the hit here instead of when drawing. 175 mData.add(Math.min(mMax, Math.max(mMin, value))); 176 } 177 size()178 public int size() { 179 return mData.size(); 180 } 181 182 /** 183 * Fetch a previous value. A delayIndex of 1 will return the most recently written value. 184 * A delayIndex of 2 will return the previously written value; 185 * @param delayIndex positive index of previously written data 186 * @return old value 187 */ get(int delayIndex)188 public float get(int delayIndex) { 189 return mData.get(delayIndex); 190 } getMax()191 public float getMax() { 192 return mMax; 193 } getMin()194 public float getMin() { 195 return mMin; 196 } 197 setMin(float min)198 public void setMin(float min) { 199 mMin = min; 200 } setMax(float max)201 public void setMax(float max) { 202 mMax = max; 203 } 204 } 205 206 // Use explicit type for performance reasons. 207 private static class CircularFloatArray { 208 private float[] mData; 209 private int mIndexMask; 210 private int mCursor; // next location to be written 211 CircularFloatArray(int numValuesPowerOf2)212 public CircularFloatArray(int numValuesPowerOf2) { 213 if ((numValuesPowerOf2 & (numValuesPowerOf2 - 1)) != 0) { 214 throw new IllegalArgumentException("numValuesPowerOf2 not 2^N, was " + numValuesPowerOf2); 215 } 216 mData = new float[numValuesPowerOf2]; 217 mIndexMask = numValuesPowerOf2 - 1; 218 } 219 220 /** 221 * Add one value to the array. 222 * This may overwrite the oldest data. 223 * @param value 224 */ add(float value)225 public void add(float value) { 226 int index = mCursor & mIndexMask; 227 mData[index] = value; 228 mCursor++; 229 } 230 231 /** 232 * Number of valid entries. 233 * @return 234 */ size()235 public int size() { 236 return Math.min(mCursor, mData.length); 237 } 238 239 /** 240 * Fetch a previous value. A delayIndex of 1 will return the most recently written value. 241 * A delayIndex of 2 will return the previously written value; 242 * @param delayIndex positive index of previously written data 243 * @return old value 244 */ get(int delayIndex)245 public float get(int delayIndex) { 246 int index = (mCursor - delayIndex) & mIndexMask; 247 return mData[index]; 248 } 249 clear()250 public void clear() { 251 mCursor = 0; 252 } 253 } 254 }