From 6bfd3dcd064e199d8dc6ec46ee4722e9c21f2cc2 Mon Sep 17 00:00:00 2001 From: Severin von Wnuck-Lipinski Date: Sun, 25 Aug 2024 20:16:11 +0200 Subject: [PATCH] Add weight chart --- .../charts/ActivityChartsActivity.java | 7 + .../charts/WeightChartFragment.java | 252 ++++++++++++++++++ .../main/res/layout/fragment_weightchart.xml | 78 ++++++ app/src/main/res/values/arrays.xml | 3 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/values.xml | 1 + 6 files changed, 345 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeightChartFragment.java create mode 100644 app/src/main/res/layout/fragment_weightchart.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java index 983b40007..3e3d0ceea 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java @@ -110,6 +110,9 @@ public class ActivityChartsActivity extends AbstractChartsActivity { if(!coordinator.supportsCyclingData()) { tabList.remove("cycling"); } + if (!coordinator.supportsWeightMeasurement()) { + tabList.remove("weight"); + } if (!coordinator.supportsHrvMeasurement()) { tabList.remove("hrvstatus"); } @@ -163,6 +166,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity { return new TemperatureChartFragment(); case "cycling": return new CyclingChartFragment(); + case "weight": + return new WeightChartFragment(); } return null; } @@ -209,6 +214,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity { return getString(R.string.menuitem_temperature); case "cycling": return getString(R.string.title_cycling); + case "weight": + return getString(R.string.menuitem_weight); } return super.getPageTitle(position); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeightChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeightChartFragment.java new file mode 100644 index 000000000..f2d3364b5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeightChartFragment.java @@ -0,0 +1,252 @@ +/* Copyright (C) 2024 Severin von Wnuck-Lipinski + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +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.TextView; + +import androidx.fragment.app.Fragment; + +import com.github.mikephil.charting.animation.Easing; +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.LimitLine; +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 java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.WeightSample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; + +public class WeightChartFragment extends AbstractChartFragment { + private int colorBackground; + private int colorSecondaryText; + + private int totalDays; + private boolean imperialUnits; + private int weightTargetKg; + + private LineChart chart; + private TextView textTimeSpan; + private TextView textWeightLatest; + private TextView textWeightTarget; + + @Override + public String getTitle() { + return getString(R.string.menuitem_weight); + } + + @Override + protected void init() { + GBPrefs prefs = GBApplication.getPrefs(); + + colorBackground = GBApplication.getBackgroundColor(requireContext()); + colorSecondaryText = GBApplication.getSecondaryTextColor(requireContext()); + + if (prefs.getBoolean("charts_range", true)) + totalDays = 30; + else + totalDays = 7; + + String unitSystem = prefs.getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, getString(R.string.p_unit_metric)); + + if (unitSystem.equals(getString(R.string.p_unit_imperial))) + imperialUnits = true; + else + imperialUnits = false; + + weightTargetKg = prefs.getInt(ActivityUser.PREF_USER_GOAL_WEIGHT_KG, ActivityUser.defaultUserGoalWeightKg); + } + + @Override + protected WeightChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { + long tsStart = getTSStart() * 1000L; + long tsEnd = getTSEnd() * 1000L; + + DeviceCoordinator coordinator = device.getDeviceCoordinator(); + TimeSampleProvider provider = coordinator.getWeightSampleProvider(device, db.getDaoSession()); + List samples = provider.getAllSamples(tsStart, tsEnd); + WeightSample latestSample = provider.getLatestSample(); + + return createChartsData(samples, latestSample); + } + + @Override + protected void renderCharts() { + chart.animateX(ANIM_TIME, Easing.EaseInOutQuart); + } + + @Override + protected void setupLegend(Chart chart) {} + + @Override + protected void updateChartsnUIThread(WeightChartsData chartsData) { + chart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317 + chart.getXAxis().setValueFormatter(chartsData.getXValueFormatter()); + chart.getXAxis().setAvoidFirstLastClipping(true); + chart.setData(chartsData.getData()); + + Date dateStart = DateTimeUtils.parseTimeStamp(getTSStart()); + Date dateEnd = DateTimeUtils.parseTimeStamp(getTSEnd()); + SimpleDateFormat format = new SimpleDateFormat("E, MMM dd"); + WeightSample latestSample = chartsData.getLatestSample(); + + textTimeSpan.setText(format.format(dateStart) + " - " + format.format(dateEnd)); + + if (latestSample != null) + textWeightLatest.setText(formatWeight(weightFromKg(latestSample.getWeightKg()))); + + textWeightTarget.setText(formatWeight(weightFromKg(weightTargetKg))); + } + + @Override + protected int getTSStart() { + return DateTimeUtils.shiftDays(getTSEnd(), -totalDays + 1); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_weightchart, container, false); + + chart = rootView.findViewById(R.id.weight_chart); + textTimeSpan = rootView.findViewById(R.id.weight_time_span_text); + textWeightLatest = rootView.findViewById(R.id.weight_latest_text); + textWeightTarget = rootView.findViewById(R.id.weight_target_text); + + configureBarLineChartDefaults(chart); + chart.setBackgroundColor(colorBackground); + chart.getDescription().setEnabled(false); + chart.getLegend().setEnabled(false); + chart.getAxisRight().setEnabled(false); + chart.setDoubleTapToZoomEnabled(false); + + LimitLine targetLine = new LimitLine(weightFromKg(weightTargetKg)); + targetLine.setTextColor(colorSecondaryText); + + XAxis xAxis = chart.getXAxis(); + xAxis.setTextColor(colorSecondaryText); + xAxis.setDrawLabels(true); + xAxis.setDrawLimitLinesBehindData(true); + + YAxis yAxis = chart.getAxisLeft(); + yAxis.setTextColor(colorSecondaryText); + yAxis.addLimitLine(targetLine); + yAxis.setDrawGridLines(true); + + refresh(); + + return rootView; + } + + private WeightChartsData createChartsData(List samples, WeightSample latestSample) { + List entries = new ArrayList<>(); + TimestampTranslation tsTranslation = new TimestampTranslation(); + + for (WeightSample sample : samples) { + int tsSeconds = (int)(sample.getTimestamp() / 1000L); + float weight = weightFromKg(sample.getWeightKg()); + + entries.add(new Entry(tsTranslation.shorten(tsSeconds), weight)); + } + + LineDataSet dataSet = new LineDataSet(entries, getString(R.string.menuitem_weight)); + dataSet.setLineWidth(2.2f); + dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); + dataSet.setCubicIntensity(0.1f); + dataSet.setCircleRadius(5); + dataSet.setDrawCircleHole(false); + dataSet.setDrawValues(true); + dataSet.setValueTextSize(10); + dataSet.setValueTextColor(colorSecondaryText); + dataSet.setValueFormatter(new ValueFormatter() { + @Override + public String getPointLabel(Entry entry) { + return formatWeight(entry.getY()); + } + }); + + return new WeightChartsData(new LineData(dataSet), tsTranslation, latestSample); + } + + private float weightFromKg(float weight) { + // Convert to lbs + if (imperialUnits) + weight *= 2.2046226f; + + return weight; + } + + private String formatWeight(float weight) { + int weightString = imperialUnits ? R.string.weight_lbs : R.string.weight_kg; + + return getString(weightString, weight); + } + + protected static class WeightChartsData extends DefaultChartsData { + private final WeightSample latestSample; + + public WeightChartsData(LineData lineData, TimestampTranslation tsTranslation, WeightSample latestSample) { + super(lineData, new DateFormatter(tsTranslation)); + + this.latestSample = latestSample; + } + + private WeightSample getLatestSample() { + return latestSample; + } + } + + private static class DateFormatter extends ValueFormatter { + private TimestampTranslation translation; + private SimpleDateFormat format = new SimpleDateFormat("dd.MM."); + private Calendar calendar = GregorianCalendar.getInstance(); + + public DateFormatter(TimestampTranslation translation) { + this.translation = translation; + } + + @Override + public String getFormattedValue(float value) { + calendar.clear(); + calendar.setTimeInMillis(translation.toOriginalValue((int)value) * 1000L); + + return format.format(calendar.getTime()); + } + } +} diff --git a/app/src/main/res/layout/fragment_weightchart.xml b/app/src/main/res/layout/fragment_weightchart.xml new file mode 100644 index 000000000..3bb535d60 --- /dev/null +++ b/app/src/main/res/layout/fragment_weightchart.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index a28d0ad16..2d457e985 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -3041,6 +3041,7 @@ @string/liveactivity_live_activity @string/pref_header_spo2 @string/menuitem_temperature + @string/menuitem_weight @@ -3056,6 +3057,7 @@ @string/p_live_stats @string/p_spo2 @string/p_temperature + @string/p_weight @@ -3072,6 +3074,7 @@ @string/p_live_stats @string/p_spo2 @string/p_temperature + @string/p_weight diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d9cb4614..5d4af8401 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1886,6 +1886,7 @@ Events Widgets Temperature + Weight Barometer Flashlight E-mail @@ -2491,6 +2492,9 @@ High Total Day increase + %1$.2f kg + %1$.2f lbs + Target Mode Off Noise Cancelling diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index dbc8f1333..18ffb9d4d 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -112,6 +112,7 @@ livestats spo2 temperature + weight off complete