mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-26 10:35:50 +01:00
VO2Max: initalize activity
This commit is contained in:
parent
e2be851097
commit
d440ec1e36
@ -127,7 +127,7 @@ public class GBApplication extends Application {
|
||||
private static SharedPreferences sharedPrefs;
|
||||
private static final String PREFS_VERSION = "shared_preferences_version";
|
||||
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
|
||||
private static final int CURRENT_PREFS_VERSION = 39;
|
||||
private static final int CURRENT_PREFS_VERSION = 40;
|
||||
|
||||
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
|
||||
private static GBPrefs prefs;
|
||||
@ -1793,6 +1793,36 @@ public class GBApplication extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 40) {
|
||||
// Add the new VO2Max tab to all devices
|
||||
try (DBHandler db = acquireDB()) {
|
||||
final DaoSession daoSession = db.getDaoSession();
|
||||
final List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
|
||||
|
||||
for (final Device dbDevice : activeDevices) {
|
||||
final SharedPreferences deviceSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
|
||||
|
||||
final String chartsTabsValue = deviceSharedPrefs.getString("charts_tabs", null);
|
||||
if (chartsTabsValue == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String newPrefValue;
|
||||
if (!StringUtils.isBlank(chartsTabsValue)) {
|
||||
newPrefValue = chartsTabsValue + ",vo2max";
|
||||
} else {
|
||||
newPrefValue = "vo2max";
|
||||
}
|
||||
|
||||
final SharedPreferences.Editor deviceSharedPrefsEdit = deviceSharedPrefs.edit();
|
||||
deviceSharedPrefsEdit.putString("charts_tabs", newPrefValue);
|
||||
deviceSharedPrefsEdit.apply();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to migrate prefs to version 40", e);
|
||||
}
|
||||
}
|
||||
|
||||
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
|
||||
editor.apply();
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
@ -186,7 +187,7 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
|
||||
if (samples.isEmpty()) {
|
||||
lineData = new LineData();
|
||||
ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
|
||||
ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm");
|
||||
return new DefaultChartsData<>(lineData, xValueFormatter);
|
||||
}
|
||||
|
||||
@ -198,7 +199,7 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
for (int i = 0; i < 6; i++) {
|
||||
entries.add(new ArrayList<>());
|
||||
}
|
||||
|
||||
|
||||
boolean hr = supportsHeartrate(gbDevice);
|
||||
List<Entry> heartrateEntries = hr ? new ArrayList<Entry>(numEntries) : null;
|
||||
|
||||
@ -270,7 +271,7 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
|
||||
lineData = new LineData(lineDataSets);
|
||||
|
||||
ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
|
||||
ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm");
|
||||
return new DefaultChartsData<>(lineData, xValueFormatter);
|
||||
}
|
||||
|
||||
|
@ -130,6 +130,9 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
||||
if (!coordinator.supportsBodyEnergy()) {
|
||||
tabList.remove("bodyenergy");
|
||||
}
|
||||
if (!coordinator.supportsVO2Max()) {
|
||||
tabList.remove("vo2max");
|
||||
}
|
||||
return tabList;
|
||||
}
|
||||
|
||||
@ -164,6 +167,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
||||
return new HRVStatusFragment();
|
||||
case "bodyenergy":
|
||||
return new BodyEnergyFragment();
|
||||
case "vo2max":
|
||||
return new VO2MaxFragment();
|
||||
case "stress":
|
||||
return new StressChartFragment();
|
||||
case "pai":
|
||||
@ -207,6 +212,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
||||
return getString(R.string.pref_header_hrv_status);
|
||||
case "bodyenergy":
|
||||
return getString(R.string.body_energy);
|
||||
case "vo2max":
|
||||
return getString(R.string.vo2max);
|
||||
case "stress":
|
||||
return getString(R.string.menuitem_stress);
|
||||
case "pai":
|
||||
|
@ -47,6 +47,7 @@ import com.github.mikephil.charting.utils.Utils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
@ -465,7 +466,7 @@ public class LiveActivityFragment extends AbstractActivityChartFragment<ChartsDa
|
||||
x.setDrawGridLines(false);
|
||||
x.setEnabled(true);
|
||||
x.setTextColor(CHART_TEXT_COLOR);
|
||||
x.setValueFormatter(new SampleXLabelFormatter(tsTranslation));
|
||||
x.setValueFormatter(new SampleXLabelFormatter(tsTranslation, "HH:mm"));
|
||||
x.setDrawLimitLinesBehindData(true);
|
||||
|
||||
YAxis y = chart.getAxisLeft();
|
||||
|
@ -28,12 +28,13 @@ import java.util.GregorianCalendar;
|
||||
class SampleXLabelFormatter extends ValueFormatter {
|
||||
private final TimestampTranslation tsTranslation;
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private final SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
|
||||
private final SimpleDateFormat annotationDateFormat;
|
||||
// SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
|
||||
private final Calendar cal = GregorianCalendar.getInstance();
|
||||
|
||||
public SampleXLabelFormatter(final TimestampTranslation tsTranslation) {
|
||||
public SampleXLabelFormatter(final TimestampTranslation tsTranslation, String simpleDateFormatPattern) {
|
||||
this.tsTranslation = tsTranslation;
|
||||
this.annotationDateFormat = new SimpleDateFormat(simpleDateFormatPattern);
|
||||
}
|
||||
|
||||
// TODO: this does not work. Cannot use precomputed labels
|
||||
|
@ -41,6 +41,7 @@ import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -256,7 +257,7 @@ public class Spo2ChartFragment extends AbstractChartFragment<Spo2ChartFragment.S
|
||||
lineDataSets.add(createDataSet(lineEntries));
|
||||
|
||||
final LineData lineData = new LineData(lineDataSets);
|
||||
final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
|
||||
final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm");
|
||||
final DefaultChartsData<LineData> chartsData = new DefaultChartsData<>(lineData, xValueFormatter);
|
||||
return new Spo2ChartsData(chartsData, Math.round((float) averageSum / averageNumSamples));
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ public class StepsDailyFragment extends StepsFragment<StepsDailyFragment.StepsDa
|
||||
lineEntries.add(new Entry(tsTranslation.shorten(sample.getTimestamp()), sum));
|
||||
}
|
||||
|
||||
stepsChart.getXAxis().setValueFormatter(new SampleXLabelFormatter(tsTranslation));
|
||||
stepsChart.getXAxis().setValueFormatter(new SampleXLabelFormatter(tsTranslation, "HH:mm"));
|
||||
|
||||
if (sum < STEPS_GOAL) {
|
||||
stepsChart.getAxisLeft().setAxisMaximum(STEPS_GOAL);
|
||||
|
@ -484,7 +484,7 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
||||
final PieData pieData = new PieData(pieDataSet);
|
||||
|
||||
final LineData lineData = new LineData(lineDataSets);
|
||||
final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
|
||||
final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm");
|
||||
final DefaultChartsData<LineData> chartsData = new DefaultChartsData<>(lineData, xValueFormatter);
|
||||
return new StressChartsData(pieData, chartsData, Math.round((float) averageSum / averageNumSamples), stressZoneTimes);
|
||||
}
|
||||
|
@ -0,0 +1,351 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.charts.LineChart;
|
||||
import com.github.mikephil.charting.components.XAxis;
|
||||
import com.github.mikephil.charting.components.YAxis;
|
||||
import com.github.mikephil.charting.data.Entry;
|
||||
import com.github.mikephil.charting.data.LineData;
|
||||
import com.github.mikephil.charting.data.LineDataSet;
|
||||
import com.github.mikephil.charting.formatter.ValueFormatter;
|
||||
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.GaugeDrawer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.Vo2MaxSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample;
|
||||
|
||||
|
||||
public class VO2MaxFragment extends AbstractChartFragment<VO2MaxFragment.VO2MaxData> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(VO2MaxFragment.class);
|
||||
|
||||
private TextView mDateView;
|
||||
private TextView vo2MaxGeneralValue;
|
||||
private TextView vo2MaxRunningValue;
|
||||
private TextView vo2MaxCyclingValue;
|
||||
private ImageView vo2MaxGeneralGauge;
|
||||
private ImageView vo2MaxRunningGauge;
|
||||
private ImageView vo2MaxCyclingGauge;
|
||||
protected GaugeDrawer gaugeDrawer = new GaugeDrawer();
|
||||
private LineChart vo2MaxChart;
|
||||
private RelativeLayout vo2maxCyclingWrapper;
|
||||
private RelativeLayout vo2maxRunningWrapper;
|
||||
private RelativeLayout vo2maxGeneralWrapper;
|
||||
private GridLayout tilesGridWrapper;
|
||||
private int tsFrom;
|
||||
GBDevice device;
|
||||
|
||||
protected int CHART_TEXT_COLOR;
|
||||
protected int LEGEND_TEXT_COLOR;
|
||||
protected int TEXT_COLOR;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_vo2max, container, false);
|
||||
|
||||
mDateView = rootView.findViewById(R.id.vo2max_date_view);
|
||||
vo2MaxGeneralValue = rootView.findViewById(R.id.vo2max_general_gauge_value);
|
||||
vo2MaxRunningValue = rootView.findViewById(R.id.vo2max_running_gauge_value);
|
||||
vo2MaxCyclingValue = rootView.findViewById(R.id.vo2max_cycling_gauge_value);
|
||||
vo2MaxGeneralGauge = rootView.findViewById(R.id.vo2max_general_gauge);
|
||||
vo2MaxRunningGauge = rootView.findViewById(R.id.vo2max_running_gauge);
|
||||
vo2MaxCyclingGauge = rootView.findViewById(R.id.vo2max_cycling_gauge);
|
||||
vo2MaxChart = rootView.findViewById(R.id.vo2max_chart);
|
||||
vo2maxCyclingWrapper = rootView.findViewById(R.id.vo2max_cycling_card_layout);
|
||||
vo2maxGeneralWrapper = rootView.findViewById(R.id.vo2max_general_card_layout);
|
||||
vo2maxRunningWrapper = rootView.findViewById(R.id.vo2max_running_card_layout);
|
||||
tilesGridWrapper = rootView.findViewById(R.id.tiles_grid_wrapper);
|
||||
device = getChartsHost().getDevice();
|
||||
if (!supportsVO2MaxCycling(device)) {
|
||||
tilesGridWrapper.removeView(vo2maxCyclingWrapper);
|
||||
}
|
||||
if (!supportsVO2MaxRunning(device)) {
|
||||
tilesGridWrapper.removeView(vo2maxRunningWrapper);
|
||||
}
|
||||
if (!supportsVO2MaxGeneral(device)) {
|
||||
tilesGridWrapper.removeView(vo2maxGeneralWrapper);
|
||||
}
|
||||
setupVO2MaxChart();
|
||||
refresh();
|
||||
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
public boolean supportsVO2MaxCycling(GBDevice device) {
|
||||
DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
return coordinator != null && coordinator.supportsVO2MaxCycling();
|
||||
}
|
||||
|
||||
public boolean supportsVO2MaxGeneral(GBDevice device) {
|
||||
DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
return coordinator != null && coordinator.supportsVO2MaxGeneral();
|
||||
}
|
||||
|
||||
public boolean supportsVO2MaxRunning(GBDevice device) {
|
||||
DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
return coordinator != null && coordinator.supportsVO2MaxRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.vo2max);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
TEXT_COLOR = GBApplication.getTextColor(requireContext());
|
||||
LEGEND_TEXT_COLOR = GBApplication.getTextColor(requireContext());
|
||||
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VO2MaxData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
List<VO2MaxRecord> records = new ArrayList<>();
|
||||
int tsEnd = getTSEnd();
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTimeInMillis(tsEnd * 1000L); //we need today initially, which is the end of the time range
|
||||
day.set(Calendar.HOUR_OF_DAY, 0); //and we set time for the start and end of the same day
|
||||
day.set(Calendar.MINUTE, 0);
|
||||
day.set(Calendar.SECOND, 0);
|
||||
day.add(Calendar.DAY_OF_YEAR, -30);
|
||||
tsFrom = (int) (day.getTimeInMillis() / 1000);
|
||||
List<? extends Vo2MaxSample> samples = getAllSamples(db, device, tsFrom, tsEnd);
|
||||
for (Vo2MaxSample sample : samples) {
|
||||
records.add(new VO2MaxRecord(sample.getTimestamp() / 1000, sample.getValue(), sample.getType()));
|
||||
}
|
||||
Map<Vo2MaxSample.Type, VO2MaxRecord> latestValues = new HashMap<>();
|
||||
for (Vo2MaxSample.Type type : Vo2MaxSample.Type.values()) {
|
||||
Vo2MaxSample sample = getLatestVo2MaxSample(db, device, type);
|
||||
if (sample != null) {
|
||||
latestValues.put(type, new VO2MaxRecord(sample.getTimestamp() / 1000, sample.getValue(), type));
|
||||
}
|
||||
}
|
||||
return new VO2MaxData(records, latestValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(VO2MaxData vo2MaxData) {
|
||||
TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
List<Entry> runningEntries = new ArrayList<>();
|
||||
List<Entry> cyclingEntries = new ArrayList<>();
|
||||
List<Entry> generalEntries = new ArrayList<>();
|
||||
vo2MaxData.records.forEach((record) -> {
|
||||
float nd = (float) (record.timestamp - this.tsFrom) / (60 * 60 * 24);
|
||||
switch (record.type) {
|
||||
case RUNNING:
|
||||
runningEntries.add(new Entry(nd, record.value));
|
||||
break;
|
||||
case CYCLING:
|
||||
cyclingEntries.add(new Entry(nd, record.value));
|
||||
break;
|
||||
case GENERAL:
|
||||
generalEntries.add(new Entry(nd, record.value));
|
||||
break;
|
||||
}
|
||||
});
|
||||
final int[] colors = {
|
||||
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_poor_color),
|
||||
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_fair_color),
|
||||
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_good_color),
|
||||
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_excellent_color),
|
||||
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_superior_color),
|
||||
};
|
||||
final float[] segments = {
|
||||
0.20F,
|
||||
0.20F,
|
||||
0.20F,
|
||||
0.20F,
|
||||
0.20F,
|
||||
};
|
||||
float[] vo2MaxRanges = {
|
||||
55.4F,
|
||||
51.1F,
|
||||
45.4F,
|
||||
41.7F,
|
||||
0.0F,
|
||||
};
|
||||
|
||||
final List<ILineDataSet> lineDataSets = new ArrayList<>();
|
||||
if (supportsVO2MaxGeneral(device)) {
|
||||
VO2MaxRecord latestGeneralRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.GENERAL);
|
||||
float generalVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestGeneralRecord != null ? latestGeneralRecord.value : 0);
|
||||
gaugeDrawer.drawSegmentedGauge(vo2MaxGeneralGauge, colors, segments, generalVO2MaxValue, false, true);
|
||||
vo2MaxGeneralValue.setText(String.valueOf(latestGeneralRecord != null ? Math.round(latestGeneralRecord.value) : "-"));
|
||||
lineDataSets.add(createDataSet(generalEntries, getResources().getColor(R.color.vo2max_general_char_line_color), getString(R.string.vo2_max_general)));
|
||||
}
|
||||
if (supportsVO2MaxRunning(device)) {
|
||||
VO2MaxRecord latestRunningRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.RUNNING);
|
||||
float runningVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestRunningRecord != null ? latestRunningRecord.value : 0);
|
||||
vo2MaxRunningValue.setText(String.valueOf(latestRunningRecord != null ? Math.round(latestRunningRecord.value) : "-"));
|
||||
gaugeDrawer.drawSegmentedGauge(vo2MaxRunningGauge, colors, segments, runningVO2MaxValue, false, true);
|
||||
lineDataSets.add(createDataSet(runningEntries, getResources().getColor(R.color.vo2max_running_char_line_color), getString(R.string.vo2_max_running)));
|
||||
}
|
||||
if (supportsVO2MaxCycling(device)) {
|
||||
VO2MaxRecord latestCyclingRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.CYCLING);
|
||||
float cyclingVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestCyclingRecord != null ? latestCyclingRecord.value : 0);
|
||||
gaugeDrawer.drawSegmentedGauge(vo2MaxCyclingGauge, colors, segments, cyclingVO2MaxValue, false, true);
|
||||
vo2MaxCyclingValue.setText(String.valueOf(latestCyclingRecord != null ? Math.round(latestCyclingRecord.value) : "-"));
|
||||
lineDataSets.add(createDataSet(cyclingEntries, getResources().getColor(R.color.vo2max_cycling_char_line_color), getString(R.string.vo2_max_cycling)));
|
||||
}
|
||||
final LineData lineData = new LineData(lineDataSets);
|
||||
vo2MaxChart.getXAxis().setValueFormatter(getVO2MaxLineChartValueFormatter());
|
||||
vo2MaxChart.setData(lineData);
|
||||
}
|
||||
|
||||
ValueFormatter getVO2MaxLineChartValueFormatter() {
|
||||
return new ValueFormatter() {
|
||||
@Override
|
||||
public String getFormattedValue(float value) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTimeInMillis(tsFrom * 1000L);
|
||||
day.add(Calendar.DAY_OF_YEAR, (int) value);
|
||||
return new SimpleDateFormat("dd/MM").format(day.getTime());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private float calculateVO2maxGaugeValue(float[] vo2MaxRanges, float vo2MaxValue) {
|
||||
float value = -1;
|
||||
for (int i = 0; i < vo2MaxRanges.length; i++) {
|
||||
if (vo2MaxValue - vo2MaxRanges[i] > 0) {
|
||||
float rangeValue = i - 1 >= 0 ? vo2MaxRanges[i-1] : 60F;
|
||||
float rangeDiff = rangeValue - vo2MaxRanges[i];
|
||||
float valueDiff = vo2MaxValue - vo2MaxRanges[i];
|
||||
float multiplayer = valueDiff / rangeDiff;
|
||||
value = (4 - i) * 0.2F + 0.2F * (multiplayer > 1 ? 1 : multiplayer) ;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected LineDataSet createDataSet(final List<Entry> values, int color, String label) {
|
||||
final LineDataSet lineDataSet = new LineDataSet(values, label);
|
||||
lineDataSet.setColor(color);
|
||||
lineDataSet.setDrawCircles(false);
|
||||
lineDataSet.setLineWidth(2f);
|
||||
lineDataSet.setFillAlpha(255);
|
||||
lineDataSet.setCircleRadius(5f);
|
||||
lineDataSet.setDrawCircles(true);
|
||||
lineDataSet.setDrawCircleHole(true);
|
||||
lineDataSet.setCircleColor(color);
|
||||
lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
|
||||
lineDataSet.setDrawValues(true);
|
||||
lineDataSet.setValueTextSize(10f);
|
||||
lineDataSet.setValueTextColor(CHART_TEXT_COLOR);
|
||||
lineDataSet.setValueFormatter(new ValueFormatter() {
|
||||
@Override
|
||||
public String getFormattedValue(float value) {
|
||||
return String.format(Locale.ROOT, "%d", Math.round(value));
|
||||
}
|
||||
});
|
||||
return lineDataSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderCharts() {
|
||||
vo2MaxChart.invalidate();
|
||||
}
|
||||
|
||||
|
||||
public List<? extends Vo2MaxSample> getAllSamples(final DBHandler db, final GBDevice device, int tsFrom, int tsTo) {
|
||||
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
final TimeSampleProvider<? extends Vo2MaxSample> sampleProvider = coordinator.getVo2MaxSampleProvider(device, db.getDaoSession());
|
||||
return sampleProvider.getAllSamples(tsFrom * 1000L, tsTo * 1000L);
|
||||
}
|
||||
|
||||
public Vo2MaxSample getLatestVo2MaxSample(final DBHandler db, final GBDevice device, Vo2MaxSample.Type type) {
|
||||
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
final Vo2MaxSampleProvider sampleProvider = (Vo2MaxSampleProvider) coordinator.getVo2MaxSampleProvider(device, db.getDaoSession());
|
||||
return sampleProvider.getLatestSample(type);
|
||||
}
|
||||
|
||||
private void setupVO2MaxChart() {
|
||||
final XAxis xAxisBottom = vo2MaxChart.getXAxis();
|
||||
xAxisBottom.setPosition(XAxis.XAxisPosition.BOTTOM);
|
||||
xAxisBottom.setDrawLabels(true);
|
||||
xAxisBottom.setDrawGridLines(false);
|
||||
xAxisBottom.setEnabled(true);
|
||||
xAxisBottom.setDrawLimitLinesBehindData(true);
|
||||
xAxisBottom.setTextColor(CHART_TEXT_COLOR);
|
||||
xAxisBottom.setAxisMinimum(0f);
|
||||
xAxisBottom.setAxisMaximum(31f);
|
||||
xAxisBottom.setGranularity(1f);
|
||||
xAxisBottom.setGranularityEnabled(true);
|
||||
|
||||
final YAxis yAxisLeft = vo2MaxChart.getAxisLeft();
|
||||
yAxisLeft.setDrawGridLines(true);
|
||||
yAxisLeft.setAxisMaximum(100);
|
||||
yAxisLeft.setAxisMinimum(0);
|
||||
yAxisLeft.setDrawTopYLabelEntry(true);
|
||||
yAxisLeft.setEnabled(true);
|
||||
yAxisLeft.setTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
final YAxis yAxisRight = vo2MaxChart.getAxisRight();
|
||||
yAxisRight.setEnabled(true);
|
||||
yAxisRight.setDrawLabels(false);
|
||||
yAxisRight.setDrawGridLines(false);
|
||||
yAxisRight.setDrawAxisLine(true);
|
||||
}
|
||||
|
||||
protected void setupLegend(Chart<?> chart) {}
|
||||
|
||||
protected static class VO2MaxRecord {
|
||||
float value;
|
||||
long timestamp;
|
||||
Vo2MaxSample.Type type;
|
||||
|
||||
protected VO2MaxRecord(long timestamp, float value, Vo2MaxSample.Type type) {
|
||||
this.timestamp = timestamp;
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
protected static class VO2MaxData extends ChartsData {
|
||||
private final List<? extends VO2MaxRecord> records;
|
||||
private final Map<Vo2MaxSample.Type, VO2MaxRecord> latestValues;
|
||||
|
||||
protected VO2MaxData(List<? extends VO2MaxRecord> records, Map<Vo2MaxSample.Type, VO2MaxRecord> latestValues) {
|
||||
this.records = records;
|
||||
this.latestValues = latestValues;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public VO2MaxRecord getLatestValue(Vo2MaxSample.Type type) {
|
||||
return this.latestValues.get(type);
|
||||
}
|
||||
}
|
||||
}
|
@ -38,7 +38,6 @@ import androidx.annotation.StringRes;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
|
||||
@ -47,6 +46,7 @@ public abstract class AbstractGaugeWidget extends AbstractDashboardWidget {
|
||||
|
||||
private TextView gaugeValue;
|
||||
private ImageView gaugeBar;
|
||||
protected GaugeDrawer gaugeDrawer;
|
||||
|
||||
private final int label;
|
||||
private final String targetActivityTab;
|
||||
@ -66,6 +66,7 @@ public abstract class AbstractGaugeWidget extends AbstractDashboardWidget {
|
||||
|
||||
gaugeValue = fragmentView.findViewById(R.id.gauge_value);
|
||||
gaugeBar = fragmentView.findViewById(R.id.gauge_bar);
|
||||
gaugeDrawer = new GaugeDrawer();
|
||||
final TextView gaugeLabel = fragmentView.findViewById(R.id.gauge_label);
|
||||
gaugeLabel.setText(label);
|
||||
|
||||
@ -143,50 +144,7 @@ public abstract class AbstractGaugeWidget extends AbstractDashboardWidget {
|
||||
*/
|
||||
protected void drawSimpleGauge(final int color,
|
||||
final float value) {
|
||||
|
||||
final int width = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
150,
|
||||
GBApplication.getContext().getResources().getDisplayMetrics()
|
||||
);
|
||||
|
||||
// Draw gauge
|
||||
gaugeBar.setImageBitmap(drawSimpleGaugeInternal(
|
||||
width,
|
||||
Math.round(width * 0.075f),
|
||||
color,
|
||||
value
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param width Bitmap width in pixels
|
||||
* @param barWidth Gauge bar width in pixels
|
||||
* @param filledColor Color of the filled part of the gauge
|
||||
* @param filledFactor Factor between 0 and 1 that determines the amount of the gauge that should be filled
|
||||
* @return Bitmap containing the gauge
|
||||
*/
|
||||
private Bitmap drawSimpleGaugeInternal(final int width, final int barWidth, @ColorInt final int filledColor, final float filledFactor) {
|
||||
final int height = width / 2;
|
||||
final int barMargin = (int) Math.ceil(barWidth / 2f);
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
final Canvas canvas = new Canvas(bitmap);
|
||||
final Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
paint.setStrokeWidth(barWidth * 0.75f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180 + 180 * filledFactor, 180 - 180 * filledFactor, false, paint);
|
||||
|
||||
if (filledFactor >= 0) {
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(filledColor);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180, 180 * filledFactor, false, paint);
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
gaugeDrawer.drawSimpleGauge(gaugeBar, color, value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -203,116 +161,6 @@ public abstract class AbstractGaugeWidget extends AbstractDashboardWidget {
|
||||
final float value,
|
||||
final boolean fadeOutsideDot,
|
||||
final boolean gapBetweenSegments) {
|
||||
if (colors.length != segments.length) {
|
||||
LOG.error("Colors length {} differs from segments length {}", colors.length, segments.length);
|
||||
return;
|
||||
}
|
||||
|
||||
final int width = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
150,
|
||||
GBApplication.getContext().getResources().getDisplayMetrics()
|
||||
);
|
||||
|
||||
final int barWidth = Math.round(width * 0.075f);
|
||||
|
||||
final int height = width / 2;
|
||||
final int barMargin = (int) Math.ceil(barWidth / 2f);
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
final Canvas canvas = new Canvas(bitmap);
|
||||
final Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeCap(Paint.Cap.BUTT);
|
||||
paint.setStrokeWidth(barWidth);
|
||||
|
||||
final double cornersGapRadians = Math.asin((width * 0.055f) / (double) height);
|
||||
final double cornersGapFactor = cornersGapRadians / Math.PI;
|
||||
|
||||
int dotColor = 0;
|
||||
float angleSum = 0;
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
if (segments[i] == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
paint.setColor(colors[i]);
|
||||
paint.setStrokeWidth(barWidth);
|
||||
|
||||
if (value < 0 || (value >= angleSum && value <= angleSum + segments[i])) {
|
||||
dotColor = colors[i];
|
||||
} else {
|
||||
if (fadeOutsideDot) {
|
||||
paint.setColor(colors[i] - 0xB0000000);
|
||||
} else {
|
||||
paint.setStrokeWidth(barWidth * 0.75f);
|
||||
}
|
||||
}
|
||||
|
||||
float startAngleDegrees = 180 + angleSum * 180;
|
||||
float sweepAngleDegrees = segments[i] * 180;
|
||||
|
||||
if (value >= 0) {
|
||||
// Do not draw to the end if it will be overlapped by the dot
|
||||
if (i == 0 && value <= cornersGapFactor) {
|
||||
startAngleDegrees += (float) Math.toDegrees(cornersGapRadians);
|
||||
sweepAngleDegrees -= (float) Math.toDegrees(cornersGapRadians);
|
||||
} else if (i == segments.length - 1 && value >= 1 - cornersGapFactor) {
|
||||
sweepAngleDegrees -= (float) Math.toDegrees(cornersGapRadians);
|
||||
}
|
||||
}
|
||||
|
||||
if (gapBetweenSegments) {
|
||||
if (i + 1 < segments.length) {
|
||||
sweepAngleDegrees -= 2;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawArc(
|
||||
barMargin,
|
||||
barMargin,
|
||||
width - barMargin,
|
||||
width - barMargin,
|
||||
startAngleDegrees,
|
||||
sweepAngleDegrees,
|
||||
false,
|
||||
paint
|
||||
);
|
||||
angleSum += segments[i];
|
||||
}
|
||||
|
||||
if (value >= 0) {
|
||||
// Prevent the dot from going outside the widget in the extremities
|
||||
final float angleRadians = (float) normalize(value, 0, 1, cornersGapRadians, Math.toRadians(180) - cornersGapRadians);
|
||||
|
||||
paint.setColor(Color.TRANSPARENT);
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
|
||||
// In the corners the circle is slightly offset, so adjust it slightly
|
||||
final float widthAdjustment = width * 0.04f * (float) normalize(Math.abs(value - 0.5d), 0, 0.5d);
|
||||
|
||||
final float x = ((width - (barWidth / 2f) - widthAdjustment) / 2f) * (float) Math.cos(angleRadians);
|
||||
final float y = (height - (barWidth / 2f)) * (float) Math.sin(angleRadians);
|
||||
|
||||
// Draw hole
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
canvas.drawCircle((width / 2f) - x, height - y, barMargin * 1.6f, paint);
|
||||
|
||||
// Draw dot
|
||||
paint.setColor(dotColor);
|
||||
paint.setXfermode(null);
|
||||
canvas.drawCircle((width / 2f) - x, height - y, barMargin, paint);
|
||||
}
|
||||
|
||||
gaugeBar.setImageBitmap(bitmap);
|
||||
}
|
||||
|
||||
protected static double normalize(final double value, final double min, final double max) {
|
||||
return normalize(value, min, max, 0, 1);
|
||||
}
|
||||
|
||||
public static double normalize(final double value, final double minSource, final double maxSource, final double minTarget, final double maxTarget) {
|
||||
return ((value - minSource) * (maxTarget - minTarget)) / (maxSource - minSource) + minTarget;
|
||||
gaugeDrawer.drawSegmentedGauge(gaugeBar, colors, segments, value, fadeOutsideDot, gapBetweenSegments);
|
||||
}
|
||||
}
|
||||
|
@ -111,13 +111,13 @@ public class DashboardHrvWidget extends AbstractGaugeWidget {
|
||||
valueText = getString(R.string.hrv_status_unit, hrvData.weeklyAverage);
|
||||
|
||||
if (hrvData.weeklyAverage < hrvData.baselineLowUpper) {
|
||||
value = 0.125f * (float) normalize(hrvData.weeklyAverage, 0f, hrvData.baselineLowUpper);
|
||||
value = 0.125f * (float) GaugeDrawer.normalize(hrvData.weeklyAverage, 0f, hrvData.baselineLowUpper);
|
||||
} else if (hrvData.weeklyAverage < hrvData.baselineBalancedLower) {
|
||||
value = 0.125f + 0.125f * (float) normalize((float) hrvData.weeklyAverage, hrvData.baselineLowUpper, hrvData.baselineBalancedLower);
|
||||
value = 0.125f + 0.125f * (float) GaugeDrawer.normalize((float) hrvData.weeklyAverage, hrvData.baselineLowUpper, hrvData.baselineBalancedLower);
|
||||
} else if (hrvData.weeklyAverage < hrvData.baselineBalancedUpper) {
|
||||
value = 0.125f + 0.125f + 0.5f * (float) normalize((float) hrvData.weeklyAverage, hrvData.baselineBalancedLower, hrvData.baselineBalancedUpper);
|
||||
value = 0.125f + 0.125f + 0.5f * (float) GaugeDrawer.normalize((float) hrvData.weeklyAverage, hrvData.baselineBalancedLower, hrvData.baselineBalancedUpper);
|
||||
} else {
|
||||
value = 0.125f + 0.125f + 0.5f + 0.125f * (float) normalize((float) hrvData.weeklyAverage, hrvData.baselineBalancedUpper, 2 * hrvData.baselineBalancedUpper);
|
||||
value = 0.125f + 0.125f + 0.5f + 0.125f * (float) GaugeDrawer.normalize((float) hrvData.weeklyAverage, hrvData.baselineBalancedUpper, 2 * hrvData.baselineBalancedUpper);
|
||||
}
|
||||
} else {
|
||||
value = -1;
|
||||
|
@ -0,0 +1,205 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
|
||||
public class GaugeDrawer {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GaugeDrawer.class);
|
||||
protected @ColorInt int color_unknown = Color.argb(25, 128, 128, 128);
|
||||
|
||||
/**
|
||||
* Draw a simple gauge.
|
||||
*
|
||||
* @param color the gauge color
|
||||
* @param value the gauge value. Range: [0, 1]
|
||||
*/
|
||||
public void drawSimpleGauge(ImageView gaugeBar, final int color,
|
||||
final float value) {
|
||||
|
||||
final int width = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
150,
|
||||
GBApplication.getContext().getResources().getDisplayMetrics()
|
||||
);
|
||||
|
||||
// Draw gauge
|
||||
gaugeBar.setImageBitmap(drawSimpleGaugeInternal(
|
||||
width,
|
||||
Math.round(width * 0.075f),
|
||||
color,
|
||||
value
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param width Bitmap width in pixels
|
||||
* @param barWidth Gauge bar width in pixels
|
||||
* @param filledColor Color of the filled part of the gauge
|
||||
* @param filledFactor Factor between 0 and 1 that determines the amount of the gauge that should be filled
|
||||
* @return Bitmap containing the gauge
|
||||
*/
|
||||
private Bitmap drawSimpleGaugeInternal(final int width, final int barWidth, @ColorInt final int filledColor, final float filledFactor) {
|
||||
final int height = width / 2;
|
||||
final int barMargin = (int) Math.ceil(barWidth / 2f);
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
final Canvas canvas = new Canvas(bitmap);
|
||||
final Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
paint.setStrokeWidth(barWidth * 0.75f);
|
||||
paint.setColor(color_unknown);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180 + 180 * filledFactor, 180 - 180 * filledFactor, false, paint);
|
||||
|
||||
if (filledFactor >= 0) {
|
||||
paint.setStrokeWidth(barWidth);
|
||||
paint.setColor(filledColor);
|
||||
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180, 180 * filledFactor, false, paint);
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a segmented gauge.
|
||||
*
|
||||
* @param colors the colors of each segment
|
||||
* @param segments the size of each segment. The sum of all segments should be 1
|
||||
* @param value the gauge value, in range [0, 1], or -1 for no value and only segments
|
||||
* @param fadeOutsideDot whether to fade out colors outside the dot value
|
||||
* @param gapBetweenSegments whether to introduce a small gap between the segments
|
||||
*/
|
||||
public void drawSegmentedGauge(ImageView gaugeBar,
|
||||
final int[] colors,
|
||||
final float[] segments,
|
||||
final float value,
|
||||
final boolean fadeOutsideDot,
|
||||
final boolean gapBetweenSegments) {
|
||||
if (colors.length != segments.length) {
|
||||
LOG.error("Colors length {} differs from segments length {}", colors.length, segments.length);
|
||||
return;
|
||||
}
|
||||
|
||||
final int width = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
150,
|
||||
GBApplication.getContext().getResources().getDisplayMetrics()
|
||||
);
|
||||
|
||||
final int barWidth = Math.round(width * 0.075f);
|
||||
|
||||
final int height = width / 2;
|
||||
final int barMargin = (int) Math.ceil(barWidth / 2f);
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
final Canvas canvas = new Canvas(bitmap);
|
||||
final Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeCap(Paint.Cap.BUTT);
|
||||
paint.setStrokeWidth(barWidth);
|
||||
|
||||
final double cornersGapRadians = Math.asin((width * 0.055f) / (double) height);
|
||||
final double cornersGapFactor = cornersGapRadians / Math.PI;
|
||||
|
||||
int dotColor = 0;
|
||||
float angleSum = 0;
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
if (segments[i] == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
paint.setColor(colors[i]);
|
||||
paint.setStrokeWidth(barWidth);
|
||||
|
||||
if (value < 0 || (value >= angleSum && value <= angleSum + segments[i])) {
|
||||
dotColor = colors[i];
|
||||
} else {
|
||||
if (fadeOutsideDot) {
|
||||
paint.setColor(colors[i] - 0xB0000000);
|
||||
} else {
|
||||
paint.setStrokeWidth(barWidth * 0.75f);
|
||||
}
|
||||
}
|
||||
|
||||
float startAngleDegrees = 180 + angleSum * 180;
|
||||
float sweepAngleDegrees = segments[i] * 180;
|
||||
|
||||
if (value >= 0) {
|
||||
// Do not draw to the end if it will be overlapped by the dot
|
||||
if (i == 0 && value <= cornersGapFactor) {
|
||||
startAngleDegrees += (float) Math.toDegrees(cornersGapRadians);
|
||||
sweepAngleDegrees -= (float) Math.toDegrees(cornersGapRadians);
|
||||
} else if (i == segments.length - 1 && value >= 1 - cornersGapFactor) {
|
||||
sweepAngleDegrees -= (float) Math.toDegrees(cornersGapRadians);
|
||||
}
|
||||
}
|
||||
|
||||
if (gapBetweenSegments) {
|
||||
if (i + 1 < segments.length) {
|
||||
sweepAngleDegrees -= 2;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawArc(
|
||||
barMargin,
|
||||
barMargin,
|
||||
width - barMargin,
|
||||
width - barMargin,
|
||||
startAngleDegrees,
|
||||
sweepAngleDegrees,
|
||||
false,
|
||||
paint
|
||||
);
|
||||
angleSum += segments[i];
|
||||
}
|
||||
|
||||
if (value >= 0) {
|
||||
// Prevent the dot from going outside the widget in the extremities
|
||||
final float angleRadians = (float) normalize(value, 0, 1, cornersGapRadians, Math.toRadians(180) - cornersGapRadians);
|
||||
|
||||
paint.setColor(Color.TRANSPARENT);
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
|
||||
// In the corners the circle is slightly offset, so adjust it slightly
|
||||
final float widthAdjustment = width * 0.04f * (float) normalize(Math.abs(value - 0.5d), 0, 0.5d);
|
||||
|
||||
final float x = ((width - (barWidth / 2f) - widthAdjustment) / 2f) * (float) Math.cos(angleRadians);
|
||||
final float y = (height - (barWidth / 2f)) * (float) Math.sin(angleRadians);
|
||||
|
||||
// Draw hole
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
canvas.drawCircle((width / 2f) - x, height - y, barMargin * 1.6f, paint);
|
||||
|
||||
// Draw dot
|
||||
paint.setColor(dotColor);
|
||||
paint.setXfermode(null);
|
||||
canvas.drawCircle((width / 2f) - x, height - y, barMargin, paint);
|
||||
}
|
||||
|
||||
gaugeBar.setImageBitmap(bitmap);
|
||||
}
|
||||
|
||||
public static double normalize(final double value, final double min, final double max) {
|
||||
return normalize(value, min, max, 0, 1);
|
||||
}
|
||||
|
||||
public static double normalize(final double value, final double minSource, final double maxSource, final double minTarget, final double maxTarget) {
|
||||
return ((value - minSource) * (maxTarget - minTarget)) / (maxSource - minSource) + minTarget;
|
||||
}
|
||||
|
||||
}
|
@ -476,7 +476,22 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsVo2Max() {
|
||||
public boolean supportsVO2Max() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsVO2MaxCycling() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsVO2MaxRunning() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsVO2MaxGeneral() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -218,11 +218,11 @@ public interface DeviceCoordinator {
|
||||
boolean supportsStressMeasurement();
|
||||
|
||||
boolean supportsBodyEnergy();
|
||||
|
||||
boolean supportsHrvMeasurement();
|
||||
|
||||
boolean supportsVo2Max();
|
||||
|
||||
boolean supportsVO2Max();
|
||||
boolean supportsVO2MaxCycling();
|
||||
boolean supportsVO2MaxGeneral();
|
||||
boolean supportsVO2MaxRunning();
|
||||
boolean supportsSleepMeasurement();
|
||||
boolean supportsStepCounter();
|
||||
boolean supportsSpeedzones();
|
||||
|
@ -218,7 +218,17 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsVo2Max() {
|
||||
public boolean supportsVO2Max() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsVO2MaxCycling() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsVO2MaxRunning() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
157
app/src/main/res/layout/fragment_vo2max.xml
Normal file
157
app/src/main/res/layout/fragment_vo2max.xml
Normal file
@ -0,0 +1,157 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.VO2Max">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:id="@+id/vo2max_date_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
/>
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/tiles_grid_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="2"
|
||||
>
|
||||
<RelativeLayout
|
||||
android:id="@+id/vo2max_general_card_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="UselessParent"
|
||||
android:layout_gravity="fill"
|
||||
android:layout_columnWeight="1"
|
||||
>
|
||||
<ImageView
|
||||
android:id="@+id/vo2max_general_gauge"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:scaleType="fitStart" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/vo2max_general_gauge_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="28dp"
|
||||
android:text="@string/stats_empty_value"
|
||||
android:textSize="30sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/vo2max_general_gauge_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/vo2max_general_gauge_value"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/vo2max_general_gauge_label" />
|
||||
|
||||
</RelativeLayout>
|
||||
<RelativeLayout
|
||||
android:id="@+id/vo2max_running_card_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="UselessParent"
|
||||
android:layout_gravity="fill"
|
||||
android:layout_columnWeight="1"
|
||||
>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/vo2max_running_gauge"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:scaleType="fitStart" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/vo2max_running_gauge_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="28dp"
|
||||
android:text="@string/stats_empty_value"
|
||||
android:textSize="30sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/vo2max_running_gauge_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/vo2max_running_gauge_value"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/vo2max_running_gauge_label" />
|
||||
|
||||
</RelativeLayout>
|
||||
<RelativeLayout
|
||||
android:id="@+id/vo2max_cycling_card_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="UselessParent"
|
||||
android:layout_gravity="fill"
|
||||
android:layout_columnWeight="1"
|
||||
>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/vo2max_cycling_gauge"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="75dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:scaleType="fitStart" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/vo2max_cycling_gauge_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="28dp"
|
||||
android:text="@string/stats_empty_value"
|
||||
android:textSize="30sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/vo2max_cycling_gauge_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/vo2max_cycling_gauge_value"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/vo2max_cycling_gauge_label" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</GridLayout>
|
||||
<TextView
|
||||
android:layout_marginTop="30dp"
|
||||
android:layout_marginStart="25dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start"
|
||||
android:textSize="20sp"
|
||||
android:text="@string/thirty_days_timeline"
|
||||
/>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250sp"
|
||||
>
|
||||
<com.github.mikephil.charting.charts.LineChart
|
||||
android:id="@+id/vo2max_chart"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="2" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
@ -3041,6 +3041,7 @@
|
||||
<item>@string/weekstepschart_steps_a_week_or_month</item>
|
||||
<item>@string/pref_header_hrv_status</item>
|
||||
<item>@string/body_energy</item>
|
||||
<item>@string/vo2max</item>
|
||||
<item>@string/menuitem_stress</item>
|
||||
<item>@string/menuitem_pai</item>
|
||||
<item>@string/stats_title</item>
|
||||
@ -3057,6 +3058,7 @@
|
||||
<item>@string/p_steps_week</item>
|
||||
<item>@string/p_hrv_status</item>
|
||||
<item>@string/p_body_energy</item>
|
||||
<item>@string/p_vo2max</item>
|
||||
<item>@string/p_stress</item>
|
||||
<item>@string/p_pai</item>
|
||||
<item>@string/p_speed_zones</item>
|
||||
@ -3073,6 +3075,7 @@
|
||||
<item>@string/p_sleep</item>
|
||||
<item>@string/p_hrv_status</item>
|
||||
<item>@string/p_body_energy</item>
|
||||
<item>@string/p_vo2max</item>
|
||||
<item>@string/p_steps_week</item>
|
||||
<item>@string/p_stress</item>
|
||||
<item>@string/p_pai</item>
|
||||
@ -4174,6 +4177,7 @@
|
||||
<item>@string/active_time</item>
|
||||
<item>@string/menuitem_sleep</item>
|
||||
<item>@string/body_energy</item>
|
||||
<item>@string/vo2max</item>
|
||||
<item>@string/menuitem_stress_simple</item>
|
||||
<item>@string/menuitem_stress_segmented</item>
|
||||
<item>@string/menuitem_stress_breakdown</item>
|
||||
@ -4188,6 +4192,7 @@
|
||||
<item>activetime</item>
|
||||
<item>sleep</item>
|
||||
<item>bodyenergy</item>
|
||||
<item>vo2max</item>
|
||||
<item>stress_simple</item>
|
||||
<item>stress_segmented</item>
|
||||
<item>stress_breakdown</item>
|
||||
|
@ -51,6 +51,14 @@
|
||||
<color name="hrv_status_low" type="color">#fc5203</color>
|
||||
<color name="hrv_status_poor" type="color">#be03fc</color>
|
||||
<color name="hrv_status_char_line_color" type="color">#d12a2a</color>
|
||||
<color name="vo2max_running_char_line_color" type="color">#46acea</color>
|
||||
<color name="vo2max_cycling_char_line_color" type="color">#59b22c</color>
|
||||
<color name="vo2max_general_char_line_color" type="color">#824be3</color>
|
||||
<color name="vo2max_value_poor_color" type="color">#d93832</color>
|
||||
<color name="vo2max_value_fair_color" type="color">#ffa703</color>
|
||||
<color name="vo2max_value_good_color" type="color">#04c79c</color>
|
||||
<color name="vo2max_value_excellent_color" type="color">#02a8e6</color>
|
||||
<color name="vo2max_value_superior_color" type="color">#824be3</color>
|
||||
<color name="body_energy_level_color" type="color">#5ac234</color>
|
||||
<color name="body_energy_lost_color" type="color">#ff6c43</color>
|
||||
<color name="steps_color" type="color">#00c9bf</color>
|
||||
|
@ -1623,6 +1623,9 @@
|
||||
<string name="hrv_status_unit">%1$d ms</string>
|
||||
<string name="hrv_status_baseline">%1$d-%2$d ms</string>
|
||||
<string name="hrv_status_baseline_label">Baseline</string>
|
||||
<string name="vo2_max_running">VO2Max Running</string>
|
||||
<string name="vo2_max_cycling">VO2Max Cycling</string>
|
||||
<string name="vo2_max_general">VO2Max General</string>
|
||||
<string name="bpm_value_unit">%1$d bpm</string>
|
||||
<string name="steps_distance_unit">%1$,.2f km</string>
|
||||
<string name="body_energy_gained">Gained</string>
|
||||
@ -1990,6 +1993,9 @@
|
||||
<string name="warning">Warning!</string>
|
||||
<string name="note">Note</string>
|
||||
<string name="no_data">No data</string>
|
||||
<string name="vo2max_general_gauge_label">VO2Max</string>
|
||||
<string name="vo2max_running_gauge_label">Running VO2Max</string>
|
||||
<string name="vo2max_cycling_gauge_label">Cycling VO2Max</string>
|
||||
<!-- LED Color -->
|
||||
<string name="preferences_led_color">LED Color</string>
|
||||
<!-- FM transmitters -->
|
||||
@ -2530,6 +2536,8 @@
|
||||
<string name="pref_header_spo2">Blood Oxygen</string>
|
||||
<string name="pref_header_hrv_status">HRV Status</string>
|
||||
<string name="body_energy">Body Energy</string>
|
||||
<string name="vo2max">VO2 Max</string>
|
||||
<string name="thirty_days_timeline">30 Days Timeline</string>
|
||||
<string name="pref_header_sony_ambient_sound_control">Ambient Sound Control</string>
|
||||
<string name="pref_header_sony_sound_control">Sound Control</string>
|
||||
<string name="pref_header_sony_device_info">Device Information</string>
|
||||
|
@ -109,6 +109,7 @@
|
||||
<item name="p_speed_zones" type="string">speedzones</item>
|
||||
<item name="p_hrv_status" type="string">hrvstatus</item>
|
||||
<item name="p_body_energy" type="string">bodyenergy</item>
|
||||
<item name="p_vo2max" type="string">vo2max</item>
|
||||
<item name="p_live_stats" type="string">livestats</item>
|
||||
<item name="p_spo2" type="string">spo2</item>
|
||||
<item name="p_temperature" type="string">temperature</item>
|
||||
|
Loading…
Reference in New Issue
Block a user