IntervalsFragment/IntervalStatisticsModel refactoring: better performance with AndroidViewModel (it doesn't recreate the model every track point done anymore).

Also, it doesn't jump anymore. Fixes #669.
This commit is contained in:
Román Martínez
2021-04-03 16:32:53 +02:00
parent 8713eb3d0c
commit 978206dee4
7 changed files with 162 additions and 123 deletions
@@ -268,7 +268,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
case 0: case 0:
return StatisticsRecordedFragment.newInstance(trackId); return StatisticsRecordedFragment.newInstance(trackId);
case 1: case 1:
return IntervalsFragment.newInstance(); return IntervalsFragment.newInstance(true);
case 2: case 2:
return ChartFragment.newInstance(false); return ChartFragment.newInstance(false);
case 3: case 3:
@@ -369,7 +369,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
case 0: case 0:
return StatisticsRecordingFragment.newInstance(); return StatisticsRecordingFragment.newInstance();
case 1: case 1:
return IntervalsFragment.IntervalsRecordingFragment.newInstance(); return IntervalsFragment.newInstance(false);
case 2: case 2:
return ChartFragment.newInstance(false); return ChartFragment.newInstance(false);
case 3: case 3:
@@ -4,73 +4,84 @@ import android.content.Context;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.Interval> { public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<IntervalStatistics.Interval> intervalList;
private final Context context;
private final StackMode stackMode; private final StackMode stackMode;
private final boolean metricUnits; private boolean metricUnits;
private final String category; private boolean isReportSpeed;
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList, String category, StackMode stackMode) { public IntervalStatisticsAdapter(Context context, StackMode stackMode, boolean metricUnits, boolean isReportSpeed) {
super(context, R.layout.interval_stats_list_item, intervalList); this.metricUnits = metricUnits;
metricUnits = PreferencesUtils.isMetricUnits(PreferencesUtils.getSharedPreferences(context), context); this.context = context;
this.category = category;
this.stackMode = stackMode; this.stackMode = stackMode;
this.isReportSpeed = isReportSpeed;
} }
//TODO Check preference handling! Should not be accessed in getView()
@NonNull @NonNull
@Override @Override
public View getView(int position, @Nullable View intervalView, @NonNull ViewGroup parent) { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
int actualPosition = stackMode == StackMode.STACK_FROM_TOP ? position : getCount() - 1 - position; View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.interval_stats_list_item, parent, false);
IntervalStatistics.Interval interval = getItem(actualPosition); return new IntervalStatisticsAdapter.ViewHolder(view);
ViewHolder viewHolder; }
if (intervalView == null) { @Override
viewHolder = new ViewHolder(); public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
int actualPosition = stackMode == StackMode.STACK_FROM_TOP ? position : getItemCount() - 1 - position;
intervalView = LayoutInflater.from(getContext()).inflate(R.layout.interval_stats_list_item, parent, false); int nextPosition = actualPosition + 1;
boolean isLast = actualPosition == getItemCount() - 1;
viewHolder.distance = intervalView.findViewById(R.id.interval_item_distance); IntervalStatisticsAdapter.ViewHolder viewHolder = (IntervalStatisticsAdapter.ViewHolder) holder;
viewHolder.rate = intervalView.findViewById(R.id.interval_item_rate); IntervalStatistics.Interval interval = intervalList.get(actualPosition);
viewHolder.gain = intervalView.findViewById(R.id.interval_item_gain); viewHolder.itemView.setTag(actualPosition);
viewHolder.loss = intervalView.findViewById(R.id.interval_item_loss);
intervalView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) intervalView.getTag();
}
float sumDistance_m; float sumDistance_m;
if (actualPosition + 1 == getCount() && actualPosition > 0) { if (isLast && actualPosition > 0) {
sumDistance_m = actualPosition * getItem(actualPosition - 1).getDistance_m() + interval.getDistance_m(); sumDistance_m = actualPosition * intervalList.get(actualPosition - 1).getDistance_m() + interval.getDistance_m();
} else { } else {
sumDistance_m = (actualPosition + 1) * interval.getDistance_m(); sumDistance_m = nextPosition * interval.getDistance_m();
} }
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), sumDistance_m, metricUnits)); viewHolder.distance.setText(StringUtils.formatDistance(context, sumDistance_m, metricUnits));
if (PreferencesUtils.isReportSpeed(PreferencesUtils.getSharedPreferences(getContext()), getContext(), category)) { viewHolder.rate.setText(StringUtils.formatSpeed(context, interval.getSpeed_ms(), metricUnits, isReportSpeed));
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, true));
} else { viewHolder.gain.setText(StringUtils.formatDistance(context, interval.getGain_m(), metricUnits));
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, false)); viewHolder.loss.setText(StringUtils.formatDistance(context, interval.getLoss_m(), metricUnits));
}
@Override
public int getItemCount() {
if (intervalList == null) {
return 0;
}
return intervalList.size();
}
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, boolean metricUnits, boolean isReportSpeed) {
if (intervalList == data && this.metricUnits == metricUnits && this.isReportSpeed == isReportSpeed) {
return null;
} }
viewHolder.gain.setText(StringUtils.formatDistance(getContext(), interval.getGain_m(), metricUnits)); this.metricUnits = metricUnits;
viewHolder.loss.setText(StringUtils.formatDistance(getContext(), interval.getLoss_m(), metricUnits)); this.isReportSpeed = isReportSpeed;
intervalList = data;
return intervalView; if (data != null) {
this.notifyDataSetChanged();
}
return data;
} }
/** /**
@@ -81,10 +92,18 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
STACK_FROM_TOP STACK_FROM_TOP
} }
private static class ViewHolder { private static class ViewHolder extends RecyclerView.ViewHolder {
private TextView distance; TextView distance;
private TextView rate; TextView rate;
private TextView gain; TextView gain;
private TextView loss; TextView loss;
public ViewHolder(@NonNull View itemView) {
super(itemView);
distance = itemView.findViewById(R.id.interval_item_distance);
rate = itemView.findViewById(R.id.interval_item_rate);
gain = itemView.findViewById(R.id.interval_item_gain);
loss = itemView.findViewById(R.id.interval_item_loss);
}
} }
} }
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -14,6 +13,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackActivityDataHubInterface; import de.dennisguse.opentracks.TrackActivityDataHubInterface;
@@ -25,7 +29,6 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.databinding.IntervalListViewBinding; import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel; import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
@@ -36,6 +39,8 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
private static final String TAG = IntervalsFragment.class.getSimpleName(); private static final String TAG = IntervalsFragment.class.getSimpleName();
private static final String FROM_TOP_TO_BOTTOM_KEY = "fromTopToBottom";
private IntervalStatisticsModel viewModel; private IntervalStatisticsModel viewModel;
protected IntervalStatisticsAdapter.StackMode stackModeListView; protected IntervalStatisticsAdapter.StackMode stackModeListView;
private IntervalStatisticsModel.IntervalOption selectedInterval; private IntervalStatisticsModel.IntervalOption selectedInterval;
@@ -47,22 +52,38 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
private String category; private boolean isReportSpeed;
private IntervalListViewBinding viewBinding; private IntervalListViewBinding viewBinding;
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) { if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext()); metricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, getContext());
if (adapter != null) { uploadIntervals();
adapter.notifyDataSetChanged(); if (spinnerAdapter != null) {
spinnerAdapter.notifyDataSetChanged(); spinnerAdapter.notifyDataSetChanged();
} }
} }
}; };
public static Fragment newInstance() { /**
return new IntervalsFragment(); * Creates an instance of this class.
*
* @param fromTopToBottom If true then the intervals are shown from top to bottom (the first interval on top). Otherwise the intervals are shown from bottom to top.
* @return IntervalsFragment instance.
*/
public static Fragment newInstance(boolean fromTopToBottom) {
Bundle bundle = new Bundle();
bundle.putBoolean(FROM_TOP_TO_BOTTOM_KEY, fromTopToBottom);
IntervalsFragment intervalsFragment = new IntervalsFragment();
intervalsFragment.setArguments(bundle);
return intervalsFragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
stackModeListView = getArguments().getBoolean(FROM_TOP_TO_BOTTOM_KEY, true) ? IntervalStatisticsAdapter.StackMode.STACK_FROM_TOP : IntervalStatisticsAdapter.StackMode.STACK_FROM_BOTTOM;
} }
@Override @Override
@@ -76,14 +97,11 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext()); sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView); adapter = new IntervalStatisticsAdapter(getContext(), stackModeListView, metricUnits, isReportSpeed);
viewBinding.intervalList.setLayoutManager(new LinearLayoutManager(getContext()));
stackModeListView = IntervalStatisticsAdapter.StackMode.STACK_FROM_TOP; // TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
viewBinding.intervalList.setAdapter(adapter);
viewModel = new IntervalStatisticsModel();
spinnerAdapter = new ArrayAdapter<IntervalStatisticsModel.IntervalOption>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) { spinnerAdapter = new ArrayAdapter<IntervalStatisticsModel.IntervalOption>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
@NonNull @NonNull
@@ -109,7 +127,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
@Override @Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
selectedInterval = IntervalStatisticsModel.IntervalOption.values()[i]; selectedInterval = IntervalStatisticsModel.IntervalOption.values()[i];
loadIntervals(); uploadIntervals();
} }
@Override @Override
@@ -121,6 +139,10 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
viewModel = new ViewModelProvider(getActivity()).get(IntervalStatisticsModel.class);
loadIntervals();
resumeTrackDataHub(); resumeTrackDataHub();
} }
@@ -128,7 +150,6 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
pauseTrackDataHub(); pauseTrackDataHub();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
} }
@@ -155,10 +176,15 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
if (viewModel == null) { if (viewModel == null) {
return; return;
} }
LiveData<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(metricUnits, selectedInterval);
liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, metricUnits, isReportSpeed));
}
IntervalStatistics intervalStatistics = viewModel.getIntervalStats(metricUnits, selectedInterval); private synchronized void uploadIntervals() {
adapter = new IntervalStatisticsAdapter(getContext(), intervalStatistics.getIntervalList(), category, stackModeListView); if (viewModel == null) {
viewBinding.intervalList.setAdapter(adapter); return;
}
viewModel.upload(metricUnits, selectedInterval);
} }
/** /**
@@ -185,11 +211,11 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
getActivity().runOnUiThread(() -> { getActivity().runOnUiThread(() -> {
if (isResumed()) { if (isResumed()) {
// Set category. // Set category.
category = track != null ? track.getCategory() : ""; String category = track != null ? track.getCategory() : "";
// Set rate label. // Set rate label.
boolean reportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category); //TODO Handle sharedPreferenceChangeListener isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, getContext(), category);
viewBinding.intervalRate.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace); viewBinding.intervalRate.setText(isReportSpeed ? R.string.stats_speed : R.string.stats_pace);
} }
}); });
} }
@@ -219,7 +245,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
@Override @Override
public void onNewTrackPointsDone(@NonNull TrackPoint unused) { public void onNewTrackPointsDone(@NonNull TrackPoint unused) {
if (isResumed()) { if (isResumed()) {
runOnUiThread(this::loadIntervals); runOnUiThread(viewModel::onNewTrackPoints);
} }
} }
@@ -249,48 +275,4 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
fragmentActivity.runOnUiThread(runnable); fragmentActivity.runOnUiThread(runnable);
} }
} }
public static class IntervalsRecordingFragment extends IntervalsFragment {
// Refreshing intervals stats it's not so demanding so 5 seconds is enough to balance performance and user experience.
private static final long UI_UPDATE_INTERVAL = 5 * UnitConversions.ONE_SECOND_MS;
private Handler intervalHandler;
private final Runnable intervalRunner = new Runnable() {
@Override
public void run() {
if (isResumed()) {
updateIntervals();
intervalHandler.postDelayed(intervalRunner, UI_UPDATE_INTERVAL);
}
}
};
public static Fragment newInstance() {
return new IntervalsRecordingFragment();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
intervalHandler = new Handler();
stackModeListView = IntervalStatisticsAdapter.StackMode.STACK_FROM_BOTTOM;
}
@Override
public void onResume() {
super.onResume();
intervalHandler.post(intervalRunner);
}
@Override
public void onPause() {
super.onPause();
intervalHandler.removeCallbacks(intervalRunner);
}
private void updateIntervals() {
loadIntervals();
}
}
} }
@@ -55,7 +55,6 @@ public class IntervalStatistics {
} }
} }
public List<Interval> getIntervalList() { public List<Interval> getIntervalList() {
return intervalList; return intervalList;
} }
@@ -1,6 +1,11 @@
package de.dennisguse.opentracks.viewmodels; package de.dennisguse.opentracks.viewmodels;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -12,33 +17,67 @@ import de.dennisguse.opentracks.util.UnitConversions;
* This model is used to load intervals for a track. * This model is used to load intervals for a track.
* It uses a default interval but it can be set from outside to manage the interval length. * It uses a default interval but it can be set from outside to manage the interval length.
*/ */
public class IntervalStatisticsModel { public class IntervalStatisticsModel extends AndroidViewModel {
private final List<TrackPoint> trackPoints = new ArrayList<>(); private final List<TrackPoint> trackPoints = new ArrayList<>();
private MutableLiveData<List<IntervalStatistics.Interval>> intervalsLiveData;
private float distanceInterval;
public IntervalStatistics getIntervalStats(boolean metricUnits, @Nullable IntervalOption interval) { public IntervalStatisticsModel(@NonNull Application application) {
super(application);
}
public MutableLiveData<List<IntervalStatistics.Interval>> getIntervalStats(boolean metricUnits, @Nullable IntervalOption interval) {
synchronized (trackPoints) { synchronized (trackPoints) {
if (interval == null) { if (intervalsLiveData == null) {
interval = IntervalOption.OPTION_1; if (interval == null) {
} interval = IntervalOption.OPTION_1;
}
float distanceInterval = metricUnits ? (float) (interval.getValue() * UnitConversions.KM_TO_M) : (float) (interval.getValue() * UnitConversions.MI_TO_M); intervalsLiveData = new MutableLiveData<>();
return new IntervalStatistics(trackPoints, distanceInterval); distanceInterval = metricUnits ? (float) (interval.getValue() * UnitConversions.KM_TO_M) : (float) (interval.getValue() * UnitConversions.MI_TO_M);
loadIntervalStatistics();
}
return intervalsLiveData;
} }
} }
private void loadIntervalStatistics() {
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval);
intervalsLiveData.postValue(intervalStatistics.getIntervalList());
}
public void add(TrackPoint trackPoint) { public void add(TrackPoint trackPoint) {
synchronized (trackPoints) { synchronized (trackPoints) {
trackPoints.add(trackPoint); trackPoints.add(trackPoint);
} }
} }
public void onNewTrackPoints() {
synchronized (trackPoints) {
if (intervalsLiveData != null) {
loadIntervalStatistics();
}
}
}
public void clear() { public void clear() {
synchronized (trackPoints) { synchronized (trackPoints) {
trackPoints.clear(); trackPoints.clear();
} }
} }
public void upload(boolean metricUnits, @Nullable IntervalOption interval) {
synchronized (trackPoints) {
if (interval == null) {
interval = IntervalOption.OPTION_1;
}
distanceInterval = metricUnits ? (float) (interval.getValue() * UnitConversions.KM_TO_M) : (float) (interval.getValue() * UnitConversions.MI_TO_M);
loadIntervalStatistics();
}
}
/** /**
* Intervals length this view model support. * Intervals length this view model support.
*/ */
+1 -1
View File
@@ -57,7 +57,7 @@
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:background="@color/stats_separator" /> android:background="@color/stats_separator" />
<ListView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/interval_list" android:id="@+id/interval_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"