added moved files
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
package de.tonifetzer.conductorssensor.utilities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by toni on 12/01/18.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class: DataFolder
|
||||||
|
* Description: SDK save file class. Is able to open a folder on the device independent of the given
|
||||||
|
* device and android skd version.
|
||||||
|
*/
|
||||||
|
public class DataFolder {
|
||||||
|
|
||||||
|
private File folder;
|
||||||
|
private static final String TAG = "DataFolder";
|
||||||
|
|
||||||
|
public DataFolder(Context context, String folderName){
|
||||||
|
|
||||||
|
// 1) try external data folder
|
||||||
|
folder = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), folderName);
|
||||||
|
if (isOK(folder)) {return;}
|
||||||
|
|
||||||
|
// 2) try sd-card folder
|
||||||
|
folder = new File(Environment.getExternalStorageDirectory() + "/" + folderName);
|
||||||
|
if (isOK(folder)) {return;}
|
||||||
|
|
||||||
|
// 3) try internal data folder
|
||||||
|
folder = new File(context.getApplicationInfo().dataDir);
|
||||||
|
if (isOK(folder)) {return;}
|
||||||
|
|
||||||
|
// all failed
|
||||||
|
throw new RuntimeException("failed to create/access storage folder");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ensure the given folder is OK */
|
||||||
|
private static final boolean isOK(final File folder) {
|
||||||
|
folder.mkdirs();
|
||||||
|
final boolean ok = folder.exists() && folder.isDirectory();
|
||||||
|
if (ok) {
|
||||||
|
Log.d(TAG, "using: " + folder);
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "not OK: " + folder);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getFolder(){
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package de.tonifetzer.conductorssensor.utilities;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import de.tonifetzer.conductorssensor.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by toni on 14/01/18.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class SensorDataFileWriter {
|
||||||
|
|
||||||
|
private static final int FLUSH_LIMIT = 2*1024*1024;
|
||||||
|
private static final String TAG = "SensorDataFileWriter";
|
||||||
|
|
||||||
|
private File mSensorDataFile;
|
||||||
|
private FileOutputStream mFileOutputStream;
|
||||||
|
private StringBuilder mStringBuilder = new StringBuilder();
|
||||||
|
private final DataFolder mFolder;
|
||||||
|
private boolean mStreamOpenend = false;
|
||||||
|
|
||||||
|
public SensorDataFileWriter(Context context) {
|
||||||
|
|
||||||
|
mFolder = new DataFolder(context, "sensorOutFiles");
|
||||||
|
|
||||||
|
//write some description for this file in the first line
|
||||||
|
EditText editView = ((Activity) context).findViewById(R.id.comments);
|
||||||
|
TextView textView = ((Activity) context).findViewById(R.id.bpmText);
|
||||||
|
writeDescription("Kommentar: " + editView.getText().toString() + "\nMetronom: " + textView.getText().toString() + " bpm");
|
||||||
|
|
||||||
|
//open connection to file
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void writeVector3D(long ts, int id, double x, double y, double z){
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
|
||||||
|
if (mStreamOpenend) {
|
||||||
|
|
||||||
|
mStringBuilder.append(ts).append(';').append(id).append(';').append(x).append(';').append(y).append(';').append(z).append('\n');
|
||||||
|
|
||||||
|
if (mStringBuilder.length() > FLUSH_LIMIT) {flush(false);}
|
||||||
|
} else {
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeDescription(String str){
|
||||||
|
synchronized (this) {
|
||||||
|
mStringBuilder.append(str).append('\n').append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush data to disk
|
||||||
|
public final void toDisk(){
|
||||||
|
synchronized (this) {
|
||||||
|
flush(true);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** helper method for exception-less writing. DO NOT CALL DIRECTLY! */
|
||||||
|
private void _write(final byte[] data) {
|
||||||
|
try {
|
||||||
|
mFileOutputStream.write(data);
|
||||||
|
Log.d(TAG, "flushed " + data.length + " bytes to disk");
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new RuntimeException("error while writing log-file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** helper-class for background writing */
|
||||||
|
class FlushAsync extends AsyncTask<byte[], Integer, Integer> {
|
||||||
|
@Override
|
||||||
|
protected final Integer doInBackground(byte[][] data) {
|
||||||
|
_write(data[0]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** flush current buffer-contents to disk */
|
||||||
|
private void flush(boolean sync) {
|
||||||
|
|
||||||
|
// fetch current buffer contents to write and hereafter empty the buffer
|
||||||
|
// this action MUST be atomic, just like the add-method
|
||||||
|
byte[] data = null;
|
||||||
|
synchronized (this) {
|
||||||
|
data = mStringBuilder.toString().getBytes(); // fetch data to write
|
||||||
|
mStringBuilder.setLength(0); // reset the buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// write
|
||||||
|
if (sync) {
|
||||||
|
// write to disk using the current thread
|
||||||
|
_write(data);
|
||||||
|
} else {
|
||||||
|
// write to disk using a background-thread
|
||||||
|
new FlushAsync().execute(new byte[][] {data});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void open() {
|
||||||
|
mSensorDataFile = new File(mFolder.getFolder(), System.currentTimeMillis() + ".csv");
|
||||||
|
|
||||||
|
try {
|
||||||
|
mFileOutputStream = new FileOutputStream(mSensorDataFile);
|
||||||
|
mStreamOpenend = true;
|
||||||
|
|
||||||
|
Log.d(TAG, "will write to: " + mSensorDataFile.toString());
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
mStreamOpenend = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close() {
|
||||||
|
try {
|
||||||
|
mFileOutputStream.close();
|
||||||
|
mStreamOpenend = false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package de.tonifetzer.conductorswatch.bpmEstimation;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.hardware.Sensor;
|
||||||
|
import android.hardware.SensorEvent;
|
||||||
|
import android.hardware.SensorEventListener;
|
||||||
|
import android.hardware.SensorManager;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
import de.tonifetzer.conductorswatch.network.SensorDataFileStreamer;
|
||||||
|
import de.tonifetzer.conductorswatch.utilities.ByteStreamWriter;
|
||||||
|
import de.tonifetzer.conductorswatch.utilities.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by toni on 13/11/17.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class Estimator implements SensorEventListener {
|
||||||
|
|
||||||
|
private SensorManager mSensorManager;
|
||||||
|
private Sensor mAccelerometer;
|
||||||
|
private Sensor mRawAccelerometer;
|
||||||
|
private Context mContext;
|
||||||
|
private AccelerometerWindowBuffer mAccelerometerWindowBuffer;
|
||||||
|
private EstimatorAutoCorr mBpmEstimator;
|
||||||
|
private double mCurrentBpm;
|
||||||
|
|
||||||
|
private ByteStreamWriter mByteStreamWriterAcc;
|
||||||
|
private ByteStreamWriter mByteStreamWriterGyro;
|
||||||
|
private SensorDataFileStreamer mStreamer;
|
||||||
|
|
||||||
|
private Timer mTimer = new Timer();
|
||||||
|
|
||||||
|
|
||||||
|
public Estimator(Context mContext){
|
||||||
|
this.mContext = mContext;
|
||||||
|
this.mCurrentBpm = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
|
||||||
|
mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
|
||||||
|
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
|
||||||
|
mRawAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
||||||
|
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_FASTEST);
|
||||||
|
mSensorManager.registerListener(this, mRawAccelerometer, SensorManager.SENSOR_DELAY_FASTEST);
|
||||||
|
|
||||||
|
|
||||||
|
mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 750);
|
||||||
|
mBpmEstimator = new EstimatorAutoCorr(mAccelerometerWindowBuffer, 0, 5000);
|
||||||
|
|
||||||
|
mTimer.scheduleAtFixedRate(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
if (mAccelerometerWindowBuffer.isNextWindowReady()) {
|
||||||
|
|
||||||
|
LinkedList<Double> bpmList = new LinkedList<>();
|
||||||
|
|
||||||
|
//todo: wie viele dieser Klassen kann ich wegwerfen um das Ergebnis nich schlechter zu machen?
|
||||||
|
double bpm60 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow());
|
||||||
|
double bpm85 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(3500, 750));
|
||||||
|
double bpm110 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(2600, 750));
|
||||||
|
double bpm135 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(2000, 750));
|
||||||
|
double bpm160 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(1600,750));
|
||||||
|
double bpm200 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(1200, 750));
|
||||||
|
|
||||||
|
//add to list
|
||||||
|
//todo: make this cool...
|
||||||
|
//vielleicht einen weighted mean?
|
||||||
|
bpmList.add(bpm60);
|
||||||
|
bpmList.add(bpm85);
|
||||||
|
bpmList.add(bpm110); //110, 135, 160 langen auch schon
|
||||||
|
bpmList.add(bpm135);
|
||||||
|
bpmList.add(bpm160);
|
||||||
|
bpmList.add(bpm200);
|
||||||
|
|
||||||
|
//Log.d("BPM: ", bpmList.toString());
|
||||||
|
|
||||||
|
//remove all -1 and calc bpmMean
|
||||||
|
while(bpmList.remove(Double.valueOf(-1))) {}
|
||||||
|
|
||||||
|
//remove outliers
|
||||||
|
//todo: aktuell wird die liste hier sortiert.. eig net so schön.
|
||||||
|
Utils.removeOutliersZScore(bpmList, 3.4);
|
||||||
|
//Utils.removeOutliersHeuristic();
|
||||||
|
|
||||||
|
//Log.d("BPM: ", bpmList.toString());
|
||||||
|
|
||||||
|
double bpm = -1;
|
||||||
|
if(!bpmList.isEmpty()) {
|
||||||
|
double bpmMean = Utils.mean(bpmList);
|
||||||
|
double bpmMedian = Utils.median(bpmList);
|
||||||
|
|
||||||
|
double bpmDiffSlowFast = bpmList.getFirst() - bpmList.getLast();
|
||||||
|
if (Math.abs(bpmDiffSlowFast) > 25) {
|
||||||
|
|
||||||
|
double tmpBPM = bpmMean + 25;
|
||||||
|
|
||||||
|
while(bpm == -1){
|
||||||
|
if (tmpBPM < 60) {
|
||||||
|
bpm = bpm60;
|
||||||
|
if(bpm == -1){
|
||||||
|
bpm = bpmMean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (tmpBPM < 85) {
|
||||||
|
bpm = bpm85;
|
||||||
|
} else if (tmpBPM < 110) {
|
||||||
|
bpm = bpm110;
|
||||||
|
} else if (tmpBPM < 135) {
|
||||||
|
bpm = bpm135;
|
||||||
|
} else if (tmpBPM < 160) {
|
||||||
|
bpm = bpm160;
|
||||||
|
} else {
|
||||||
|
bpm = bpm200;
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpBPM -= 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Log.d("BPM: ", "CHANGE");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
bpm = bpmMean;
|
||||||
|
//Log.d("BPM: ", "STAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (OnBpmEstimatorListener listener : listeners) {
|
||||||
|
listener.onNewDataAvailable(bpm); //135 gibt gute ergebnisse!
|
||||||
|
}
|
||||||
|
|
||||||
|
//update the windowSize and updaterate depending on current bpm
|
||||||
|
//updateWindowSizeAndOverlap(bpm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0, 100);
|
||||||
|
|
||||||
|
mByteStreamWriterAcc = new ByteStreamWriter();
|
||||||
|
mByteStreamWriterGyro = new ByteStreamWriter();
|
||||||
|
|
||||||
|
mStreamer = new SensorDataFileStreamer(mContext);
|
||||||
|
new Thread(mStreamer).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
|
||||||
|
mTimer.cancel();
|
||||||
|
mTimer.purge();
|
||||||
|
|
||||||
|
mSensorManager.unregisterListener(this);
|
||||||
|
|
||||||
|
//send data and close the ByteStreamWriter
|
||||||
|
for (OnBpmEstimatorListener listener : listeners) {
|
||||||
|
//listener.onEstimationStopped(mByteStreamWriter.getByteArray());
|
||||||
|
}
|
||||||
|
mByteStreamWriterAcc.close();
|
||||||
|
mByteStreamWriterGyro.close();
|
||||||
|
|
||||||
|
mStreamer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSensorChanged(SensorEvent se) {
|
||||||
|
|
||||||
|
//TODO: at the moment this runs in main thread... since worker fragment runs also in main thread
|
||||||
|
|
||||||
|
//ca 200hz, every 5 to 6 ms we have an update
|
||||||
|
if (se.sensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION) {
|
||||||
|
mAccelerometerWindowBuffer.add(new AccelerometerData(System.currentTimeMillis(), se.values[0], se.values[1], se.values[2]));
|
||||||
|
mByteStreamWriterAcc.writeSensor3D(Sensor.TYPE_LINEAR_ACCELERATION, se.values[0], se.values[1], se.values[2]);
|
||||||
|
|
||||||
|
mStreamer.sendByteArray(mByteStreamWriterAcc.getByteArray());
|
||||||
|
mByteStreamWriterAcc.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (se.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||||
|
mByteStreamWriterGyro.writeSensor3D(Sensor.TYPE_ACCELEROMETER, se.values[0], se.values[1], se.values[2]);
|
||||||
|
|
||||||
|
mStreamer.sendByteArray(mByteStreamWriterGyro.getByteArray());
|
||||||
|
mByteStreamWriterGyro.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAccuracyChanged(Sensor sensor, int i) {
|
||||||
|
// do nothin
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for callback calculated bpm
|
||||||
|
*/
|
||||||
|
public interface OnBpmEstimatorListener {
|
||||||
|
void onNewDataAvailable(double bpm);
|
||||||
|
void onEstimationStopped(byte[] sensorData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<OnBpmEstimatorListener> listeners = new CopyOnWriteArrayList<OnBpmEstimatorListener>();
|
||||||
|
public void add(OnBpmEstimatorListener listener){listeners.add(listener);}
|
||||||
|
public void remove(OnBpmEstimatorListener listener){listeners.remove(listener);}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple function that sets die windowSize and Overlap time to a specific value
|
||||||
|
* depending on the currentBPM. Nothing rly dynamical. However, should work out
|
||||||
|
* for our purposes.
|
||||||
|
* @param bpm
|
||||||
|
*/
|
||||||
|
private void updateWindowSizeAndOverlap(double bpm){
|
||||||
|
|
||||||
|
//round to nearest tenner. this limits the number of windowsize and overlap changes.
|
||||||
|
int newBpmRounded = (int) Math.round(bpm / 10.0) * 10;
|
||||||
|
int curBpmRounded = (int) Math.round(mCurrentBpm / 10.0) * 10;
|
||||||
|
|
||||||
|
//TODO: i guess this is not the best method.. if the default sizes always produces -1, we run into problems
|
||||||
|
if(bpm != -1 && curBpmRounded != newBpmRounded){
|
||||||
|
|
||||||
|
int overlap_ms = 60000 / newBpmRounded;
|
||||||
|
int window_ms = overlap_ms * 5;
|
||||||
|
|
||||||
|
//idea: wenn man mehrere fenster parallel laufen lässt und beobachtet, müsste das kleinste fenster die tempowechsel eigentlich am
|
||||||
|
// besten mitbekommen. dieses fenster dann bestimmen lassen?
|
||||||
|
|
||||||
|
mAccelerometerWindowBuffer.setWindowSize(window_ms);
|
||||||
|
mAccelerometerWindowBuffer.setOverlapSize(overlap_ms);
|
||||||
|
|
||||||
|
} else if (bpm == -1){
|
||||||
|
//if bpm is -1 due to a non-classification, reset to default.
|
||||||
|
|
||||||
|
//idea: anstatt auf einen festen wert zu setzen, könnte man das fenster dann auch einfach ein wenig größer / kleiner machen.
|
||||||
|
mAccelerometerWindowBuffer.setWindowSize(3000);
|
||||||
|
mAccelerometerWindowBuffer.setOverlapSize(750);
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentBpm = bpm;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package de.tonifetzer.conductorswatch.bpmEstimation;
|
||||||
|
|
||||||
|
import de.tonifetzer.conductorswatch.utilities.MovingFilter;
|
||||||
|
import de.tonifetzer.conductorswatch.utilities.SimpleKalman;
|
||||||
|
import de.tonifetzer.conductorswatch.utilities.Utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by toni on 17/12/17.
|
||||||
|
*/
|
||||||
|
public class EstimatorAutoCorr {
|
||||||
|
|
||||||
|
private AccelerometerWindowBuffer mBuffer;
|
||||||
|
private double mSampleRate_ms;
|
||||||
|
|
||||||
|
private LinkedList<Double> mBpmHistory_X;
|
||||||
|
private LinkedList<Double> mBpmHistory_Y;
|
||||||
|
private LinkedList<Double> mBpmHistory_Z;
|
||||||
|
|
||||||
|
private LinkedList<Double> mBpmHistory;
|
||||||
|
private int mResetCounter;
|
||||||
|
private int mResetLimit_ms;
|
||||||
|
|
||||||
|
private MovingFilter mMvg;
|
||||||
|
//private SimpleKalman mKalman;
|
||||||
|
|
||||||
|
|
||||||
|
public EstimatorAutoCorr(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){
|
||||||
|
mBuffer = windowBuffer;
|
||||||
|
mSampleRate_ms = sampleRate_ms;
|
||||||
|
|
||||||
|
mBpmHistory_X = new LinkedList<>();
|
||||||
|
mBpmHistory_Y = new LinkedList<>();
|
||||||
|
mBpmHistory_Z = new LinkedList<>();
|
||||||
|
|
||||||
|
mBpmHistory = new LinkedList<>();
|
||||||
|
mResetCounter = 0;
|
||||||
|
mResetLimit_ms = resetAfter_ms;
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: this is to easy. since the new dyn. windowsize produces smaller update times, we need to consider something, that
|
||||||
|
//TODO: holds more values, if they are similar, an resets the history if not.
|
||||||
|
mMvg = new MovingFilter(2);
|
||||||
|
//mKalman = new SimpleKalman();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double estimate(AccelerometerWindowBuffer fixedWindow){
|
||||||
|
|
||||||
|
double sampleRate = mSampleRate_ms;
|
||||||
|
if(sampleRate <= 0){
|
||||||
|
sampleRate = Math.round(Utils.mean(Utils.diff(fixedWindow.getTs())));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sampleRate == 0){
|
||||||
|
int breakhere = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: ist doch käse das zu interpolieren, das mach ich ja für jedes mal estimation.
|
||||||
|
//TODO: eigentlich muss ich ja nur das größte interpolieren und dann supwindows nehmen.
|
||||||
|
AccelerometerInterpolator interp = new AccelerometerInterpolator(fixedWindow, sampleRate);
|
||||||
|
|
||||||
|
//are we conducting?
|
||||||
|
//just look at the newest 512 samples
|
||||||
|
//List<AccelerometerData> subBuffer = mBuffer.subList(mBuffer.size() - 512, mBuffer.size());
|
||||||
|
|
||||||
|
double[] xAutoCorr = new AutoCorrelation(interp.getX(), fixedWindow.size()).getCorr();
|
||||||
|
double[] yAutoCorr = new AutoCorrelation(interp.getY(), fixedWindow.size()).getCorr();
|
||||||
|
double[] zAutoCorr = new AutoCorrelation(interp.getZ(), fixedWindow.size()).getCorr();
|
||||||
|
|
||||||
|
|
||||||
|
//find a peak within range of 250 ms
|
||||||
|
int peakWidth = (int) Math.round(250 / sampleRate);
|
||||||
|
Peaks pX = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false);
|
||||||
|
Peaks pY = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false);
|
||||||
|
Peaks pZ = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false);
|
||||||
|
|
||||||
|
mBpmHistory_X.add(pX.getBPM(sampleRate));
|
||||||
|
mBpmHistory_Y.add(pY.getBPM(sampleRate));
|
||||||
|
mBpmHistory_Z.add(pZ.getBPM(sampleRate));
|
||||||
|
|
||||||
|
double estimatedBPM = getBestBpmEstimation(pX, pY, pZ);
|
||||||
|
if(estimatedBPM != -1){
|
||||||
|
|
||||||
|
//moving avg (lohnt dann, wenn wir viele daten haben)
|
||||||
|
mMvg.add(estimatedBPM);
|
||||||
|
mBpmHistory.add(mMvg.getAverage());
|
||||||
|
|
||||||
|
//mBpmHistory.add(estimatedBPM);
|
||||||
|
|
||||||
|
//moving median (lohnt nur bei konstantem tempo, da wir nur ein tempo damit gut halten können.)
|
||||||
|
//mMvg.add(estimatedBPM);
|
||||||
|
//mBpmHistory.add(mMvg.getMedian());
|
||||||
|
|
||||||
|
//kalman filter (lohnt dann, wenn wir konstantes tempo haben, mit startangabe!)
|
||||||
|
|
||||||
|
//standard last element
|
||||||
|
//mBpmHistory.add(estimatedBPM);
|
||||||
|
//mResetCounter = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int resetAfter = (int) Math.round((double) (mResetLimit_ms / (mBuffer.getOverlapSize())));
|
||||||
|
if(++mResetCounter > resetAfter){
|
||||||
|
mBpmHistory.clear();
|
||||||
|
|
||||||
|
//TODO: send signal to clear.
|
||||||
|
//mBuffer.clear();
|
||||||
|
mMvg.clear();
|
||||||
|
mResetCounter = 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//last element
|
||||||
|
return mBpmHistory.getLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getMeanBpm(){
|
||||||
|
return Utils.mean(mBpmHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getMedianBPM(){
|
||||||
|
return Utils.median(mBpmHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getBestBpmEstimation(Peaks peaksX, Peaks peaksY, Peaks peaksZ) throws IllegalArgumentException {
|
||||||
|
|
||||||
|
int cntNumAxis = 0;
|
||||||
|
double sumCorr = 0; //to prevent division by zero
|
||||||
|
double sumRms = 0;
|
||||||
|
double sumNumInter = 0;
|
||||||
|
|
||||||
|
double corrMeanX = 0, corrRmsX = 0;
|
||||||
|
int corrNumInterX = 0;
|
||||||
|
if(peaksX.hasPeaks()){
|
||||||
|
corrMeanX = Utils.geometricMean(peaksX.getPeaksValueWithoutZeroIdxAndNegativeValues());
|
||||||
|
corrRmsX = Utils.rms(peaksX.getPeaksValueWithoutZeroIdx());
|
||||||
|
corrNumInterX = Utils.intersectionNumber(peaksX.getData(), 0.2f);
|
||||||
|
|
||||||
|
++cntNumAxis;
|
||||||
|
sumCorr += corrMeanX;
|
||||||
|
sumRms += corrRmsX;
|
||||||
|
sumNumInter += corrNumInterX;
|
||||||
|
}
|
||||||
|
|
||||||
|
double corrMeanY = 0, corrRmsY = 0;
|
||||||
|
int corrNumInterY = 0;
|
||||||
|
if(peaksY.hasPeaks()){
|
||||||
|
corrMeanY = Utils.geometricMean(peaksY.getPeaksValueWithoutZeroIdxAndNegativeValues());
|
||||||
|
corrRmsY = Utils.rms(peaksY.getPeaksValueWithoutZeroIdx());
|
||||||
|
corrNumInterY = Utils.intersectionNumber(peaksY.getData(), 0.2f);
|
||||||
|
|
||||||
|
++cntNumAxis;
|
||||||
|
sumCorr += corrMeanY;
|
||||||
|
sumRms += corrRmsY;
|
||||||
|
sumNumInter += corrNumInterY;
|
||||||
|
}
|
||||||
|
|
||||||
|
double corrMeanZ = 0, corrRmsZ = 0;
|
||||||
|
int corrNumInterZ = 0;
|
||||||
|
if(peaksZ.hasPeaks()){
|
||||||
|
corrMeanZ = Utils.geometricMean(peaksZ.getPeaksValueWithoutZeroIdxAndNegativeValues());
|
||||||
|
corrRmsZ = Utils.rms(peaksZ.getPeaksValueWithoutZeroIdx());
|
||||||
|
corrNumInterZ = Utils.intersectionNumber(peaksZ.getData(), 0.2f);
|
||||||
|
|
||||||
|
++cntNumAxis;
|
||||||
|
sumCorr += corrMeanZ;
|
||||||
|
sumRms += corrRmsZ;
|
||||||
|
sumNumInter += corrNumInterZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
//no peaks, reject
|
||||||
|
if(cntNumAxis == 0){
|
||||||
|
//throw new IllegalArgumentException("All Peaks are empty! -> Reject Estimation");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
System.out.println("RMS-X: " + corrRmsX);
|
||||||
|
System.out.println("GEO-X: " + corrMeanX);
|
||||||
|
System.out.println("INTER-X: " + corrNumInterX);
|
||||||
|
|
||||||
|
System.out.println("RMS-Y: " + corrRmsY);
|
||||||
|
System.out.println("GEO-Y: " + corrMeanY);
|
||||||
|
System.out.println("INTER-Y: " + corrNumInterY);
|
||||||
|
|
||||||
|
System.out.println("RMS-Z: " + corrRmsZ);
|
||||||
|
System.out.println("GEO-Z: " + corrMeanZ);
|
||||||
|
System.out.println("INTER-Z: " + corrNumInterZ);
|
||||||
|
*/
|
||||||
|
|
||||||
|
//values to low, reject
|
||||||
|
//TODO: this is a pretty simple assumption. first shot!
|
||||||
|
if(corrRmsX < 0.25 && corrRmsY < 0.25 && corrRmsZ < 0.25){
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
double quantityX = ((corrMeanX / sumCorr) + (corrRmsX / sumRms) + (corrNumInterX / sumNumInter)) / cntNumAxis;
|
||||||
|
double quantityY = ((corrMeanY / sumCorr) + (corrRmsY / sumRms) + (corrNumInterY / sumNumInter)) / cntNumAxis;
|
||||||
|
double quantityZ = ((corrMeanZ / sumCorr) + (corrRmsZ / sumRms) + (corrNumInterZ / sumNumInter)) / cntNumAxis;
|
||||||
|
|
||||||
|
//get best axis by quantity and estimate bpm
|
||||||
|
if(quantityX > quantityY && quantityX > quantityZ){
|
||||||
|
return mBpmHistory_X.getLast();
|
||||||
|
}
|
||||||
|
else if(quantityY > quantityZ){
|
||||||
|
return mBpmHistory_Y.getLast();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return mBpmHistory_Z.getLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,828 @@
|
|||||||
|
package de.tonifetzer.conductorswatch.ui;
|
||||||
|
|
||||||
|
import android.animation.ArgbEvaluator;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import de.tonifetzer.conductorswatch.R;
|
||||||
|
import de.tonifetzer.conductorswatch.utilities.Utils;
|
||||||
|
|
||||||
|
public class Croller extends View {
|
||||||
|
|
||||||
|
private float midx, midy;
|
||||||
|
private Paint textPaint, circlePaint, circlePaint2, linePaint;
|
||||||
|
private float currdeg = 0, deg = 3, downdeg = 0;
|
||||||
|
|
||||||
|
private boolean isContinuous = false;
|
||||||
|
|
||||||
|
private int backCircleColor = Color.parseColor("#222222");
|
||||||
|
private int mainCircleColor = Color.parseColor("#000000");
|
||||||
|
private int indicatorColor = Color.parseColor("#FFA036");
|
||||||
|
private int progressPrimaryColor = Color.parseColor("#FFA036");
|
||||||
|
private int progressSecondaryColor = Color.parseColor("#111111");
|
||||||
|
|
||||||
|
private float progressPrimaryCircleSize = -1;
|
||||||
|
private float progressSecondaryCircleSize = -1;
|
||||||
|
|
||||||
|
private float progressPrimaryStrokeWidth = 25;
|
||||||
|
private float progressSecondaryStrokeWidth = 10;
|
||||||
|
|
||||||
|
private float mainCircleRadius = -1;
|
||||||
|
private float backCircleRadius = -1;
|
||||||
|
private float progressRadius = -1;
|
||||||
|
|
||||||
|
private float touchCircleRadiusMax = -1;
|
||||||
|
private float touchCircleRadiusMin = -1;
|
||||||
|
|
||||||
|
private int max = 25;
|
||||||
|
private int min = 1;
|
||||||
|
|
||||||
|
private float indicatorWidth = 7;
|
||||||
|
|
||||||
|
private String label = "Label";
|
||||||
|
private int labelSize = 20;
|
||||||
|
private int labelColor = Color.WHITE;
|
||||||
|
|
||||||
|
private int startOffset = 30;
|
||||||
|
private int startOffset2 = 0;
|
||||||
|
private int sweepAngle = -1;
|
||||||
|
|
||||||
|
private boolean isAntiClockwise = false;
|
||||||
|
|
||||||
|
private boolean startEventSent = false;
|
||||||
|
|
||||||
|
RectF oval;
|
||||||
|
|
||||||
|
private onProgressChangedListener mProgressChangeListener;
|
||||||
|
private OnCrollerChangeListener mCrollerChangeListener;
|
||||||
|
|
||||||
|
public interface onProgressChangedListener {
|
||||||
|
void onProgressChanged(int progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnProgressChangedListener(onProgressChangedListener mProgressChangeListener) {
|
||||||
|
this.mProgressChangeListener = mProgressChangeListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnCrollerChangeListener {
|
||||||
|
void onProgressChanged(Croller croller, int progress);
|
||||||
|
void onStartTrackingTouch(Croller croller);
|
||||||
|
void onStopTrackingTouch(Croller croller);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnCrollerChangeListener(OnCrollerChangeListener mCrollerChangeListener) {
|
||||||
|
this.mCrollerChangeListener = mCrollerChangeListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Croller(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Croller(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
initXMLAttrs(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Croller(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
initXMLAttrs(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
textPaint = new Paint();
|
||||||
|
textPaint.setAntiAlias(true);
|
||||||
|
textPaint.setColor(labelColor);
|
||||||
|
textPaint.setStyle(Paint.Style.FILL);
|
||||||
|
textPaint.setTextSize(labelSize);
|
||||||
|
textPaint.setFakeBoldText(true);
|
||||||
|
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
|
||||||
|
circlePaint = new Paint();
|
||||||
|
circlePaint.setAntiAlias(true);
|
||||||
|
circlePaint.setColor(progressSecondaryColor);
|
||||||
|
circlePaint.setStrokeWidth(progressSecondaryStrokeWidth);
|
||||||
|
circlePaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
circlePaint2 = new Paint();
|
||||||
|
circlePaint2.setAntiAlias(true);
|
||||||
|
circlePaint2.setColor(progressPrimaryColor);
|
||||||
|
circlePaint2.setStrokeWidth(progressPrimaryStrokeWidth);
|
||||||
|
circlePaint2.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
linePaint = new Paint();
|
||||||
|
linePaint.setAntiAlias(true);
|
||||||
|
linePaint.setColor(indicatorColor);
|
||||||
|
linePaint.setStrokeWidth(indicatorWidth);
|
||||||
|
|
||||||
|
oval = new RectF();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initXMLAttrs(Context context, AttributeSet attrs) {
|
||||||
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Croller);
|
||||||
|
final int N = a.getIndexCount();
|
||||||
|
for (int i = 0; i < N; ++i) {
|
||||||
|
int attr = a.getIndex(i);
|
||||||
|
if (attr == R.styleable.Croller_progress) {
|
||||||
|
setProgress(a.getInt(attr, 1));
|
||||||
|
} else if (attr == R.styleable.Croller_label) {
|
||||||
|
setLabel(a.getString(attr));
|
||||||
|
} else if (attr == R.styleable.Croller_back_circle_color) {
|
||||||
|
setBackCircleColor(a.getColor(attr, Color.parseColor("#222222")));
|
||||||
|
} else if (attr == R.styleable.Croller_main_circle_color) {
|
||||||
|
setMainCircleColor(a.getColor(attr, Color.parseColor("#000000")));
|
||||||
|
} else if (attr == R.styleable.Croller_indicator_color) {
|
||||||
|
setIndicatorColor(a.getColor(attr, Color.parseColor("#FFA036")));
|
||||||
|
} else if (attr == R.styleable.Croller_progress_primary_color) {
|
||||||
|
setProgressPrimaryColor(a.getColor(attr, Color.parseColor("#FFA036")));
|
||||||
|
} else if (attr == R.styleable.Croller_progress_secondary_color) {
|
||||||
|
setProgressSecondaryColor(a.getColor(attr, Color.parseColor("#111111")));
|
||||||
|
} else if (attr == R.styleable.Croller_label_size) {
|
||||||
|
setLabelSize(a.getInteger(attr, 40));
|
||||||
|
} else if (attr == R.styleable.Croller_label_color) {
|
||||||
|
setLabelColor(a.getColor(attr, Color.WHITE));
|
||||||
|
} else if (attr == R.styleable.Croller_indicator_width) {
|
||||||
|
setIndicatorWidth(a.getFloat(attr, 7));
|
||||||
|
} else if (attr == R.styleable.Croller_is_continuous) {
|
||||||
|
setIsContinuous(a.getBoolean(attr, false));
|
||||||
|
} else if (attr == R.styleable.Croller_progress_primary_circle_size) {
|
||||||
|
setProgressPrimaryCircleSize(a.getFloat(attr, -1));
|
||||||
|
} else if (attr == R.styleable.Croller_progress_secondary_circle_size) {
|
||||||
|
setProgressSecondaryCircleSize(a.getFloat(attr, -1));
|
||||||
|
} else if (attr == R.styleable.Croller_progress_primary_stroke_width) {
|
||||||
|
setProgressPrimaryStrokeWidth(a.getFloat(attr, 25));
|
||||||
|
} else if (attr == R.styleable.Croller_progress_secondary_stroke_width) {
|
||||||
|
setProgressSecondaryStrokeWidth(a.getFloat(attr, 10));
|
||||||
|
} else if (attr == R.styleable.Croller_sweep_angle) {
|
||||||
|
setSweepAngle(a.getInt(attr, -1));
|
||||||
|
} else if (attr == R.styleable.Croller_start_offset) {
|
||||||
|
setStartOffset(a.getInt(attr, 30));
|
||||||
|
} else if (attr == R.styleable.Croller_max) {
|
||||||
|
setMax(a.getInt(attr, 25));
|
||||||
|
} else if (attr == R.styleable.Croller_min) {
|
||||||
|
setMin(a.getInt(attr, 1));
|
||||||
|
deg = min + 2;
|
||||||
|
} else if (attr == R.styleable.Croller_main_circle_radius) {
|
||||||
|
setMainCircleRadius(a.getFloat(attr, -1));
|
||||||
|
} else if (attr == R.styleable.Croller_back_circle_radius) {
|
||||||
|
setBackCircleRadius(a.getFloat(attr, -1));
|
||||||
|
} else if (attr == R.styleable.Croller_progress_radius) {
|
||||||
|
setProgressRadius(a.getFloat(attr, -1));
|
||||||
|
} else if (attr == R.styleable.Croller_touch_circle_radius_max) {
|
||||||
|
setTouchCircleRadiusMax(a.getFloat(attr, -1));
|
||||||
|
} else if (attr == R.styleable.Croller_touch_circle_radius_min) {
|
||||||
|
setTouchCircleRadiusMin(a.getFloat(attr, -1));
|
||||||
|
}else if (attr == R.styleable.Croller_anticlockwise) {
|
||||||
|
setAntiClockwise(a.getBoolean(attr, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
int minWidth = (int) Utils.convertDpToPixel(160, getContext());
|
||||||
|
int minHeight = (int) Utils.convertDpToPixel(160, getContext());
|
||||||
|
|
||||||
|
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
|
||||||
|
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
|
||||||
|
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
|
||||||
|
if (widthMode == MeasureSpec.EXACTLY) {
|
||||||
|
width = widthSize;
|
||||||
|
} else if (widthMode == MeasureSpec.AT_MOST) {
|
||||||
|
width = Math.min(minWidth, widthSize);
|
||||||
|
} else {
|
||||||
|
// only in case of ScrollViews, otherwise MeasureSpec.UNSPECIFIED is never triggered
|
||||||
|
// If width is wrap_content i.e. MeasureSpec.UNSPECIFIED, then make width equal to height
|
||||||
|
width = heightSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heightMode == MeasureSpec.EXACTLY) {
|
||||||
|
height = heightSize;
|
||||||
|
} else if (heightMode == MeasureSpec.AT_MOST) {
|
||||||
|
height = Math.min(minHeight, heightSize);
|
||||||
|
} else {
|
||||||
|
// only in case of ScrollViews, otherwise MeasureSpec.UNSPECIFIED is never triggered
|
||||||
|
// If height is wrap_content i.e. MeasureSpec.UNSPECIFIED, then make height equal to width
|
||||||
|
height = widthSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widthMode == MeasureSpec.UNSPECIFIED && heightMode == MeasureSpec.UNSPECIFIED) {
|
||||||
|
width = minWidth;
|
||||||
|
height = minHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMeasuredDimension(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
//called way to much!
|
||||||
|
if (mProgressChangeListener != null)
|
||||||
|
mProgressChangeListener.onProgressChanged((int) (deg - 2));
|
||||||
|
|
||||||
|
if (mCrollerChangeListener != null)
|
||||||
|
mCrollerChangeListener.onProgressChanged(this, (int) (deg - 2));
|
||||||
|
|
||||||
|
midx = canvas.getWidth() / 2;
|
||||||
|
midy = canvas.getHeight() / 2;
|
||||||
|
|
||||||
|
if (!isContinuous) {
|
||||||
|
|
||||||
|
startOffset2 = startOffset - 15;
|
||||||
|
|
||||||
|
circlePaint.setColor(progressSecondaryColor);
|
||||||
|
circlePaint2.setColor(progressPrimaryColor);
|
||||||
|
linePaint.setStrokeWidth(indicatorWidth);
|
||||||
|
linePaint.setColor(indicatorColor);
|
||||||
|
textPaint.setColor(labelColor);
|
||||||
|
textPaint.setTextSize(labelSize);
|
||||||
|
|
||||||
|
int radius = (int) (Math.min(midx, midy) * ((float) 14.5 / 16));
|
||||||
|
|
||||||
|
if (sweepAngle == -1) {
|
||||||
|
sweepAngle = 360 - (2 * startOffset2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainCircleRadius == -1) {
|
||||||
|
mainCircleRadius = radius * ((float) 11 / 15);
|
||||||
|
}
|
||||||
|
if (backCircleRadius == -1) {
|
||||||
|
backCircleRadius = radius * ((float) 13 / 15);
|
||||||
|
}
|
||||||
|
if (progressRadius == -1) {
|
||||||
|
progressRadius = radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touchCircleRadiusMax == -1) {
|
||||||
|
touchCircleRadiusMax = Math.max(mainCircleRadius, Math.max(backCircleRadius, progressRadius));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touchCircleRadiusMin == -1) {
|
||||||
|
touchCircleRadiusMin = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float x, y;
|
||||||
|
float deg2 = Math.max(3, deg);
|
||||||
|
float deg3 = Math.min(deg, max + 2);
|
||||||
|
for (int i = (int) (deg2); i < max + 3; i++) {
|
||||||
|
float tmp = ((float) startOffset2 / 360) + ((float) sweepAngle / 360) * (float) i / (max + 5);
|
||||||
|
|
||||||
|
if (isAntiClockwise) {
|
||||||
|
tmp = 1.0f - tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = midx + (float) (progressRadius * Math.sin(2 * Math.PI * (1.0 - tmp)));
|
||||||
|
y = midy + (float) (progressRadius * Math.cos(2 * Math.PI * (1.0 - tmp)));
|
||||||
|
circlePaint.setColor(progressSecondaryColor);
|
||||||
|
if (progressSecondaryCircleSize == -1)
|
||||||
|
canvas.drawCircle(x, y, ((float) radius / 30 * ((float) 20 / max) * ((float) sweepAngle / 270)), circlePaint);
|
||||||
|
else
|
||||||
|
canvas.drawCircle(x, y, progressSecondaryCircleSize, circlePaint);
|
||||||
|
}
|
||||||
|
for (int i = 3; i <= deg3; i++) {
|
||||||
|
float tmp = ((float) startOffset2 / 360) + ((float) sweepAngle / 360) * (float) i / (max + 5);
|
||||||
|
|
||||||
|
if (isAntiClockwise) {
|
||||||
|
tmp = 1.0f - tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = midx + (float) (progressRadius * Math.sin(2 * Math.PI * (1.0 - tmp)));
|
||||||
|
y = midy + (float) (progressRadius * Math.cos(2 * Math.PI * (1.0 - tmp)));
|
||||||
|
if (progressPrimaryCircleSize == -1)
|
||||||
|
canvas.drawCircle(x, y, (progressRadius / 15 * ((float) 20 / max) * ((float) sweepAngle / 270)), circlePaint2);
|
||||||
|
else
|
||||||
|
canvas.drawCircle(x, y, progressPrimaryCircleSize, circlePaint2);
|
||||||
|
}
|
||||||
|
|
||||||
|
float tmp2 = ((float) startOffset2 / 360) + ((float) sweepAngle / 360) * deg / (max + 5);
|
||||||
|
|
||||||
|
if (isAntiClockwise) {
|
||||||
|
tmp2 = 1.0f - tmp2;
|
||||||
|
}
|
||||||
|
|
||||||
|
float x1 = midx + (float) (radius * ((float) 2 / 5) * Math.sin(2 * Math.PI * (1.0 - tmp2)));
|
||||||
|
float y1 = midy + (float) (radius * ((float) 2 / 5) * Math.cos(2 * Math.PI * (1.0 - tmp2)));
|
||||||
|
float x2 = midx + (float) (radius * ((float) 3 / 5) * Math.sin(2 * Math.PI * (1.0 - tmp2)));
|
||||||
|
float y2 = midy + (float) (radius * ((float) 3 / 5) * Math.cos(2 * Math.PI * (1.0 - tmp2)));
|
||||||
|
|
||||||
|
circlePaint.setColor(backCircleColor);
|
||||||
|
canvas.drawCircle(midx, midy, backCircleRadius, circlePaint);
|
||||||
|
circlePaint.setColor(mainCircleColor);
|
||||||
|
canvas.drawCircle(midx, midy, mainCircleRadius, circlePaint);
|
||||||
|
canvas.drawText(label, midx, midy + (float) (radius * 1.1), textPaint);
|
||||||
|
canvas.drawLine(x1, y1, x2, y2, linePaint);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
int radius = (int) (Math.min(midx, midy) * ((float) 14.5 / 16));
|
||||||
|
|
||||||
|
if (sweepAngle == -1) {
|
||||||
|
sweepAngle = 360 - (2 * startOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainCircleRadius == -1) {
|
||||||
|
mainCircleRadius = radius * ((float) 11 / 15);
|
||||||
|
}
|
||||||
|
if (backCircleRadius == -1) {
|
||||||
|
backCircleRadius = radius * ((float) 13 / 15);
|
||||||
|
}
|
||||||
|
if (progressRadius == -1) {
|
||||||
|
progressRadius = radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touchCircleRadiusMax == -1) {
|
||||||
|
touchCircleRadiusMax = Math.max(mainCircleRadius, Math.max(backCircleRadius, progressRadius));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (touchCircleRadiusMin == -1) {
|
||||||
|
touchCircleRadiusMin = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
circlePaint.setColor(progressSecondaryColor);
|
||||||
|
circlePaint.setStrokeWidth(progressSecondaryStrokeWidth);
|
||||||
|
circlePaint.setStyle(Paint.Style.STROKE);
|
||||||
|
circlePaint2.setColor(progressPrimaryColor);
|
||||||
|
circlePaint2.setStrokeWidth(progressPrimaryStrokeWidth);
|
||||||
|
circlePaint2.setStyle(Paint.Style.STROKE);
|
||||||
|
linePaint.setStrokeWidth(indicatorWidth);
|
||||||
|
linePaint.setColor(indicatorColor);
|
||||||
|
textPaint.setColor(labelColor);
|
||||||
|
textPaint.setTextSize(labelSize);
|
||||||
|
|
||||||
|
float deg3 = Math.min(deg, max + 2);
|
||||||
|
|
||||||
|
oval.set(midx - progressRadius, midy - progressRadius, midx + progressRadius, midy + progressRadius);
|
||||||
|
|
||||||
|
canvas.drawArc(oval, (float) 90 + startOffset, (float) sweepAngle, false, circlePaint);
|
||||||
|
if (isAntiClockwise) {
|
||||||
|
canvas.drawArc(oval, (float) 90 - startOffset, -1 * ((deg3 - 2) * ((float) sweepAngle / max)), false, circlePaint2);
|
||||||
|
} else {
|
||||||
|
canvas.drawArc(oval, (float) 90 + startOffset, ((deg3 - 2) * ((float) sweepAngle / max)), false, circlePaint2);
|
||||||
|
}
|
||||||
|
|
||||||
|
float tmp2 = ((float) startOffset / 360) + (((float) sweepAngle / 360) * ((deg - 2) / (max)));
|
||||||
|
|
||||||
|
if (isAntiClockwise) {
|
||||||
|
tmp2 = 1.0f - tmp2;
|
||||||
|
}
|
||||||
|
|
||||||
|
float x1 = midx + (float) (radius * ((float) 2 / 5) * Math.sin(2 * Math.PI * (1.0 - tmp2)));
|
||||||
|
float y1 = midy + (float) (radius * ((float) 2 / 5) * Math.cos(2 * Math.PI * (1.0 - tmp2)));
|
||||||
|
float x2 = midx + (float) (radius * ((float) 3 / 5) * Math.sin(2 * Math.PI * (1.0 - tmp2)));
|
||||||
|
float y2 = midy + (float) (radius * ((float) 3 / 5) * Math.cos(2 * Math.PI * (1.0 - tmp2)));
|
||||||
|
|
||||||
|
circlePaint.setStyle(Paint.Style.FILL);
|
||||||
|
|
||||||
|
circlePaint.setColor(backCircleColor);
|
||||||
|
canvas.drawCircle(midx, midy, backCircleRadius, circlePaint);
|
||||||
|
circlePaint.setColor(mainCircleColor);
|
||||||
|
canvas.drawCircle(midx, midy, mainCircleRadius, circlePaint);
|
||||||
|
canvas.drawText(label, midx, midy + (float) (radius * 0.9), textPaint);
|
||||||
|
canvas.drawLine(x1, y1, x2, y2, linePaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent e) {
|
||||||
|
|
||||||
|
double distancePointToMiddle = Utils.getDistance(e.getX(), e.getY(), midx, midy);
|
||||||
|
if ((distancePointToMiddle > touchCircleRadiusMax || distancePointToMiddle < touchCircleRadiusMin)) {
|
||||||
|
if (startEventSent && mCrollerChangeListener != null) {
|
||||||
|
mCrollerChangeListener.onStopTrackingTouch(this);
|
||||||
|
startEventSent = false;
|
||||||
|
}
|
||||||
|
return super.onTouchEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.getAction() == MotionEvent.ACTION_DOWN) {
|
||||||
|
float dx = e.getX() - midx;
|
||||||
|
float dy = e.getY() - midy;
|
||||||
|
downdeg = (float) ((Math.atan2(dy, dx) * 180) / Math.PI);
|
||||||
|
downdeg -= 90;
|
||||||
|
if (downdeg < 0) {
|
||||||
|
downdeg += 360;
|
||||||
|
}
|
||||||
|
downdeg = (float) Math.floor((downdeg / 360) * (max + 5));
|
||||||
|
|
||||||
|
if (mCrollerChangeListener != null) {
|
||||||
|
mCrollerChangeListener.onStartTrackingTouch(this);
|
||||||
|
startEventSent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (e.getAction() == MotionEvent.ACTION_MOVE) {
|
||||||
|
float dx = e.getX() - midx;
|
||||||
|
float dy = e.getY() - midy;
|
||||||
|
currdeg = (float) ((Math.atan2(dy, dx) * 180) / Math.PI);
|
||||||
|
currdeg -= 90;
|
||||||
|
if (currdeg < 0) {
|
||||||
|
currdeg += 360;
|
||||||
|
}
|
||||||
|
currdeg = (float) Math.floor((currdeg / 360) * (max + 5));
|
||||||
|
|
||||||
|
if ((currdeg / (max + 4)) > 0.75f && ((downdeg - 0) / (max + 4)) < 0.25f) {
|
||||||
|
if (isAntiClockwise) {
|
||||||
|
deg++;
|
||||||
|
if (deg > max + 2) {
|
||||||
|
deg = max + 2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deg--;
|
||||||
|
if (deg < (min + 2)) {
|
||||||
|
deg = (min + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ((downdeg / (max + 4)) > 0.75f && ((currdeg - 0) / (max + 4)) < 0.25f) {
|
||||||
|
if (isAntiClockwise) {
|
||||||
|
deg--;
|
||||||
|
if (deg < (min + 2)) {
|
||||||
|
deg = (min + 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deg++;
|
||||||
|
if (deg > max + 2) {
|
||||||
|
deg = max + 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isAntiClockwise) {
|
||||||
|
deg -= (currdeg - downdeg);
|
||||||
|
} else {
|
||||||
|
deg += (currdeg - downdeg);
|
||||||
|
}
|
||||||
|
if (deg > max + 2) {
|
||||||
|
deg = max + 2;
|
||||||
|
}
|
||||||
|
if (deg < (min + 2)) {
|
||||||
|
deg = (min + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downdeg = currdeg;
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
if (e.getAction() == MotionEvent.ACTION_UP) {
|
||||||
|
if (mCrollerChangeListener != null) {
|
||||||
|
mCrollerChangeListener.onStopTrackingTouch(this);
|
||||||
|
startEventSent = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onTouchEvent(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchTouchEvent(MotionEvent event) {
|
||||||
|
if (getParent() != null && event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||||
|
getParent().requestDisallowInterceptTouchEvent(true);
|
||||||
|
}
|
||||||
|
return super.dispatchTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProgress() {
|
||||||
|
return (int) (deg - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgress(int x) {
|
||||||
|
if(deg != x + 2){
|
||||||
|
deg = x + 2;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String txt) {
|
||||||
|
if(!label.equals(txt)){
|
||||||
|
label = txt;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBackCircleColor() {
|
||||||
|
return backCircleColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBackCircleColor(int backCircleColor) {
|
||||||
|
if(this.backCircleColor != backCircleColor){
|
||||||
|
this.backCircleColor = backCircleColor;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMainCircleColor() {
|
||||||
|
return mainCircleColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMainCircleColor(int mainCircleColor) {
|
||||||
|
if(this.mainCircleColor != mainCircleColor){
|
||||||
|
this.mainCircleColor = mainCircleColor;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueAnimator mainCircleAnimation;
|
||||||
|
public void setMainCircleColorAnimated(int startColor, int endColor, int duration) {
|
||||||
|
|
||||||
|
mainCircleAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), startColor, endColor);
|
||||||
|
mainCircleAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationUpdate(ValueAnimator animator) {
|
||||||
|
setMainCircleColor((Integer) animator.getAnimatedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
mainCircleAnimation.setDuration(duration);
|
||||||
|
mainCircleAnimation.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopMainCircleColorAnimated(){
|
||||||
|
if(mainCircleAnimation != null){
|
||||||
|
if(mainCircleAnimation.isRunning()){
|
||||||
|
mainCircleAnimation.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void interruptMainCircleColorAnimated(){
|
||||||
|
if(mainCircleAnimation != null){
|
||||||
|
if(mainCircleAnimation.isRunning()){
|
||||||
|
mainCircleAnimation.setDuration(0);
|
||||||
|
mainCircleAnimation.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void interruptBackCircleAnimated(){
|
||||||
|
if(backCircleAnimation != null){
|
||||||
|
if(backCircleAnimation.isRunning()){
|
||||||
|
backCircleAnimation.setDuration(0);
|
||||||
|
backCircleAnimation.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMainCircleAnimationRunning(){
|
||||||
|
if(mainCircleAnimation != null){
|
||||||
|
return mainCircleAnimation.isRunning();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBackCircleAnimationRunning(){
|
||||||
|
if(backCircleAnimation != null){
|
||||||
|
return backCircleAnimation.isRunning();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueAnimator backCircleAnimation;
|
||||||
|
public void setBackCircleColorAnimated(int startColor, int endColor, int duration) {
|
||||||
|
|
||||||
|
backCircleAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), startColor, endColor);
|
||||||
|
backCircleAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationUpdate(ValueAnimator animator) {
|
||||||
|
setBackCircleColor((Integer) animator.getAnimatedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
backCircleAnimation.setDuration(duration);
|
||||||
|
backCircleAnimation.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopBackCircleColorAnimated(){
|
||||||
|
backCircleAnimation.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIndicatorColor() {
|
||||||
|
return indicatorColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIndicatorColor(int indicatorColor) {
|
||||||
|
this.indicatorColor = indicatorColor;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProgressPrimaryColor() {
|
||||||
|
return progressPrimaryColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgressPrimaryColor(int progressPrimaryColor) {
|
||||||
|
this.progressPrimaryColor = progressPrimaryColor;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProgressSecondaryColor() {
|
||||||
|
return progressSecondaryColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgressSecondaryColor(int progressSecondaryColor) {
|
||||||
|
this.progressSecondaryColor = progressSecondaryColor;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLabelSize() {
|
||||||
|
return labelSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabelSize(int labelSize) {
|
||||||
|
this.labelSize = labelSize;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLabelColor() {
|
||||||
|
return labelColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabelColor(int labelColor) {
|
||||||
|
this.labelColor = labelColor;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getIndicatorWidth() {
|
||||||
|
return indicatorWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIndicatorWidth(float indicatorWidth) {
|
||||||
|
this.indicatorWidth = indicatorWidth;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isContinuous() {
|
||||||
|
return isContinuous;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsContinuous(boolean isContinuous) {
|
||||||
|
this.isContinuous = isContinuous;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getProgressPrimaryCircleSize() {
|
||||||
|
return progressPrimaryCircleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgressPrimaryCircleSize(float progressPrimaryCircleSize) {
|
||||||
|
this.progressPrimaryCircleSize = progressPrimaryCircleSize;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getProgressSecondaryCircleSize() {
|
||||||
|
return progressSecondaryCircleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgressSecondaryCircleSize(float progressSecondaryCircleSize) {
|
||||||
|
this.progressSecondaryCircleSize = progressSecondaryCircleSize;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getProgressPrimaryStrokeWidth() {
|
||||||
|
return progressPrimaryStrokeWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgressPrimaryStrokeWidth(float progressPrimaryStrokeWidth) {
|
||||||
|
this.progressPrimaryStrokeWidth = progressPrimaryStrokeWidth;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getProgressSecondaryStrokeWidth() {
|
||||||
|
return progressSecondaryStrokeWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgressSecondaryStrokeWidth(float progressSecondaryStrokeWidth) {
|
||||||
|
this.progressSecondaryStrokeWidth = progressSecondaryStrokeWidth;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSweepAngle() {
|
||||||
|
return sweepAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSweepAngle(int sweepAngle) {
|
||||||
|
this.sweepAngle = sweepAngle;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStartOffset() {
|
||||||
|
return startOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStartOffset(int startOffset) {
|
||||||
|
this.startOffset = startOffset;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMax() {
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMax(int max) {
|
||||||
|
if (max < min) {
|
||||||
|
this.max = min;
|
||||||
|
} else {
|
||||||
|
this.max = max;
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMin() {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMin(int min) {
|
||||||
|
if (min < 0) {
|
||||||
|
this.min = 0;
|
||||||
|
} else if (min > max) {
|
||||||
|
this.min = max;
|
||||||
|
} else {
|
||||||
|
this.min = min;
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getMainCircleRadius() {
|
||||||
|
return mainCircleRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMainCircleRadius(float mainCircleRadius) {
|
||||||
|
this.mainCircleRadius = mainCircleRadius;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getBackCircleRadius() {
|
||||||
|
return backCircleRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBackCircleRadius(float backCircleRadius) {
|
||||||
|
this.backCircleRadius = backCircleRadius;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getProgressRadius() {
|
||||||
|
return progressRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProgressRadius(float progressRadius) {
|
||||||
|
this.progressRadius = progressRadius;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getTouchCircleRadiusMax() {
|
||||||
|
return touchCircleRadiusMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTouchCircleRadiusMax(float touchCircleRadiusMax) {
|
||||||
|
this.touchCircleRadiusMax = touchCircleRadiusMax;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public float getTouchCircleRadiusMin() {
|
||||||
|
return touchCircleRadiusMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTouchCircleRadiusMin(float touchCircleRadiusMin) {
|
||||||
|
this.touchCircleRadiusMin = touchCircleRadiusMin;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isAntiClockwise() {
|
||||||
|
return isAntiClockwise;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAntiClockwise(boolean antiClockwise) {
|
||||||
|
isAntiClockwise = antiClockwise;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package de.tonifetzer.conductorswatch.ui;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Vibrator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by toni on 13/11/17.
|
||||||
|
*/
|
||||||
|
public class Metronome extends TimerTask {
|
||||||
|
|
||||||
|
private Vibrator mVibrator;
|
||||||
|
|
||||||
|
public Metronome(Context context){
|
||||||
|
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
mVibrator.vibrate(10);
|
||||||
|
|
||||||
|
for (OnMetronomeListener listener:listeners) {
|
||||||
|
listener.onNewClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for callback metronome clicks
|
||||||
|
* I know that java has observable.. but this why it is more clean and easy
|
||||||
|
*/
|
||||||
|
public interface OnMetronomeListener {
|
||||||
|
void onNewClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<OnMetronomeListener> listeners = new CopyOnWriteArrayList<OnMetronomeListener>();
|
||||||
|
public void add(OnMetronomeListener listener){listeners.add(listener);}
|
||||||
|
public void remove(OnMetronomeListener listener){listeners.remove(listener);}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package de.tonifetzer.conductorswatch.ui;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Vector;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by toni on 20/11/17.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class TapBpm implements Runnable {
|
||||||
|
|
||||||
|
private Vector<Long> mReceivedTabs = new Vector<Long>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
int breakCounter = 2000;
|
||||||
|
int bpmTapped = 0;
|
||||||
|
int calcNewBpmCounter = 3;
|
||||||
|
|
||||||
|
if(!mReceivedTabs.isEmpty()){
|
||||||
|
do{
|
||||||
|
if(bpmTapped > 0){
|
||||||
|
breakCounter = 2 * (60000 / bpmTapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mReceivedTabs.size() > calcNewBpmCounter) {
|
||||||
|
|
||||||
|
long sumDifferenceMs = 0L;
|
||||||
|
for (int i = 0; i < mReceivedTabs.size() -1; ++i) {
|
||||||
|
sumDifferenceMs += mReceivedTabs.get(i + 1)- mReceivedTabs.get(i);
|
||||||
|
}
|
||||||
|
bpmTapped = (int) (60000 / (sumDifferenceMs / (mReceivedTabs.size() - 1)));
|
||||||
|
|
||||||
|
for (TapBpm.OnTapBpmListener listener:listeners) {
|
||||||
|
listener.onNewTapEstimation(bpmTapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
//only update everytime a new timestamp arrives
|
||||||
|
++calcNewBpmCounter;
|
||||||
|
}
|
||||||
|
}while(System.currentTimeMillis() - mReceivedTabs.lastElement() < breakCounter);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TapBpm.OnTapBpmListener listener:listeners) {
|
||||||
|
listener.onTapFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addTimestamp(Long ts){
|
||||||
|
mReceivedTabs.add(ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearTimestamps(){
|
||||||
|
mReceivedTabs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for callback calculated bpm
|
||||||
|
*/
|
||||||
|
public interface OnTapBpmListener {
|
||||||
|
void onTapFinished();
|
||||||
|
void onNewTapEstimation(int bpm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TapBpm.OnTapBpmListener> listeners = new CopyOnWriteArrayList<TapBpm.OnTapBpmListener>();
|
||||||
|
public void add(TapBpm.OnTapBpmListener listener){listeners.add(listener);}
|
||||||
|
public void remove(TapBpm.OnTapBpmListener listener){listeners.remove(listener);}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user