I have written a custom view that shows a simulated oscilloscope, it essentially holds a series of points to plot, and periodically places a pulse (a separate set of points) into the main series. The only problem is that I'd like to update the points at the correct rate of 25mm/sec on one plot and 4mm/sec on another.
setRate() is called before the view is drawn. updateData() is called from a thread that runs a loop at 60fps with SystemClock.elapsedRealtime(). The problem is that pxToDraw is not the right value to keep it at the rate I'd like.
Here is my code
static class FakePlot extends OscView.Plot {
public void setRate(int width, DisplayMetrics dm) {
float xdpi = dm.xdpi;
mWidthMm = width/ xdpi * 25.4f;
pxPerSec =TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, mScanRateMM, dm);
pxPerTick = ((float)width) / mData.length;
lastPulse = SystemClock.elapsedRealtime();
Log.e(TAG,String.format("setRate() %s: pxPerSec=%f pxPerTick=%f",mTitle,pxPerSec,pxPerTick));
}
/**
* Copy the next set of points along
* #param realTime
*/
public void updateData(long realTime) {
long diff = realTime - lastUpdate;
lastUpdate = realTime;
float pxToDraw = ((diff/1000f)/pxPerSec);
float ticksToDraw = pxToDraw/(1f/pxPerTick);
for (int j = 0; j < ticksToDraw; j++){
// update mData with the next ticksToDrawPoints
}
setChanged();
notifyObservers();
}
}
A full working example (without the correct scan rate is here: http://pastebin.com/nHBxumhV)
The correct code is:
float extraToDraw;
public void updateData(long realTime) {
float mmPerTick = mWidthMm/mData.length;
float mmToDraw = (mScanRateMM*diffSec)+extraToDraw;
if(mmToDraw < 1) {extraToDraw = mmToDraw;return;}
extraToDraw = 0;
float drawn = 0;
while(drawn < mmToDraw) {
drawn+= mmPerTick;
// draw the next tick
}
}
Related
I have two tables in my game. When I tap on one the cells of the first table, I want to draw an arrow that follows my finger so that I can move the arrow head to a table cell from the second table. I need to be able to know which was the initial cell and which was the final cell (where the arrow started and where the arrow finished). If the user takes the finger off the screen and it wasn't on a table cell I want nothing to happen.
ArrayList<Actor> myActor = new ArrayList<Actor>();
ArrayList<Actor> anotherActor = new ArrayList<Actor>();
// fill array with my actors
Table firstTable = new Table();
for (int i = 0; i < 7; i++) {
firstTable.add(myActor.get(i));
myActor.get(i).addListener(new ActorGestureListener() {
public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
// draw arrow that follows finger
}
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
// if event finished and was on a cell from the
// second table get starting actor and finishing actor
// else nothing happens
}
});
}
Table secondTable = new Table();
for (int i = 0; i < 7; i++) {
secondTable.add(anotherActor.get(i));
}
How should I do this?
Hope this helps.
Actor startFromActor = null; // Variable outside of method
public void foo() {
// smt before
ArrayList<Actor> myActor = new ArrayList<Actor>();
ArrayList<Actor> anotherActor = new ArrayList<Actor>();
// fill array with my actors
Table firstTable = new Table();
Table secondTable = new Table();
for (int i = 0; i < 7; i++) {
final Actor curActor = myActor.get(i);
firstTable.add(curActor);
curActor.addListener(new ActorGestureListener() {
public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
if (pointer > 0) return false; // For only one finger
startFromActor = curActor;
// draw arrow that follows finger
}
});
}
for (int i = 0; i < 7; i++) {
final Actor curActor = anotherActor.get(i);
secondTable.add(curActor);
curActor.addListener(new ActorGestureListener() {
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
if (pointer > 0) return false; // For only one finger
if (startFromActor != null) {
// do something
// startActor - actor from first table
// curActor - actor from second table
startFromActor = null; // do not forget to null-setting
}
}
});
}
}
If you need both directions from table to table, you should set touchUp and touchDown events for every Actor and then check who is owner for start and end actors;
You can draw an arrow using the built in Mesh utilities in libgdx:
Build a Mesh at init
It is suggested to only build one mesh ( usually at init ) and then re-use ( render ) it during runtime as many times as many arrows the application requires. For the urposes of building the mesh, the Meshbuilder and ArrowShapeBuilder can be used. Example:
MeshBuilder meshbuilder = new MeshBuilder();
meshbuilder.begin(VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal, GL20.GL_TRIANGLES);
ArrowShapeBuilder.build(
meshbuilder,
0,0,0 /* Starting xyz */
5,5,0 /* Ending xyz */
0.1f, /* percentage of arrow head */
0.1f, /* percentage of stem thickness */
10 /* divisions, basically level of detail */
);
Mesh debug_arrow = meshbuilder.end();
By the above a renderable mesh is constructed.
Render the arrow to your app
This step is quite simple, provided a Batch is already available :
debug_arrow.render(batch.getShader(), GL20.GL_TRIANGLES);
The position of the arrow is then being set by the stage projection matrix. A Batch should be available, the very least the rendering stage has one through getBatch().
Note: The mesh parameter musn't always be GL20.GL_TRIANGLES, but the parameters must match in meshbuilder.begin and debug_arrow.render.
So I am using the libgdx framework and am new to android games development. I have an array of yellow circle objects stored in each index which are shown on the game screen when I run them, all the circle objects are in different x positions but on the same y axis. I want to basically set the visibility of each circle for a given amount of time before the next one becomes visible for say 1000 ms per circle. So for example circle 1 will be visible for 1000 ms then it will become invisible and circle 2 will then become visible for 1000ms so on and so forth, till I reach the end of the list.
public class Spot {
private float x;
private float y;
private float radius;
private Circle spot;
private Circle spotList[];
public Spot(float x, float y, float radius, int amount){
this.x = x;
this.y = y;
this.radius = radius;
spot = new Circle(x,y,radius);
spotList = new Circle[amount];
for(int i = 0; i < spotList.length; i++){
spotList[i] = new Circle(this.x+(i*15), this.y, this.radius);
}
}
public void update(float delta){
}
public Float getX(){
return x;
}
public Float getY(){
return y;
}
public float getRadius(){
return radius;
}
public Circle[] getSpots(){
return spotList;
}
public Circle getSpot(){
return spot;
}
}
The rendering of the shape has been outsourced to a different class I want to be able to handle the visibility of the circles etc in the update method.
Since you're not using Actors (https://github.com/libgdx/libgdx/wiki/Scene2d), you need to keep track of time yourself and make the circles visible/not visible.
You will want to add
private float elapsedTime = 0.0f;
private float currentCircle = 0;
two fields, one for keeping track of elapsed time and one for the currently visible circle.
public void update(float delta){
elapsedTime += delta;
if (elapsedTime >= 1.0f) { // more than one second passed
spot[currentCircle].setVisible(false);
if (currentCircle + 1 < spotList.size()) {
currentCircle++;
spot[currentCircle].setVisible(true);
elapsedTime -= 1.0f; // reset timer (don't just set to 0.0f to avoid time shifts)
}
}
}
In the update method, count elapsedTime and if more then one seconds passed, make old circle not visible and new circle visible. Also make sure that initially the first circle is visible and that the timer is 0.
A possible solution is to use the Universal Tween Engine which works well with the libGDX framework. Visit the link to see how to include it in your project and for documentation.
As a quick demonstration on how to use it:-
Create and instantiate a TweenManager and your array of Spots in your class responsible for rendering.
protected TweenManager tweenManager = new TweenManager();
Spot spots[] = new Spot[...];
...
Create your timer by implementing the TweenCallback()
final int numberOfSpots = 5;
int spotArrayIndex = 0;
float timeDelay = 1000f; /*1000 milliseconds*/
Tween.call(new TweenCallback() {
#Override
public void onEvent(int type, BaseTween<?> source) {
/*set or unset the visibility of your Spot objects here i.e. */
spots[spotArrayIndex++].setVisible(false); /*Set current spot invisible*/
spots[spotArrayIndex].setVisible(true); /*Set next spot to be visible*/
}
}).repeat(numberOfSpots, timeDelay).start(tweenManager);
Update the tweenManager in your render method
public void render(float delta) {
...
tweenManger.update(delta);
...
}
Please check the code for errors if you use it, as I am not sure of the rest of your code.
I am developing some application like Runtastic Pedometer using the algorithm but I am not getting any similarity between the results.
my code is as follows:
public void onSensorChanged(SensorEvent event)
{
Sensor sensor = event.sensor;
synchronized (this)
{
if (sensor.getType() == Sensor.TYPE_ORIENTATION) {}
else {
int j = (sensor.getType() == Sensor.TYPE_ACCELEROMETER) ? 1 : 0;
if (j == 1) {
float vSum = 0;
for (int i=0 ; i<3 ; i++) {
final float v = mYOffset + event.values[i] * mScale[j];
vSum += v;
}
int k = 0;
float v = vSum / 3;
//Log.e("data", "data"+v);
float direction = (v > mLastValues[k] ? 1 : (v < mLastValues[k] ? -1 : 0));
if (direction == - mLastDirections[k]) {
// Direction changed
int extType = (direction > 0 ? 0 : 1); // minumum or maximum?
mLastExtremes[extType][k] = mLastValues[k];
float diff = Math.abs(mLastExtremes[extType][k] - mLastExtremes[1 - extType][k]);
if (diff > mLimit) {
boolean isAlmostAsLargeAsPrevious = diff > (mLastDiff[k]*2/3);
boolean isPreviousLargeEnough = mLastDiff[k] > (diff/3);
boolean isNotContra = (mLastMatch != 1 - extType);
if (isAlmostAsLargeAsPrevious && isPreviousLargeEnough && isNotContra) {
for (StepListener stepListener : mStepListeners) {
stepListener.onStep();
}
mLastMatch = extType;
}
else {
Log.i(TAG, "no step");
mLastMatch = -1;
}
}
mLastDiff[k] = diff;
}
mLastDirections[k] = direction;
mLastValues[k] = v;
}
}
}
}
for registering sensors:
mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
mSensor = mSensorManager.getDefaultSensor(
Sensor.TYPE_ACCELEROMETER);
mSensorManager.registerListener(mStepDetector,mSensor,SensorManager.SENSOR_DELAY_NORMAL);
in the algorithm i have different levels for sensitivity as public void
setSensitivity(float sensitivity) {
mLimit = sensitivity; // 1.97 2.96 4.44 6.66 10.00 15.00 22.50 33.75 50.62
}
on various sensitivity level my result is:
sensitivity rantastic pedometer my app
10.00 3870 5500
11.00 3000 4000
11.15 3765 4576
13.00 2000 890
11.30 754 986
I am not getting any proper pattern to match with the requirement.
As per my analysis this application is using Sensor.TYPE_MAGNETIC_FIELD for steps calculation please let me know some algorithm so that I can meet with the requirement.
The first thing you need to do is decide on an algorithm. As far as I know there are roughly speaking three ways to detect steps using accelerometers that are described in the literature:
Use the Pythagorean theorem to calculate the magnitude of the acceleration vector of each sample from the accelerometer. Low-pass filter the magnitude signal to remove high frequency noise and then look for peaks and valleys in the filtered signal. You may need to add additional requirements to remove false positives. This is by far the simplest way to detect steps, it is also the way that most if not all ordinary pedometers of the sort that you can buy from a sports store work.
Use Pythagoras' like in (1), then run the signal through an FFT and compare the output from the FFT to known outputs of walking. This requires you to have access to a fairly large amount of training data.
Feed the accelerometer data into an algorithm that uses some suitable machine learning technique, for example a neural network or a digital wavelet transform. You can of course include other sensors in this approach. This also requires you to have access to a fairly large amount of training data.
Once you have decided on an algorithm you will probably want to use something like Matlab or SciPy to test your algorithm on your computer using recordings that you have made on Android phones. Dump accelerometer data to a cvs file on your phone, make a record of how many steps the file represents, copy the file to your computer and run your algorithm on the data to see if it gets the step count right. That way you can detect problems with the algorithm and correct them.
If this sounds difficult, then the best way to get access to good step detection is probably to wait until more phones come with the built-in step counter that KitKat enables.
https://github.com/bagilevi/android-pedometer
i hope this might be helpfull
I am using step detection in my walking instrument.
I get nice results of step detection.
I use achartengine to plot accelerometer data.
Take a look here.
What I do:
Analysis of magnitude vector for accelerometer sensor.
Setting a changeable threshold level. When signal from accelerometer is above it I count it as a step.
Setting the time of inactive state (for step detection) after first crossing of the threshold.
Point 3. is calculated:
arbitrary setting the maximum tempo of our walking (e.g. 120bpm)
if 60bpm - 1000msec per step, then 120bpm - 500msec per step
accelerometer passes data with certain desired frequency (SENSOR_DELAY_NORMAL, SENSOR_DELAY_GAME, etc.). When DELAY_GAME: T ~= 20ms (this is included in Android documentation)
n - samples to omit (after passing the threshold)
n = 500msec / T
n = 500 / 20 = 25 (plenty of them. You can adjust this value).
after that, the threshold becomes active.
Take a look at this picture:
This is my realization. It was written about 1.5-2 years ago. And I really don't remember all this stuff that I wrote. But it worked. And it worked good for my needs.
I know that this is really big class (some methods are deleted), but may be it will be helpful. If not, I'll just remove this answer...
public class StepDetector implements SensorEventListener
{
public static final int MAX_BUFFER_SIZE = 5;
private static final int Y_DATA_COUNT = 4;
private static final double MIN_GRAVITY = 2;
private static final double MAX_GRAVITY = 1200;
public void onSensorChanged(final SensorEvent sensorEvent)
{
final float[] values = sensorEvent.values;
final Sensor sensor = sensorEvent.sensor;
if (sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
{
magneticDetector(values, sensorEvent.timestamp / (500 * 10 ^ 6l));
}
if (sensor.getType() == Sensor.TYPE_ACCELEROMETER)
{
accelDetector(values, sensorEvent.timestamp / (500 * 10 ^ 6l));
}
}
private ArrayList<float[]> mAccelDataBuffer = new ArrayList<float[]>();
private ArrayList<Long> mMagneticFireData = new ArrayList<Long>();
private Long mLastStepTime = null;
private ArrayList<Pair> mAccelFireData = new ArrayList<Pair>();
private void accelDetector(float[] detectedValues, long timeStamp)
{
float[] currentValues = new float[3];
for (int i = 0; i < currentValues.length; ++i)
{
currentValues[i] = detectedValues[i];
}
mAccelDataBuffer.add(currentValues);
if (mAccelDataBuffer.size() > StepDetector.MAX_BUFFER_SIZE)
{
double avgGravity = 0;
for (float[] values : mAccelDataBuffer)
{
avgGravity += Math.abs(Math.sqrt(
values[0] * values[0] + values[1] * values[1] + values[2] * values[2]) - SensorManager.STANDARD_GRAVITY);
}
avgGravity /= mAccelDataBuffer.size();
if (avgGravity >= MIN_GRAVITY && avgGravity < MAX_GRAVITY)
{
mAccelFireData.add(new Pair(timeStamp, true));
}
else
{
mAccelFireData.add(new Pair(timeStamp, false));
}
if (mAccelFireData.size() >= Y_DATA_COUNT)
{
checkData(mAccelFireData, timeStamp);
mAccelFireData.remove(0);
}
mAccelDataBuffer.clear();
}
}
private void checkData(ArrayList<Pair> accelFireData, long timeStamp)
{
boolean stepAlreadyDetected = false;
Iterator<Pair> iterator = accelFireData.iterator();
while (iterator.hasNext() && !stepAlreadyDetected)
{
stepAlreadyDetected = iterator.next().first.equals(mLastStepTime);
}
if (!stepAlreadyDetected)
{
int firstPosition = Collections.binarySearch(mMagneticFireData, accelFireData.get(0).first);
int secondPosition = Collections
.binarySearch(mMagneticFireData, accelFireData.get(accelFireData.size() - 1).first - 1);
if (firstPosition > 0 || secondPosition > 0 || firstPosition != secondPosition)
{
if (firstPosition < 0)
{
firstPosition = -firstPosition - 1;
}
if (firstPosition < mMagneticFireData.size() && firstPosition > 0)
{
mMagneticFireData = new ArrayList<Long>(
mMagneticFireData.subList(firstPosition - 1, mMagneticFireData.size()));
}
iterator = accelFireData.iterator();
while (iterator.hasNext())
{
if (iterator.next().second)
{
mLastStepTime = timeStamp;
accelFireData.remove(accelFireData.size() - 1);
accelFireData.add(new Pair(timeStamp, false));
onStep();
break;
}
}
}
}
}
private float mLastDirections;
private float mLastValues;
private float mLastExtremes[] = new float[2];
private Integer mLastType;
private ArrayList<Float> mMagneticDataBuffer = new ArrayList<Float>();
private void magneticDetector(float[] values, long timeStamp)
{
mMagneticDataBuffer.add(values[2]);
if (mMagneticDataBuffer.size() > StepDetector.MAX_BUFFER_SIZE)
{
float avg = 0;
for (int i = 0; i < mMagneticDataBuffer.size(); ++i)
{
avg += mMagneticDataBuffer.get(i);
}
avg /= mMagneticDataBuffer.size();
float direction = (avg > mLastValues ? 1 : (avg < mLastValues ? -1 : 0));
if (direction == -mLastDirections)
{
// Direction changed
int extType = (direction > 0 ? 0 : 1); // minumum or maximum?
mLastExtremes[extType] = mLastValues;
float diff = Math.abs(mLastExtremes[extType] - mLastExtremes[1 - extType]);
if (diff > 8 && (null == mLastType || mLastType != extType))
{
mLastType = extType;
mMagneticFireData.add(timeStamp);
}
}
mLastDirections = direction;
mLastValues = avg;
mMagneticDataBuffer.clear();
}
}
public static class Pair implements Serializable
{
Long first;
boolean second;
public Pair(long first, boolean second)
{
this.first = first;
this.second = second;
}
#Override
public boolean equals(Object o)
{
if (o instanceof Pair)
{
return first.equals(((Pair) o).first);
}
return false;
}
}
}
One main difference I spotted between your implementation and the code in the grepcode project is the way you register the listener.
Your code:
mSensorManager.registerListener(mStepDetector,
mSensor,
SensorManager.SENSOR_DELAY_NORMAL);
Their code:
mSensorManager.registerListener(mStepDetector,
mSensor,
SensorManager.SENSOR_DELAY_FASTEST);
This is a big difference. SENSOR_DELAY_NORMAL is intended for orientation changes, and is therefor not that fast (ever noticed that it takes some time between you rotating the device, and the device actually rotating? That's because this is some functionality that does not need to be super fast (that would probably be pretty annoying even). The rate at which you get updates is not that high).
On the other hand, SENSOR_DELAY_FASTEST is intended for things like pedometers: you want the sensor data as fast and often as possible, so your calculations of steps will be as accurate as possible.
Try to switch to the SENSOR_DELAY_FASTEST rate, and test again! It should make a big difference.
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType()==Sensor.TYPE_ACCELEROMETER ){
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
currentvectorSum = (x*x + y*y + z*z);
if(currentvectorSum < 100 && inStep==false){
inStep = true;
}
if(currentvectorSum > 125 && inStep==true){
inStep = false;
numSteps++;
Log.d("TAG_ACCELEROMETER", "\t" + numSteps);
}
}
}
[UPDATE]
To conclude this question, I implemented my graph using the following two methods (see below). drawCurve() receives a Canvas and an array of float. The array is properly filled (timestamps are assumed by the value index in the array) and varies from 0.0 to 1.0. The array is sent to prepareWindowArray() that takes a chunk of the array from position windowStart for windowSize-values, in a circular manner.
The array used by the GraphView and by the data provider (a Bluetooth device) is the same. A Class in the middle ensures that GraphView is not reading data that are being written by the Bluetooth device. Since the GraphView always loop thru the array and redraw it at every iteration, it will update according to the data written by the Bluetooth device, and by forcing the write frequency of the Bluetooth device to the refresh frequency of the Graph, I obtain a smooth animation of my signal.
The GraphView's invalidate() method is called by the Activity, which run a Timer to refresh the graph at every x milliseconds. The frequency at which the graph is refreshed is dynamically set, so that it adapt to the flow of data from the Bluetooth device (which specify the frequency of its signal in the header of its packet).
Find the complete code of my GraphView in the answer I wrote below (in the answer section). If you guys find errors or way to optimize it, please let me know; it would be greatly appreciated!
/**
* 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);
}
The prepareWindowArray() method that implement the circular behavior of the array:
/**
* 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]
I'm making an app that read data from a Bluetooth device at a fixed rate. Everytime that I have new data, I want them to be plotted on the graph to the right, and to translate the remainder of the graph to the left in realtime. Basically, like an oscilloscope would do.
So I made a custom View, with xy axis, a title and units. To do this, I simply draw those things on the View canvas. Now I want to draw the curve. I manage to draw a static curve from an already filled array using this method:
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);
}
It gives me something like this.
There are still things to fix on my graph (the sub-axis are not properly scaling), but these are details I can fix later.
Now I want to change this static graph (that receives a non-dynamic matrice of values) with something dynamic that would redraw the curve every 40ms, pushing the old data to the left and plotting the new data to the right, so I could visualise in real time the information provided by the Bluetooth device.
I know there are some graphing package that exists already, but I'm kinda noob with these things and I'd like to pratice by implementing this graph myself. Also, most of my GraphView class is done, except for the curve part.
Second question, I'm wondering how I should send the new values to the graph. Should I use something like a FIFO stack, or can I achieve what I want with a simple matrice of doubles?
On a side note, the 4 fields at the bottom are already dynamically updated. Well, they are kind of faking the "dynamic", they loop thru the same double matrice again and again, they don't actually take fresh values.
Thanks for your time! If something's unclear about my question, let me know and I'll update it with more details.
As mentioned in my question, here's the class that I designed to solve my problems.
/**
* 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;
}
}
Well I would start by just trying to redraw it all with the code you have and real dynalic data. Only if that is not quick enough do you need to try anything fancy like scrolling...
If you need fancy I would try somthing like this.
I would draw the dynamic part of the graph into a secondary Bitmap that you keep between frames rather than directly to the canves. I would have the background none dynamic part of the graph in another bitmap that only gets drawen on rescale etc.
In this secondary dynamic bitmap when ploting new data you first need to clear the old data you are replacing you do this by drawing the apropriate slice of the static background bitmap over the top of the stale data, thus clearing it and geting the background nice and fresh again. You then just need to draw your new bit of dynamic data. The trick is that You draw into this second bitmap left to right then just wrap back to the left at the end and start over.
To get from the soncodary bitmap to your cancas draw the bitmap to the canvas in two parts. The older data to the right of what you just added needs to be drawn onto the left part of your final canvas and the new data needs to be drawn imediatly to the right of it.
For sending the data a circular buffer would be the normal thing for this sort of data where once it's off the graph you don't care about it.
I've been trying to find a listener that can execute seekmark.markPlace(). The function takes in the dimensions and padding for a seekBar defined in my layout. When I call the function from onCreate it returns 0 for the values of the seekBar. I need a listener that can call the function outside of onCreate. The function should only run if the screen is rotated (when layout-land is created).
public class seekMark {
private int seekLength; // in pixels
private int seekLeftPad; // in pixels
private int seekBottomPad; // in pixels
private int trackLength; // in ms
public seekMark(){
seekLength = progressBar.getWidth();
seekLeftPad = progressBar.getPaddingLeft();
seekBottomPad = progressBar.getPaddingBottom();
trackLength = player.getDuration();
}
private int pxPerMs(){
return (seekLength/trackLength);
}
public void markPlace() throws XmlPullParserException, IOException {
Drawable marker = context.getResources().getDrawable(R.drawable.audio);
int y = seekBottomPad;
int x = 0;
int w = marker.getIntrinsicWidth();
int h = marker.getIntrinsicHeight();
int[] bmPos = markPxList(); // returns a list of px (from the left) read from a xml database
for(int i = 0; i <= bmPos.length; i++){
x = bmPos[i] + seekLeftPad;
marker.setBounds( x, y, x + w, y + h );
marker.draw( canvas );
}
}
}
Is it possible to make your own listener like onRotate() or something?
Since onCreate is going to be called when the device is rotated anyways, why not just use a private variable to store the orientation of the display. Each time onCreate() is called, you can compare to see if the display orientation is changed:
Display display = ((WindowManager) getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
int rotation = display.getRotation();
if(rotation != mPreviousRotation) {
mPreviousRotation = rotation;
// Insert code for handling rotation here
}
I'll leave it to you to decide how to handle initialization of the mPreviousRotation variable during the first invocation of onCreate().