From 39c993f7498460c02a99b4d1ad56adce6cad1435 Mon Sep 17 00:00:00 2001 From: Vasja Volin Date: Wed, 22 Jan 2025 21:05:17 -0800 Subject: [PATCH] Add settings toggles for chart series Adds toggles to the User Interface section of the app Settings that allow the user to display or hide the three series available in the chart: Elevation, Speed/Pace, and Heart Rate. As a user of a heart rate monitor primarily during strength workouts and combat sports, the distance and elevation graphs are not helpful to me - in addition, their scale means that I am not able to easily see my exact heart rate. This should allow users in a similar situation to better determine exactly what is most helpful for them to be seen on the graph. Also updates heart rate series possible intervals - the smallest interval was 25, which was too low for most zone 1 cardio charts - if your heart rate goes from 70 to 125, with a 25 interval, most of the chart will be whitespace. --- .../opentracks/chart/ChartFragment.java | 25 ++++++ .../opentracks/chart/ChartView.java | 82 ++++++++++++++----- .../opentracks/settings/PreferencesUtils.java | 15 ++++ src/main/res/values/settings.xml | 31 +++++-- src/main/res/values/strings.xml | 5 ++ src/main/res/xml/settings_user_interface.xml | 19 +++++ 6 files changed, 150 insertions(+), 27 deletions(-) diff --git a/src/main/java/de/dennisguse/opentracks/chart/ChartFragment.java b/src/main/java/de/dennisguse/opentracks/chart/ChartFragment.java index a847f8a26d..11a7357343 100644 --- a/src/main/java/de/dennisguse/opentracks/chart/ChartFragment.java +++ b/src/main/java/de/dennisguse/opentracks/chart/ChartFragment.java @@ -86,6 +86,7 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin }); } } + if (PreferencesUtils.isKey(R.string.stats_rate_key, key)) { boolean reportSpeed = PreferencesUtils.isReportSpeed(activityTypeLocalized); if (reportSpeed != viewBinding.chartView.getReportSpeed()) { @@ -99,6 +100,21 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin }); } } + + if (PreferencesUtils.isKey(R.string.chart_display_elevation_key, key)) { + viewBinding.chartView.setShowElevation(PreferencesUtils.shouldShowElevation()); + refreshChart(); + } + + if (PreferencesUtils.isKey(R.string.chart_display_pace_or_speed_key, key)) { + viewBinding.chartView.setShowPaceOrSpeed(PreferencesUtils.shouldShowPaceOrSpeed()); + refreshChart(); + } + + if (PreferencesUtils.isKey(R.string.chart_display_heart_rate_key, key)) { + viewBinding.chartView.setShowHeartRate(PreferencesUtils.shouldShowHeartRate()); + refreshChart(); + } } }; @@ -273,4 +289,13 @@ private void runOnUiThread(Runnable runnable) { fragmentActivity.runOnUiThread(runnable); } } + + private void refreshChart() { + runOnUiThread(() -> { + if (isResumed()) { + viewBinding.chartView.invalidate(); + viewBinding.chartView.requestLayout(); + } + }); + } } diff --git a/src/main/java/de/dennisguse/opentracks/chart/ChartView.java b/src/main/java/de/dennisguse/opentracks/chart/ChartView.java index ff96cac673..b95a390cd9 100644 --- a/src/main/java/de/dennisguse/opentracks/chart/ChartView.java +++ b/src/main/java/de/dennisguse/opentracks/chart/ChartView.java @@ -86,8 +86,10 @@ public class ChartView extends View { } private final List seriesList = new LinkedList<>(); + private final ChartValueSeries elevationSeries; private final ChartValueSeries speedSeries; private final ChartValueSeries paceSeries; + private final ChartValueSeries heartRateSeries; private final LinkedList chartPoints = new LinkedList<>(); private final List markers = new LinkedList<>(); @@ -123,6 +125,7 @@ public class ChartView extends View { private boolean chartByDistance = false; private UnitSystem unitSystem = UnitSystem.defaultUnitSystem(); private boolean reportSpeed = true; + private boolean showPaceOrSpeed = true; private boolean showPointer = false; private final GestureDetectorCompat detectorScrollFlingTab = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() { @@ -176,17 +179,17 @@ public ChartView(Context context, AttributeSet attributeSet) { int fontSizeSmall = ThemeUtils.getFontSizeSmallInPx(context); int fontSizeMedium = ThemeUtils.getFontSizeMediumInPx(context); - seriesList.add(new ChartValueSeries(context, - Integer.MIN_VALUE, - Integer.MAX_VALUE, - new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}, - R.string.description_altitude_metric, - R.string.description_altitude_imperial, - R.string.description_altitude_imperial, - R.color.chart_altitude_fill, - R.color.chart_altitude_border, - fontSizeSmall, - fontSizeMedium) { + elevationSeries = new ChartValueSeries(context, + Integer.MIN_VALUE, + Integer.MAX_VALUE, + new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}, + R.string.description_altitude_metric, + R.string.description_altitude_imperial, + R.string.description_altitude_imperial, + R.color.chart_altitude_fill, + R.color.chart_altitude_border, + fontSizeSmall, + fontSizeMedium) { @Override protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) { return chartPoint.altitude(); @@ -196,7 +199,8 @@ protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) { protected boolean drawIfChartPointHasNoData() { return false; } - }); + }; + seriesList.add(elevationSeries); speedSeries = new ChartValueSeries(context, 0, @@ -244,10 +248,15 @@ protected boolean drawIfChartPointHasNoData() { }; seriesList.add(paceSeries); - seriesList.add(new ChartValueSeries(context, + heartRateSeries = new ChartValueSeries(context, 0, Integer.MAX_VALUE, - new int[]{25, 50}, + // For Zone 5 cardio, the 25 value should result in nice visuals, as the values + // will range from ~70 - ~180 (around 4.5 intervals). + // For Zone 1 cardio, the 15 value should result in nice visuals, as the values + // will range from ~70 - ~120 (around 3.5 intervals) + // The fallback of 50 should give appropriate visuals for values outside this range. + new int[]{15, 25, 50}, R.string.description_sensor_heart_rate, R.string.description_sensor_heart_rate, R.string.description_sensor_heart_rate, @@ -264,7 +273,8 @@ protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) { protected boolean drawIfChartPointHasNoData() { return false; } - }); + }; + seriesList.add(heartRateSeries); seriesList.add(new ChartValueSeries(context, 0, @@ -343,9 +353,15 @@ protected boolean drawIfChartPointHasNoData() { setClickable(true); updateDimensions(); - // either speedSeries or paceSeries should be enabled. - speedSeries.setEnabled(reportSpeed); - paceSeries.setEnabled(!reportSpeed); + // Either speedSeries or paceSeries should be enabled, if one is shown. + if (showPaceOrSpeed) { + speedSeries.setEnabled(reportSpeed); + paceSeries.setEnabled(!reportSpeed); + } + + // Defaults for our chart series. + heartRateSeries.setEnabled(true); + elevationSeries.setEnabled(true); } @Override @@ -379,6 +395,12 @@ public void setReportSpeed(boolean value) { } public boolean applyReportSpeed() { + if (!showPaceOrSpeed) { + paceSeries.setEnabled(false); + speedSeries.setEnabled(false); + return true; + } + if (reportSpeed) { if (!speedSeries.isEnabled()) { speedSeries.setEnabled(true); @@ -396,6 +418,20 @@ public boolean applyReportSpeed() { return false; } + void setShowElevation(boolean value) { + elevationSeries.setEnabled(value); + } + void setShowPaceOrSpeed(boolean value) { + showPaceOrSpeed = value; + + // we want to make sure we show whatever version the user has + // selected when we turn this back on. + applyReportSpeed(); + } + void setShowHeartRate(boolean value) { + heartRateSeries.setEnabled(value); + } + public void setShowPointer(boolean value) { showPointer = value; } @@ -644,10 +680,18 @@ private record TitleDimensions( */ private void drawSeriesTitles(Canvas canvas) { Iterator tpI = titleDimensions.titlePositions.iterator(); + for (ChartValueSeries chartValueSeries : seriesList) { if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) { String title = getContext().getString(chartValueSeries.getTitleId(unitSystem)); Paint paint = chartValueSeries.getTitlePaint(); + + // It is possible for the titlePositions to become empty temporarily, while switching between + // chart screens quickly. + if (!tpI.hasNext()) { + return; + } + TitlePosition tp = tpI.next(); int y = topBorder - spacer - (titleDimensions.lineCount - tp.line) * (titleDimensions.lineHeight + spacer); canvas.drawText(title, tp.xPos + getScrollX(), y, paint); @@ -774,7 +818,7 @@ private void drawYAxis(Canvas canvas) { //TODO int markerXPosition = x - spacer; - int index = titleDimensions.titlePositions.size() - 1; // index only onver the visible chart series + int index = titleDimensions.titlePositions.size() - 1; // index only over the visible chart series final int lastDrawn2ndLineMarkerIndex = getYmarkerCountOn1stLine(); for (int i = seriesList.size()-1; i>=0 ;--i) { // draw markers from the last series to achieve right alignment ChartValueSeries chartValueSeries = seriesList.get(i); diff --git a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java index 38cfbe60ae..af3f002c2e 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java @@ -318,6 +318,21 @@ public static boolean shouldUseFullscreen() { return getBoolean(R.string.stats_fullscreen_while_recording_key, DEFAULT); } + public static boolean shouldShowElevation() { + final boolean DEFAULT = resources.getBoolean(R.bool.chart_display_elevation_default); + return getBoolean(R.string.chart_display_elevation_key, DEFAULT); + } + + public static boolean shouldShowPaceOrSpeed() { + final boolean DEFAULT = resources.getBoolean(R.bool.chart_display_pace_or_speed_default); + return getBoolean(R.string.chart_display_pace_or_speed_key, DEFAULT); + } + + public static boolean shouldShowHeartRate() { + final boolean DEFAULT = resources.getBoolean(R.bool.chart_display_heart_rate_default); + return getBoolean(R.string.chart_display_heart_rate_key, DEFAULT); + } + public static boolean shouldUseDynamicColors() { final boolean DEFAULT = resources.getBoolean(R.bool.settings_ui_dynamic_colors_default); return getBoolean(R.string.settings_ui_dynamic_colors_key, DEFAULT); diff --git a/src/main/res/values/settings.xml b/src/main/res/values/settings.xml index 4a3ac2651c..db5faac93e 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -1,14 +1,6 @@ - trackdetail_show_on_lockscreen_while_recording - false - - trackdetail_keep_screen_on_while_recording - false - - trackdetail_fullscreen_while_recording - false settingsDefaults @@ -279,9 +271,32 @@ showIntroduction true + + uiDynamicColors false + + trackdetail_show_on_lockscreen_while_recording + false + + trackdetail_keep_screen_on_while_recording + false + + trackdetail_fullscreen_while_recording + false + + + chart_display_elevation + true + + chart_display_pace_or_speed + true + + chart_display_heart_rate + true + + localeKey diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f9de7b82f7..baf41380fe 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -344,6 +344,11 @@ limitations under the License. By distance By time + Charts + Display elevation + Display pace or speed + Display heart rate + Recording Theme changes may require a manual restart. diff --git a/src/main/res/xml/settings_user_interface.xml b/src/main/res/xml/settings_user_interface.xml index e1eb4fa90e..9596502f64 100644 --- a/src/main/res/xml/settings_user_interface.xml +++ b/src/main/res/xml/settings_user_interface.xml @@ -58,4 +58,23 @@ android:title="@string/settings_recording_fullscreen_on_while_recording_title" /> + + + + + + + \ No newline at end of file