Added intervals in StatisticsRecordedFragment with distance, speed and pace for all activity types.

Users can select the length of the interval (in a spinner with several options).
The intervals will be in km or mi depending on settings.
This commit is contained in:
Román
2020-08-10 19:16:23 +02:00
committed by Román Martínez
parent 19e35a7609
commit 7a923a947e
10 changed files with 482 additions and 1 deletions
@@ -0,0 +1,63 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.Interval> {
private boolean metricUnits;
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList) {
super(context, R.layout.interval_stats_list_item, intervalList);
metricUnits = PreferencesUtils.isMetricUnits(context);
}
@NonNull
@Override
public View getView(int position, @Nullable View intervalView, @NonNull ViewGroup parent) {
IntervalStatistics.Interval interval = getItem(position);
ViewHolder viewHolder;
if (intervalView == null) {
viewHolder = new ViewHolder();
intervalView = LayoutInflater.from(getContext()).inflate(R.layout.interval_stats_list_item, parent, false);
viewHolder.distance = intervalView.findViewById(R.id.interval_item_distance);
viewHolder.speed = intervalView.findViewById(R.id.interval_item_speed);
viewHolder.pace = intervalView.findViewById(R.id.interval_item_pace);
intervalView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) intervalView.getTag();
}
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), interval.getDistance(), metricUnits));
Pair<String, String> speedParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed(), metricUnits, true);
viewHolder.speed.setText(speedParts.first + " " + speedParts.second);
viewHolder.pace.setText(StringUtils.getSpeedParts(getContext(), interval.getSpeed(), metricUnits, false).first);
return intervalView;
}
private static class ViewHolder {
private TextView distance;
private TextView speed;
private TextView pace;
}
}
@@ -24,12 +24,14 @@ import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
@@ -39,6 +41,8 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
import de.dennisguse.opentracks.views.IntervalListView;
/**
* A fragment to display track statistics to the user.
@@ -46,7 +50,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class StatisticsRecordedFragment extends Fragment {
public class StatisticsRecordedFragment extends Fragment implements IntervalListView.IntervalListListener {
private static final String TRACK_ID_KEY = "trackId";
@@ -54,6 +58,9 @@ public class StatisticsRecordedFragment extends Fragment {
private String category = "";
private Track track;
private IntervalStatisticsModel viewModel = null;
private IntervalListView intervalListView = null;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (isResumed()) {
@@ -72,6 +79,7 @@ public class StatisticsRecordedFragment extends Fragment {
}
/* Views */
private ViewGroup rootView;
private TextView totalTimeValueView;
private TextView distanceValue;
private TextView distanceUnit;
@@ -110,6 +118,8 @@ public class StatisticsRecordedFragment extends Fragment {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
rootView = view.findViewById(R.id.root_view);
totalTimeValueView = view.findViewById(R.id.stats_total_time_value);
distanceValue = view.findViewById(R.id.stats_distance_value);
@@ -153,6 +163,21 @@ public class StatisticsRecordedFragment extends Fragment {
});
}
private void addIntervals() {
intervalListView = new IntervalListView(getActivity(), this);
intervalListView.setId(View.generateViewId());
LinearLayout linearLayoutExtra = rootView.findViewById(R.id.linear_layout_extra);
linearLayoutExtra.removeAllViews();
linearLayoutExtra.addView(intervalListView);
viewModel = new ViewModelProvider(this).get(IntervalStatisticsModel.class);
viewModel.getIntervalStats(track.getId()).observe(getActivity(), intervalStatistics -> {
if (intervalStatistics != null) {
intervalListView.display(intervalStatistics.getIntervalList());
}
});
}
@Override
public void onResume() {
super.onResume();
@@ -269,5 +294,20 @@ public class StatisticsRecordedFragment extends Fragment {
speedMovingValue.setText(parts.first);
speedMovingUnit.setText(parts.second);
}
// Set intervals.
addIntervals();
}
@Override
public void intervalChanged(IntervalStatisticsModel.IntervalOption interval) {
if (viewModel != null && intervalListView != null) {
viewModel = new ViewModelProvider(this).get(IntervalStatisticsModel.class);
viewModel.getIntervalStats(track.getId(), interval).observe(getActivity(), intervalStatistics -> {
if (intervalStatistics != null) {
intervalListView.display(intervalStatistics.getIntervalList());
}
});
}
}
}
@@ -51,6 +51,8 @@ public class UnitConversions {
public static final double MM_TO_M = 0.001;
// Distance
// multiplication factor to convert miles to meters
public static final double MI_TO_M = 1609.344;
// multiplication factor to convert miles to feet
public static final double MI_TO_FT = 5280.0;
// multiplication factor to convert feet to miles
@@ -0,0 +1,67 @@
package de.dennisguse.opentracks.viewmodels;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions;
public class IntervalStatistics {
List<Interval> intervalList;
public IntervalStatistics() {
intervalList = new ArrayList<>();
}
public void build(List<TrackPoint> trackPointList, float distanceInterval) {
intervalList = new ArrayList<>();
if (trackPointList == null || trackPointList.size() == 0) {
return;
}
Interval interval = new Interval();
for (int i = 1; i < trackPointList.size(); i++) {
TrackPoint prevTrackPoint = trackPointList.get(i - 1);
TrackPoint trackPoint = trackPointList.get(i);
if (LocationUtils.isValidLocation(trackPoint.getLocation()) && LocationUtils.isValidLocation(prevTrackPoint.getLocation())) {
interval.distance += prevTrackPoint.distanceTo(trackPoint);
interval.time += trackPoint.getTime() - prevTrackPoint.getTime();
if (interval.distance >= distanceInterval) {
intervalList.add(interval);
interval = new Interval();
}
}
}
if (interval.distance > 1f) {
intervalList.add(interval);
}
}
public List<Interval> getIntervalList() {
return intervalList;
}
public static class Interval {
float distance = 0f;
float time = 0f;
public float getDistance() {
return distance;
}
/**
* @return speed of the interval in m/s.
*/
public float getSpeed() {
if (distance == 0f) {
return 0f;
}
return distance / (float) (time * UnitConversions.MS_TO_S);
}
}
}
@@ -0,0 +1,98 @@
package de.dennisguse.opentracks.viewmodels;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.List;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
public class IntervalStatisticsModel extends AndroidViewModel {
private MutableLiveData<IntervalStatistics> intervalStats;
private IntervalOption interval;
public IntervalStatisticsModel(@NonNull Application application) {
super(application);
}
public LiveData<IntervalStatistics> getIntervalStats(long trackId, IntervalOption interval) {
if (intervalStats == null || this.interval != interval) {
intervalStats = new MutableLiveData<>();
this.interval = interval;
loadIntervalStats(trackId);
}
return intervalStats;
}
public LiveData<IntervalStatistics> getIntervalStats(long trackId) {
return getIntervalStats(trackId, IntervalOption.OPTION_1);
}
private void loadIntervalStats(long trackId) {
new Thread(() -> {
Context context = getApplication().getApplicationContext();
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
List<TrackPoint> trackPointList = contentProviderUtils.getTrackPoints(trackId);
IntervalStatistics intervalStatistics = new IntervalStatistics();
float distanceInterval = PreferencesUtils.isMetricUnits(context) ? (float) (interval.getValue() * UnitConversions.KM_TO_M) : (float) (interval.getValue() * UnitConversions.MI_TO_M);
intervalStatistics.build(trackPointList, distanceInterval);
intervalStats.postValue(intervalStatistics);
}).start();
}
public enum IntervalOption {
OPTION_1(1),
OPTION_2(2),
OPTION_3(3),
OPTION_4(4),
OPTION_5(5),
OPTION_10(10);
private int value;
IntervalOption(int value) {
this.value = value;
}
/**
* @param pos position of the interval option.
* @return the interval option that is in the position pos.
*/
public static IntervalOption getIntervalOption(int pos) {
if (values().length > pos) {
return values()[pos];
} else {
return OPTION_1;
}
}
public int getValue() {
return value;
}
/**
* @return a string array with all options.
*/
public static String[] getAllValues() {
IntervalOption[] options = values();
String[] values = new String[options.length];
for (int i = 0; i < options.length; i++) {
values[i] = String.valueOf(options[i].getValue());
}
return values;
}
}
}
@@ -0,0 +1,72 @@
package de.dennisguse.opentracks.views;
import android.content.Context;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
public class IntervalListView extends LinearLayout {
private IntervalStatisticsAdapter adapter;
private LinearLayout linearLayoutIntervals;
private Spinner spinnerIntervals;
private TextView spinnerIntervalsUnit;
private Context context;
private IntervalListListener listener;
public IntervalListView(Context context, IntervalListListener listener) {
super(context);
this.context = context;
this.listener = listener;
init();
}
private void init() {
inflate(getContext(), R.layout.interval_list_view, this);
linearLayoutIntervals = findViewById(R.id.interval_list);
spinnerIntervals = findViewById(R.id.spinner_intervals);
spinnerIntervals.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.getAllValues()));
spinnerIntervalsUnit = findViewById(R.id.spinner_intervals_unit);
spinnerIntervalsUnit.setText(PreferencesUtils.isMetricUnits(context) ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile));
spinnerIntervals.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
IntervalStatisticsModel.IntervalOption interval = IntervalStatisticsModel.IntervalOption.getIntervalOption(i);
listener.intervalChanged(interval);
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
}
public void display(List<IntervalStatistics.Interval> intervalList) {
if (intervalList != null) {
adapter = new IntervalStatisticsAdapter(getContext(), intervalList);
linearLayoutIntervals.removeAllViews();
for (int i = 0; i < adapter.getCount(); i++) {
View intervalView = adapter.getView(i, null, linearLayoutIntervals);
linearLayoutIntervals.addView(intervalView);
}
}
}
public interface IntervalListListener {
void intervalChanged(IntervalStatisticsModel.IntervalOption interval);
}
}
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/StatsSmallLabel"
android:text="@string/stats_intervals" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stats_split_interval"
style="@style/TextMedium" />
<Spinner
android:id="@+id/spinner_intervals"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/SpinnerCustom"/>
<TextView
android:id="@+id/spinner_intervals_unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/unit_kilometer"
style="@style/TextMedium" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/interval_distance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/TextMedium"
android:layout_margin="1dp"
android:textAlignment="center"
android:layout_gravity="center"
android:text="@string/stats_distance" />
<TextView
android:id="@+id/interval_speed"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/TextMedium"
android:layout_margin="1dp"
android:textAlignment="center"
android:layout_gravity="center"
android:text="@string/stats_speed" />
<TextView
android:id="@+id/interval_pace"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/TextMedium"
android:layout_margin="1dp"
android:textAlignment="center"
android:layout_gravity="center"
android:text="@string/stats_pace" />
</LinearLayout>
<LinearLayout
android:id="@+id/interval_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
@@ -0,0 +1,39 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/interval_item_distance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/TextMedium"
tools:text="1 km" />
<TextView
android:id="@+id/interval_item_speed"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/TextMedium"
tools:text="15 km/h" />
<TextView
android:id="@+id/interval_item_pace"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/TextMedium"
tools:text="4:00" />
</LinearLayout>
</LinearLayout>
@@ -219,5 +219,15 @@
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/stats_max_speed_value" />
<LinearLayout
android:id="@+id/linear_layout_extra"
android:orientation="vertical"
android:layout_marginBottom="16dp"
app:layout_constraintLeft_toRightOf="@+id/guideline"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/stats_speed_horizontal_end_line"
android:layout_width="0dp"
android:layout_height="wrap_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
+2
View File
@@ -417,6 +417,8 @@ limitations under the License.
<string name="stats_speed">Speed</string>
<string name="stats_distance">Distance</string>
<string name="stats_total_time">Total time</string>
<string name="stats_intervals">Intervals</string>
<string name="stats_split_interval">Split interval</string>
<!-- Track -->
<string name="track_name_format">Track %1$d</string>
<string name="track_record_notification">Recording your track&#8230;</string>