diff --git a/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java b/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java index ee547e6e..6188c64d 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/MainActivity.java @@ -86,8 +86,10 @@ import com.health.openscale.gui.preferences.UserSettingsFragment; import com.health.openscale.gui.slides.AppIntroActivity; import java.io.File; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; @@ -1070,4 +1072,32 @@ public class MainActivity extends AppCompatActivity connectToBluetooth(); } }); + + // Generate random dummy measurements - ONLY FOR TESTING PURPOSE + private void generateDummyMeasurements(int measurementCount) { + for (int i=0; i 0.0f) { - color = (value > getScaleUser().getGoalWeight()) ? Color.RED : Color.GREEN; - } else if (diff < 0.0f) { - color = (value < getScaleUser().getGoalWeight()) ? Color.RED : Color.GREEN; + // skip evaluation to speed the calculation up (e.g. not needed for table view) + if (isEvalOn) { + // change color depending on if you are going towards or away from your weight goal + if (this instanceof WeightMeasurementView) { + if (diff > 0.0f) { + color = (value > getScaleUser().getGoalWeight()) ? Color.RED : Color.GREEN; + } else if (diff < 0.0f) { + color = (value < getScaleUser().getGoalWeight()) ? Color.RED : Color.GREEN; + } } - } - final float evalValue = maybeConvertToOriginalValue(value); + final float evalValue = maybeConvertToOriginalValue(value); - EvaluationSheet evalSheet = new EvaluationSheet(getScaleUser(), dateTime); - evaluationResult = evaluateSheet(evalSheet, evalValue); + EvaluationSheet evalSheet = new EvaluationSheet(getScaleUser(), dateTime); + evaluationResult = evaluateSheet(evalSheet, evalValue); - if (evaluationResult != null) { - switch (evaluationResult.eval_state) { - case LOW: - color = (diff > 0.0f) ? Color.GREEN : Color.RED; - break; - case HIGH: - color = (diff < 0.0f) ? Color.GREEN : Color.RED; - break; - case NORMAL: - color = Color.GREEN; - break; + if (evaluationResult != null) { + switch (evaluationResult.eval_state) { + case LOW: + color = (diff > 0.0f) ? Color.GREEN : Color.RED; + break; + case HIGH: + color = (diff < 0.0f) ? Color.GREEN : Color.RED; + break; + case NORMAL: + color = Color.GREEN; + break; + } } } @@ -490,6 +493,12 @@ public abstract class FloatMeasurementView extends MeasurementView { Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } + + @Override + public void appendDiffValue(final SpannableStringBuilder text, boolean newLine) { + appendDiffValue(text, newLine, true); + } + @Override protected boolean isEditable() { if (useAutoValue()) { @@ -539,9 +548,6 @@ public abstract class FloatMeasurementView extends MeasurementView { return ""; } - @Override - public boolean hasExtraPreferences() { return true; } - private class ListPreferenceWithNeutralButton extends ListPreference { ListPreferenceWithNeutralButton(Context context) { super(context); @@ -568,6 +574,7 @@ public abstract class FloatMeasurementView extends MeasurementView { @Override public void prepareExtraPreferencesScreen(PreferenceScreen screen) { + super.prepareExtraPreferencesScreen(screen); MeasurementViewSettings settings = getSettings(); CheckBoxPreference rightAxis = new CheckBoxPreference(screen.getContext()); diff --git a/android_app/app/src/main/java/com/health/openscale/gui/measurement/MeasurementView.java b/android_app/app/src/main/java/com/health/openscale/gui/measurement/MeasurementView.java index cd13d5ea..c4d972b5 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/measurement/MeasurementView.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/measurement/MeasurementView.java @@ -15,6 +15,11 @@ */ package com.health.openscale.gui.measurement; +import static com.health.openscale.gui.measurement.MeasurementView.MeasurementViewMode.ADD; +import static com.health.openscale.gui.measurement.MeasurementView.MeasurementViewMode.EDIT; +import static com.health.openscale.gui.measurement.MeasurementView.MeasurementViewMode.STATISTIC; +import static com.health.openscale.gui.measurement.MeasurementView.MeasurementViewMode.VIEW; + import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -40,6 +45,7 @@ import android.widget.TableRow; import android.widget.TextView; import androidx.core.content.ContextCompat; +import androidx.preference.CheckBoxPreference; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; @@ -53,11 +59,6 @@ import com.health.openscale.gui.utils.ColorUtil; import java.util.ArrayList; import java.util.List; -import static com.health.openscale.gui.measurement.MeasurementView.MeasurementViewMode.ADD; -import static com.health.openscale.gui.measurement.MeasurementView.MeasurementViewMode.EDIT; -import static com.health.openscale.gui.measurement.MeasurementView.MeasurementViewMode.STATISTIC; -import static com.health.openscale.gui.measurement.MeasurementView.MeasurementViewMode.VIEW; - public abstract class MeasurementView extends TableLayout { public enum MeasurementViewMode {VIEW, EDIT, ADD, STATISTIC} @@ -207,7 +208,7 @@ public abstract class MeasurementView extends TableLayout { iconView.setImageResource(iconId); iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - iconView.setPadding(25,25,25,25); + iconView.setPadding(15,15,15,15); iconView.setColorFilter(ColorUtil.COLOR_BLACK); iconView.setBackground(iconViewBackground); @@ -285,7 +286,8 @@ public abstract class MeasurementView extends TableLayout { public CharSequence getName() { return nameView.getText(); } public abstract String getValueAsString(boolean withUnit); - public void appendDiffValue(SpannableStringBuilder builder, boolean newLine) { } + public void appendDiffValue(final SpannableStringBuilder builder, boolean newLine, boolean isEvalOn) { } + public void appendDiffValue(final SpannableStringBuilder builder, boolean newLine) { } public Drawable getIcon() { return iconView.getDrawable(); } public int getIconResource() { return iconId; } public void setBackgroundIconColor(int color) { @@ -357,6 +359,8 @@ public abstract class MeasurementView extends TableLayout { return background.getColor(); } + abstract public int getColor(); + protected void showEvaluatorRow(boolean show) { if (show) { evaluatorRow.setVisibility(View.VISIBLE); @@ -423,8 +427,16 @@ public abstract class MeasurementView extends TableLayout { } public String getPreferenceSummary() { return ""; } - public boolean hasExtraPreferences() { return false; } - public void prepareExtraPreferencesScreen(PreferenceScreen screen) { } + public void prepareExtraPreferencesScreen(PreferenceScreen screen) { + MeasurementViewSettings settings = getSettings(); + + CheckBoxPreference isSticky = new CheckBoxPreference(screen.getContext()); + isSticky.setKey(settings.getIsStickyGraphKey()); + isSticky.setTitle(R.string.label_is_sticky); + isSticky.setPersistent(true); + isSticky.setDefaultValue(settings.isSticky()); + screen.addPreference(isSticky); + } protected abstract View getInputView(); protected abstract boolean validateAndSetInput(View view); diff --git a/android_app/app/src/main/java/com/health/openscale/gui/measurement/MeasurementViewSettings.java b/android_app/app/src/main/java/com/health/openscale/gui/measurement/MeasurementViewSettings.java index 608a177f..a8df3f46 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/measurement/MeasurementViewSettings.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/measurement/MeasurementViewSettings.java @@ -27,6 +27,7 @@ public class MeasurementViewSettings { private final String key; private static final String PREFERENCE_SUFFIX_ENABLE = "Enable"; + private static final String PREFERENCE_SUFFIX_IS_STICKY = "IsSticky"; private static final String PREFERENCE_SUFFIX_IN_OVERVIEW_GRAPH = "InOverviewGraph"; private static final String PREFERENCE_SUFFIX_ON_RIGHT_AXIS = "OnRightAxis"; private static final String PREFERENCE_SUFFIX_IN_GRAPH = "InGraph"; @@ -116,6 +117,26 @@ public class MeasurementViewSettings { return isEnabledIgnoringDependencies() && areDependenciesEnabled(); } + public boolean isSticky() { + boolean defaultValue; + switch (key) { + case WeightMeasurementView.KEY: + case WaterMeasurementView.KEY: + case MuscleMeasurementView.KEY: + case FatMeasurementView.KEY: + defaultValue = true; + break; + default: + defaultValue = false; + break; + } + return preferences.getBoolean(getIsStickyGraphKey(), defaultValue); + } + + public String getIsStickyGraphKey() { + return getPreferenceKey(PREFERENCE_SUFFIX_IS_STICKY); + } + public String getInOverviewGraphKey() { return getPreferenceKey(PREFERENCE_SUFFIX_IN_OVERVIEW_GRAPH); } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/measurement/TimeMeasurementView.java b/android_app/app/src/main/java/com/health/openscale/gui/measurement/TimeMeasurementView.java index 6f2662fe..c1dd00be 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/measurement/TimeMeasurementView.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/measurement/TimeMeasurementView.java @@ -22,6 +22,7 @@ import android.widget.TimePicker; import com.health.openscale.R; import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.health.openscale.gui.utils.ColorUtil; import java.text.DateFormat; import java.util.Calendar; @@ -89,6 +90,9 @@ public class TimeMeasurementView extends MeasurementView { state.putLong(getKey(), time.getTime()); } + @Override + public int getColor() { return ColorUtil.COLOR_GRAY; }; + @Override public String getValueAsString(boolean withUnit) { return timeFormat.format(time); diff --git a/android_app/app/src/main/java/com/health/openscale/gui/measurement/UserMeasurementView.java b/android_app/app/src/main/java/com/health/openscale/gui/measurement/UserMeasurementView.java index 66e3799a..4ec9c7db 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/measurement/UserMeasurementView.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/measurement/UserMeasurementView.java @@ -25,6 +25,7 @@ import com.health.openscale.R; import com.health.openscale.core.OpenScale; import com.health.openscale.core.datatypes.ScaleMeasurement; import com.health.openscale.core.datatypes.ScaleUser; +import com.health.openscale.gui.utils.ColorUtil; import java.util.ArrayList; @@ -80,6 +81,9 @@ public class UserMeasurementView extends MeasurementView { state.putInt(getKey(), userId); } + @Override + public int getColor() { return ColorUtil.COLOR_GRAY; }; + @Override public String getValueAsString(boolean withUnit) { return openScale.getScaleUser(userId).getUserName(); diff --git a/android_app/app/src/main/java/com/health/openscale/gui/overview/OverviewAdapter.java b/android_app/app/src/main/java/com/health/openscale/gui/overview/OverviewAdapter.java new file mode 100644 index 00000000..6b0a4f49 --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/gui/overview/OverviewAdapter.java @@ -0,0 +1,196 @@ +package com.health.openscale.gui.overview; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TableLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.RecyclerView; +import androidx.transition.AutoTransition; +import androidx.transition.TransitionManager; + +import com.health.openscale.R; +import com.health.openscale.core.OpenScale; +import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.health.openscale.gui.measurement.DateMeasurementView; +import com.health.openscale.gui.measurement.MeasurementEntryFragment; +import com.health.openscale.gui.measurement.MeasurementView; +import com.health.openscale.gui.measurement.TimeMeasurementView; +import com.health.openscale.gui.measurement.UserMeasurementView; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.List; + +class OverviewAdapter extends RecyclerView.Adapter { + private Activity activity; + private List scaleMeasurementList; + + public OverviewAdapter(Activity activity, List scaleMeasurementList) { + this.activity = activity; + this.scaleMeasurementList = scaleMeasurementList; + } + + private void deleteMeasurement(int measurementId) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); + boolean deleteConfirmationEnable = prefs.getBoolean("deleteConfirmationEnable", true); + + if (deleteConfirmationEnable) { + AlertDialog.Builder deleteAllDialog = new AlertDialog.Builder(activity); + deleteAllDialog.setMessage(activity.getResources().getString(R.string.question_really_delete)); + + deleteAllDialog.setPositiveButton(activity.getResources().getString(R.string.label_yes), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + doDeleteMeasurement(measurementId); + } + }); + + deleteAllDialog.setNegativeButton(activity.getResources().getString(R.string.label_no), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }); + + deleteAllDialog.show(); + } + else { + doDeleteMeasurement(measurementId); + } + } + + private void doDeleteMeasurement(int measurementId) { + OpenScale.getInstance().deleteScaleMeasurement(measurementId); + Toast.makeText(activity, activity.getResources().getString(R.string.info_data_deleted), Toast.LENGTH_SHORT).show(); + } + + @Override + public OverviewAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_overview, parent, false); + + ViewHolder viewHolder = new ViewHolder(view); + + return viewHolder; + } + + @Override + public void onBindViewHolder(@NonNull OverviewAdapter.ViewHolder holder, int position) { + holder.measurementHighlightViews.removeAllViews(); + holder.measurementViews.removeAllViews(); + + ScaleMeasurement scaleMeasurement = scaleMeasurementList.get(position); + ScaleMeasurement prevScaleMeasurement; + + // for the first measurement no previous measurement are available, use standard measurement instead + if (position == 0) { + prevScaleMeasurement = new ScaleMeasurement(); + } else { + prevScaleMeasurement = scaleMeasurementList.get(position - 1); + } + + holder.showEntry.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + OverviewFragmentDirections.ActionNavOverviewToNavDataentry action = OverviewFragmentDirections.actionNavOverviewToNavDataentry(); + action.setMeasurementId(scaleMeasurement.getId()); + action.setMode(MeasurementEntryFragment.DATA_ENTRY_MODE.VIEW); + Navigation.findNavController(activity, R.id.nav_host_fragment).navigate(action); + } + }); + + holder.editEntry.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + OverviewFragmentDirections.ActionNavOverviewToNavDataentry action = OverviewFragmentDirections.actionNavOverviewToNavDataentry(); + action.setMeasurementId(scaleMeasurement.getId()); + action.setMode(MeasurementEntryFragment.DATA_ENTRY_MODE.EDIT); + Navigation.findNavController(activity, R.id.nav_host_fragment).navigate(action); + } + }); + holder.deleteEntry.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + deleteMeasurement(scaleMeasurement.getId()); + } + }); + + holder.expandMeasurementView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + TransitionManager.beginDelayedTransition(holder.measurementViews, new AutoTransition()); + + if (holder.measurementViews.getVisibility() == View.VISIBLE) { + holder.measurementViews.setVisibility(View.GONE); + holder.expandMeasurementView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_expand_more)); + } else { + holder.measurementViews.setVisibility(View.VISIBLE); + holder.expandMeasurementView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_expand_less)); + } + } + }); + + holder.dateView.setText(DateFormat.getDateInstance(DateFormat.MEDIUM).format(scaleMeasurement.getDateTime()) + + " (" + new SimpleDateFormat("EE").format(scaleMeasurement.getDateTime()) + ") "+ + DateFormat.getTimeInstance(DateFormat.SHORT).format(scaleMeasurement.getDateTime())); + + List measurementViewList = MeasurementView.getMeasurementList(activity, MeasurementView.DateTimeOrder.LAST); + + for (MeasurementView measurementView : measurementViewList) { + if (measurementView instanceof DateMeasurementView || measurementView instanceof TimeMeasurementView || measurementView instanceof UserMeasurementView) { + measurementView.setVisible(false); + } + else if (measurementView.isVisible()) { + measurementView.loadFrom(scaleMeasurement, prevScaleMeasurement); + + if (measurementView.getSettings().isSticky()) { + holder.measurementHighlightViews.addView(measurementView); + } else{ + holder.measurementViews.addView(measurementView); + } + } + } + } + + @Override + public long getItemId(int position) { + return scaleMeasurementList.get(position).getId(); + } + + @Override + public int getItemCount() { + return scaleMeasurementList.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + TextView dateView; + ImageView showEntry; + ImageView editEntry; + ImageView deleteEntry; + TableLayout measurementHighlightViews; + ImageView expandMeasurementView; + TableLayout measurementViews; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + + dateView = itemView.findViewById(R.id.dateView); + showEntry = itemView.findViewById(R.id.showEntry); + editEntry = itemView.findViewById(R.id.editEntry); + deleteEntry = itemView.findViewById(R.id.deleteEntry); + measurementHighlightViews = itemView.findViewById(R.id.measurementHighlightViews); + expandMeasurementView = itemView.findViewById(R.id.expandMoreView); + measurementViews = itemView.findViewById(R.id.measurementViews); + measurementViews.setVisibility(View.GONE); + } + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/gui/overview/OverviewFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/overview/OverviewFragment.java index 3d683066..336033d7 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/overview/OverviewFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/overview/OverviewFragment.java @@ -15,12 +15,14 @@ */ package com.health.openscale.gui.overview; -import android.app.AlertDialog; -import android.content.DialogInterface; import android.content.SharedPreferences; import android.graphics.Color; import android.os.Bundle; import android.preference.PreferenceManager; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.RelativeSizeSpan; +import android.text.style.StyleSpan; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; @@ -30,14 +32,15 @@ import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.Spinner; -import android.widget.TableLayout; import android.widget.TextView; -import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; -import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.transition.ChangeScroll; +import androidx.transition.TransitionManager; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.highlight.Highlight; @@ -46,13 +49,14 @@ import com.health.openscale.R; import com.health.openscale.core.OpenScale; import com.health.openscale.core.datatypes.ScaleMeasurement; import com.health.openscale.core.datatypes.ScaleUser; +import com.health.openscale.core.utils.DateTimeHelpers; import com.health.openscale.gui.measurement.ChartActionBarView; import com.health.openscale.gui.measurement.ChartMeasurementView; -import com.health.openscale.gui.measurement.MeasurementEntryFragment; -import com.health.openscale.gui.measurement.MeasurementView; -import com.health.openscale.gui.utils.ColorUtil; +import com.health.openscale.gui.measurement.WeightMeasurementView; +import java.text.DateFormat; import java.util.ArrayList; +import java.util.Calendar; import java.util.List; public class OverviewFragment extends Fragment { @@ -60,8 +64,8 @@ public class OverviewFragment extends Fragment { private TextView txtTitleUser; - private List lastMeasurementViews; - + private RecyclerView recyclerView; + private OverviewAdapter overviewAdapter; private ChartMeasurementView chartView; private ChartActionBarView chartActionBarView; @@ -69,9 +73,9 @@ public class OverviewFragment extends Fragment { private PopupMenu rangePopupMenu; - private ImageView showEntry; - private ImageView editEntry; - private ImageView deleteEntry; + private TextView differenceWeightView; + private TextView initialWeightView; + private TextView goalWeightView; private ScaleUser currentScaleUser; @@ -79,6 +83,7 @@ public class OverviewFragment extends Fragment { private SharedPreferences prefs; + private List scaleMeasurementList; private ScaleMeasurement markedMeasurement; @Override @@ -87,12 +92,20 @@ public class OverviewFragment extends Fragment { prefs = PreferenceManager.getDefaultSharedPreferences(overviewView.getContext()); - txtTitleUser = overviewView.findViewById(R.id.txtTitleUser); + differenceWeightView = overviewView.findViewById(R.id.differenceWeightView); + initialWeightView = overviewView.findViewById(R.id.initialWeightView); + goalWeightView = overviewView.findViewById(R.id.goalWeightView); chartView = overviewView.findViewById(R.id.chartView); chartView.setOnChartValueSelectedListener(new onChartSelectedListener()); chartView.setProgressBar(overviewView.findViewById(R.id.progressBar)); chartView.setIsInGraphKey(false); + chartView.getLegend().setEnabled(false); + chartView.getAxisRight().setDrawLabels(false); + chartView.getAxisRight().setDrawGridLines(false); + chartView.getAxisLeft().setDrawGridLines(false); + chartView.getAxisLeft().setDrawLabels(false); + chartView.getXAxis().setDrawGridLines(false); chartActionBarView = overviewView.findViewById(R.id.chartActionBar); chartActionBarView.setIsInGraphKey(false); @@ -178,14 +191,12 @@ public class OverviewFragment extends Fragment { chartActionBarView.setVisibility(View.GONE); } - lastMeasurementViews = MeasurementView.getMeasurementList( - getContext(), MeasurementView.DateTimeOrder.LAST); - - TableLayout tableOverviewLayout = overviewView.findViewById(R.id.tableLayoutMeasurements); - - for (MeasurementView measurement : lastMeasurementViews) { - tableOverviewLayout.addView(measurement); - } + recyclerView = overviewView.findViewById(R.id.recyclerView); + LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + layoutManager.setInitialPrefetchItemCount(5); + layoutManager.setReverseLayout(true); + layoutManager.setStackFromEnd(true); + recyclerView.setLayoutManager(layoutManager); spinUserAdapter = new ArrayAdapter<>(overviewView.getContext(), R.layout.spinner_item, new ArrayList()); spinUser.setAdapter(spinUserAdapter); @@ -198,39 +209,6 @@ public class OverviewFragment extends Fragment { } }); - showEntry = overviewView.findViewById(R.id.showEntry); - showEntry.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - OverviewFragmentDirections.ActionNavOverviewToNavDataentry action = OverviewFragmentDirections.actionNavOverviewToNavDataentry(); - action.setMeasurementId(markedMeasurement.getId()); - action.setMode(MeasurementEntryFragment.DATA_ENTRY_MODE.VIEW); - Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(action); - } - }); - - editEntry = overviewView.findViewById(R.id.editEntry); - editEntry.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - OverviewFragmentDirections.ActionNavOverviewToNavDataentry action = OverviewFragmentDirections.actionNavOverviewToNavDataentry(); - action.setMeasurementId(markedMeasurement.getId()); - action.setMode(MeasurementEntryFragment.DATA_ENTRY_MODE.EDIT); - Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(action); - } - }); - deleteEntry = overviewView.findViewById(R.id.deleteEntry); - deleteEntry.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - deleteMeasurement(); - } - }); - - showEntry.setEnabled(false); - editEntry.setEnabled(false); - deleteEntry.setEnabled(false); - chartView.animateY(700); OpenScale.getInstance().getScaleMeasurementsLiveData().observe(getViewLifecycleOwner(), new Observer>() { @@ -253,14 +231,12 @@ public class OverviewFragment extends Fragment { } public void updateOnView(List scaleMeasurementList) { - if (scaleMeasurementList.isEmpty()) { - markedMeasurement = new ScaleMeasurement(); - } else { - markedMeasurement = scaleMeasurementList.get(0); - } + this.scaleMeasurementList = scaleMeasurementList; + + overviewAdapter = new OverviewAdapter(getActivity(), scaleMeasurementList); + recyclerView.setAdapter(overviewAdapter); updateUserSelection(); - updateMesurementViews(markedMeasurement); chartView.updateMeasurementList(scaleMeasurementList); updateChartView(); } @@ -270,17 +246,7 @@ public class OverviewFragment extends Fragment { chartView.setViewRange(selectedRangeMode); } - private void updateMesurementViews(ScaleMeasurement selectedMeasurement) { - ScaleMeasurement[] tupleScaleData = OpenScale.getInstance().getTupleOfScaleMeasurement(selectedMeasurement.getId()); - ScaleMeasurement prevScaleMeasurement = tupleScaleData[0]; - - for (MeasurementView measurement : lastMeasurementViews) { - measurement.loadFrom(selectedMeasurement, prevScaleMeasurement); - } - } - private void updateUserSelection() { - currentScaleUser = OpenScale.getInstance().getSelectedScaleUser(); spinUserAdapter.clear(); @@ -300,8 +266,75 @@ public class OverviewFragment extends Fragment { // Hide user selector when there is only one user int visibility = spinUserAdapter.getCount() < 2 ? View.GONE : View.VISIBLE; - txtTitleUser.setVisibility(visibility); spinUser.setVisibility(visibility); + + + WeightMeasurementView weightMeasurementView = new WeightMeasurementView(getContext()); + ScaleMeasurement initialWeightMeasurement = OpenScale.getInstance().getLastScaleMeasurement(); + + if (initialWeightMeasurement == null) { + initialWeightMeasurement = new ScaleMeasurement(); + } + + initialWeightMeasurement.setWeight(initialWeightMeasurement.getWeight()); + weightMeasurementView.loadFrom(initialWeightMeasurement, null); + + SpannableStringBuilder initialWeightValue = new SpannableStringBuilder(); + initialWeightValue.append(getResources().getString(R.string.label_weight)); + initialWeightValue.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, initialWeightValue.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + initialWeightValue.append("\n"); + initialWeightValue.append(weightMeasurementView.getValueAsString(true)); + initialWeightValue.append(("\n")); + int start = initialWeightValue.length(); + initialWeightValue.append(DateFormat.getDateInstance(DateFormat.MEDIUM).format(initialWeightMeasurement.getDateTime())); + initialWeightValue.setSpan(new RelativeSizeSpan(0.8f), start, initialWeightValue.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + initialWeightView.setText(initialWeightValue); + + ScaleMeasurement goalWeightMeasurement = new ScaleMeasurement(); + goalWeightMeasurement.setWeight(currentScaleUser.getGoalWeight()); + weightMeasurementView.loadFrom(goalWeightMeasurement, null); + + SpannableStringBuilder goalWeightValue = new SpannableStringBuilder(); + goalWeightValue.append(getResources().getString(R.string.label_goal_weight)); + goalWeightValue.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, goalWeightValue.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + goalWeightValue.append("\n"); + goalWeightValue.append(weightMeasurementView.getValueAsString(true)); + goalWeightValue.append(("\n")); + start = goalWeightValue.length(); + goalWeightValue.append(DateFormat.getDateInstance(DateFormat.MEDIUM).format(currentScaleUser.getGoalDate())); + goalWeightValue.setSpan(new RelativeSizeSpan(0.8f), start, goalWeightValue.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + goalWeightView.setText(goalWeightValue); + + ScaleMeasurement differenceWeightMeasurement = new ScaleMeasurement(); + if (initialWeightMeasurement.getWeight() > goalWeightMeasurement.getWeight()) { + differenceWeightMeasurement.setWeight(initialWeightMeasurement.getWeight() -goalWeightMeasurement.getWeight()); + } else { + differenceWeightMeasurement.setWeight(goalWeightMeasurement.getWeight() - initialWeightMeasurement.getWeight()); + } + weightMeasurementView.loadFrom(differenceWeightMeasurement, null); + + Calendar initialCalendar = Calendar.getInstance(); + initialCalendar.setTime(initialWeightMeasurement.getDateTime()); + Calendar goalCalendar = Calendar.getInstance(); + goalCalendar.setTime(currentScaleUser.getGoalDate()); + int daysBetween = Math.max(0, DateTimeHelpers.daysBetween(initialCalendar, goalCalendar)); + + SpannableStringBuilder differenceWeightValue = new SpannableStringBuilder(); + differenceWeightValue.append(getResources().getString(R.string.label_weight_difference)); + differenceWeightValue.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, differenceWeightValue.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + differenceWeightValue.append("\n"); + differenceWeightValue.append(weightMeasurementView.getValueAsString(true)); + differenceWeightValue.append(("\n")); + start = differenceWeightValue.length(); + differenceWeightValue.append(daysBetween + " " + getString(R.string.label_days_left)); + differenceWeightValue.setSpan(new RelativeSizeSpan(0.8f), start, differenceWeightValue.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + differenceWeightView.setText(differenceWeightValue); } private class onChartSelectedListener implements OnChartValueSelectedListener { @@ -313,26 +346,15 @@ public class OverviewFragment extends Fragment { markedMeasurement = (ScaleMeasurement)extraData[0]; //MeasurementView measurementView = (MeasurementView)extraData[1]; - showEntry.setEnabled(true); - editEntry.setEnabled(true); - deleteEntry.setEnabled(true); - - showEntry.setColorFilter(ColorUtil.COLOR_BLUE); - editEntry.setColorFilter(ColorUtil.COLOR_GREEN); - deleteEntry.setColorFilter(ColorUtil.COLOR_RED); - - updateMesurementViews(markedMeasurement); + if (scaleMeasurementList.contains(markedMeasurement)) { + TransitionManager.beginDelayedTransition(recyclerView, new ChangeScroll()); + recyclerView.scrollToPosition(scaleMeasurementList.indexOf(markedMeasurement)); + } } @Override public void onNothingSelected() { - showEntry.setEnabled(false); - editEntry.setEnabled(false); - deleteEntry.setEnabled(false); - - showEntry.setColorFilter(ColorUtil.COLOR_GRAY); - editEntry.setColorFilter(ColorUtil.COLOR_GRAY); - deleteEntry.setColorFilter(ColorUtil.COLOR_GRAY); + // empty } } @@ -358,44 +380,4 @@ public class OverviewFragment extends Fragment { } } - - private void deleteMeasurement() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(overviewView.getContext()); - boolean deleteConfirmationEnable = prefs.getBoolean("deleteConfirmationEnable", true); - - if (deleteConfirmationEnable) { - AlertDialog.Builder deleteAllDialog = new AlertDialog.Builder(overviewView.getContext()); - deleteAllDialog.setMessage(getResources().getString(R.string.question_really_delete)); - - deleteAllDialog.setPositiveButton(getResources().getString(R.string.label_yes), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - doDeleteMeasurement(); - } - }); - - deleteAllDialog.setNegativeButton(getResources().getString(R.string.label_no), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.dismiss(); - } - }); - - deleteAllDialog.show(); - } - else { - doDeleteMeasurement(); - } - } - - private void doDeleteMeasurement() { - OpenScale.getInstance().deleteScaleMeasurement(markedMeasurement.getId()); - Toast.makeText(overviewView.getContext(), getResources().getString(R.string.info_data_deleted), Toast.LENGTH_SHORT).show(); - - showEntry.setEnabled(false); - editEntry.setEnabled(false); - deleteEntry.setEnabled(false); - - showEntry.setColorFilter(ColorUtil.COLOR_GRAY); - editEntry.setColorFilter(ColorUtil.COLOR_GRAY); - deleteEntry.setColorFilter(ColorUtil.COLOR_GRAY); - } } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/MeasurementPreferences.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/MeasurementPreferences.java index 2a301710..7e6a09c2 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/MeasurementPreferences.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/MeasurementPreferences.java @@ -245,13 +245,6 @@ public class MeasurementPreferences extends PreferenceFragmentCompat { public boolean onSingleTapUp(MotionEvent e) { boundView.setPressed(false); - if (!measurement.hasExtraPreferences()) { - if (switchView.getVisibility() == View.VISIBLE) { - switchView.toggle(); - } - return true; - } - // Must be enabled to show extra preferences screen if (!measurement.getSettings().isEnabled()) { return true; diff --git a/android_app/app/src/main/java/com/health/openscale/gui/table/StickyHeaderTableView.java b/android_app/app/src/main/java/com/health/openscale/gui/table/StickyHeaderTableView.java new file mode 100644 index 00000000..10c6aecb --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/gui/table/StickyHeaderTableView.java @@ -0,0 +1,1465 @@ +package com.health.openscale.gui.table; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.SparseIntArray; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.DecelerateInterpolator; + +import androidx.core.view.NestedScrollingChild; +import androidx.core.view.NestedScrollingChildHelper; +import androidx.core.view.ViewCompat; + +import com.health.openscale.R; +import com.health.openscale.gui.utils.ColorUtil; + +/** + * Created by Mitul Varmora on 11/8/2016. + * StickyHeaderTableView, see https://github.com/MitulVarmora/StickyHeaderTableView + * MIT License + * modified 2023 by olie.xdev + */ + +public class StickyHeaderTableView extends View implements NestedScrollingChild { + private final Paint paintStrokeRect = new Paint(); + private final Paint paintHeaderCellFillRect = new Paint(); + private final Paint paintContentCellFillRect = new Paint(); + private final TextPaint paintLabelText = new TextPaint(); + private final Paint paintDrawable = new Paint(); + + private final TextPaint paintHeaderText = new TextPaint(); + private final Rect textRectBounds = new Rect(); + + private int maxMeasure = 0; + + /** + * Visible rect size of view which is displayed on screen + */ + private final Rect visibleContentRect = new Rect(0, 0, 0, 0); + /** + * based on scrolling this rect value will update + */ + private final Rect scrolledRect = new Rect(0, 0, 0, 0); + /** + * Actual rect size of canvas drawn content (Which may be larger or smaller than mobile screen) + */ + private final Rect actualContentRect = new Rect(0, 0, 0, 0); + // below variables are used for fling animation (Not for scrolling) + private final DecelerateInterpolator animateInterpolator = new DecelerateInterpolator(); + private NestedScrollingChildHelper nestedScrollingChildHelper; + private int NESTED_SCROLL_AXIS = ViewCompat.SCROLL_AXIS_NONE; + private OnTableCellClickListener onTableCellClickListener = null; + private boolean isScrollingHorizontally = false; + private boolean isScrollingVertically = false; + /** + * This is used to stop fling animation if user has touch intercepted + */ + private boolean isFlinging = false; + // Below are configurable variables via xml (also can be used via setter methods) + private boolean isDisplayLeftHeadersVertically = false; + private boolean is2DScrollingEnabled; + private boolean isWrapHeightOfEachRow = false; + private boolean isWrapWidthOfEachColumn = false; + private int textLabelColor; + private int textHeaderColor; + private int dividerColor; + private int textLabelSize; + private int textHeaderSize; + private int dividerThickness; + private int headerCellFillColor; + private int contentCellFillColor; + private int cellPadding; + /** + * Used to identify clicked position for #OnTableCellClickListener + */ + private Rect[][] rectEachCellBoundData = new Rect[][]{}; + private Object[][] data = null; + private int maxWidthOfCell = 0; + private int maxHeightOfCell = 0; + private SparseIntArray maxHeightSparseIntArray = new SparseIntArray(); + private SparseIntArray maxWidthSparseIntArray = new SparseIntArray(); + /** + * Used for scroll events + */ + private GestureDetector gestureDetector; + private long startTime; + private long endTime; + private float totalAnimDx; + private float totalAnimDy; + private float lastAnimDx; + private float lastAnimDy; + + public interface OnTableCellClickListener { + public void onTableCellClicked(int rowPosition, int columnPosition); + } + + public StickyHeaderTableView(Context context) { + this(context, null, 0); + } + + public StickyHeaderTableView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public StickyHeaderTableView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + final int defaultTextSize = (int) dpToPixels(getContext(), 14); + + TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, R.styleable.StickyHeaderTableView, defStyleAttr, defStyleAttr); + + if (a != null) { + try { + textLabelColor = a.getColor( + R.styleable.StickyHeaderTableView_shtv_textLabelColor, Color.BLACK); + textHeaderColor = a.getColor( + R.styleable.StickyHeaderTableView_shtv_textHeaderColor, Color.BLACK); + dividerColor = a.getColor( + R.styleable.StickyHeaderTableView_shtv_dividerColor, Color.BLACK); + + textLabelSize = a.getDimensionPixelSize( + R.styleable.StickyHeaderTableView_shtv_textLabelSize, defaultTextSize); + textHeaderSize = a.getDimensionPixelSize( + R.styleable.StickyHeaderTableView_shtv_textHeaderSize, defaultTextSize); + dividerThickness = a.getDimensionPixelSize(R.styleable.StickyHeaderTableView_shtv_dividerThickness, 0); + cellPadding = a.getDimensionPixelSize(R.styleable.StickyHeaderTableView_shtv_cellPadding, 0); + + is2DScrollingEnabled = a.getBoolean(R.styleable.StickyHeaderTableView_shtv_is2DScrollEnabled, false); + isDisplayLeftHeadersVertically = a.getBoolean(R.styleable.StickyHeaderTableView_shtv_isDisplayLeftHeadersVertically, false); + isWrapHeightOfEachRow = a.getBoolean(R.styleable.StickyHeaderTableView_shtv_isWrapHeightOfEachRow, false); + isWrapWidthOfEachColumn = a.getBoolean(R.styleable.StickyHeaderTableView_shtv_isWrapWidthOfEachColumn, false); + + headerCellFillColor = a.getColor( + R.styleable.StickyHeaderTableView_shtv_headerCellFillColor, Color.TRANSPARENT); + + contentCellFillColor = a.getColor( + R.styleable.StickyHeaderTableView_shtv_contentCellFillColor, Color.TRANSPARENT); + + } catch (Exception e) { + textLabelColor = Color.BLACK; + textHeaderColor = Color.BLACK; + dividerColor = Color.BLACK; + textLabelSize = defaultTextSize; + textHeaderSize = defaultTextSize; + dividerThickness = 0; + cellPadding = 0; + is2DScrollingEnabled = false; + headerCellFillColor = Color.TRANSPARENT; + contentCellFillColor = Color.TRANSPARENT; + } finally { + a.recycle(); + } + } else { + textLabelColor = Color.BLACK; + textHeaderColor = Color.BLACK; + dividerColor = Color.BLACK; + textLabelSize = defaultTextSize; + textHeaderSize = defaultTextSize; + dividerThickness = 0; + cellPadding = 0; + is2DScrollingEnabled = false; + headerCellFillColor = Color.TRANSPARENT; + contentCellFillColor = Color.TRANSPARENT; + } + + setupPaint(); + setupScrolling(); + } + + private float dpToPixels(Context context, float dpValue) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, metrics); + } + + private void setupPaint() { + paintStrokeRect.setStyle(Paint.Style.STROKE); + paintStrokeRect.setColor(dividerColor); + paintStrokeRect.setStrokeWidth(dividerThickness); + + paintHeaderCellFillRect.setStyle(Paint.Style.FILL); + paintHeaderCellFillRect.setColor(headerCellFillColor); + + paintContentCellFillRect.setStyle(Paint.Style.FILL); + paintContentCellFillRect.setColor(contentCellFillColor); + + paintLabelText.setStyle(Paint.Style.FILL); + paintLabelText.setColor(textLabelColor); + paintLabelText.setTextSize(textLabelSize); + paintLabelText.setTextAlign(Paint.Align.LEFT); + + paintHeaderText.setStyle(Paint.Style.FILL); + paintHeaderText.setColor(textHeaderColor); + paintHeaderText.setTextSize(textHeaderSize); + paintHeaderText.setTextAlign(Paint.Align.LEFT); + } + + private void setupScrolling() { + + nestedScrollingChildHelper = new NestedScrollingChildHelper(this); + + GestureDetector.SimpleOnGestureListener simpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() { + + public boolean onDown(MotionEvent e) { + if (isNestedScrollingEnabled()) { + startNestedScroll(NESTED_SCROLL_AXIS); + } + if (isFlinging) { + isFlinging = false; + } + return true; + } + + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (isNestedScrollingEnabled()) { + dispatchNestedPreFling(velocityX, velocityY); + } + + if (!canScrollHorizontally() && !canScrollVertically()) { + return false; + } + + final float distanceTimeFactor = 0.4f; + totalAnimDx = (distanceTimeFactor * velocityX / 2); + totalAnimDy = (distanceTimeFactor * velocityY / 2); + lastAnimDx = 0; + lastAnimDy = 0; + startTime = System.currentTimeMillis(); + endTime = startTime + (long) (1000 * distanceTimeFactor); + + float deltaY = e2.getY() - e1.getY(); + float deltaX = e2.getX() - e1.getX(); + + if (!is2DScrollingEnabled) { + if (Math.abs(deltaX) > Math.abs(deltaY)) { + isScrollingHorizontally = true; + } else { + isScrollingVertically = true; + } + } + isFlinging = true; + + if (onFlingAnimateStep()) { + if (isNestedScrollingEnabled()) { + dispatchNestedFling(-velocityX, -velocityY, true); + } + return true; + } else { + if (isNestedScrollingEnabled()) { + dispatchNestedFling(-velocityX, -velocityY, false); + } + return false; + } + + } + + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + + if (isNestedScrollingEnabled()) { + dispatchNestedPreScroll((int) distanceX, (int) distanceY, null, null); + } + + boolean isScrolled; + + if (is2DScrollingEnabled) { + isScrolled = scroll2D(distanceX, distanceY); + } else { + + if (isScrollingHorizontally) { + isScrolled = scrollHorizontal(distanceX); + } else if (isScrollingVertically) { + isScrolled = scrollVertical(distanceY); + } else { + + float deltaY = e2.getY() - e1.getY(); + float deltaX = e2.getX() - e1.getX(); + + if (Math.abs(deltaX) > Math.abs(deltaY)) { + // if deltaX > 0 : the user made a sliding right gesture + // else : the user made a sliding left gesture + isScrollingHorizontally = true; + isScrolled = scrollHorizontal(distanceX); + } else { + // if deltaY > 0 : the user made a sliding down gesture + // else : the user made a sliding up gesture + isScrollingVertically = true; + isScrolled = scrollVertical(distanceY); + } + } + } + + // Fix scrolling (if any parent view is scrollable in layout hierarchy, + // than this will disallow intercepting touch event) + if (getParent() != null && isScrolled) { + getParent().requestDisallowInterceptTouchEvent(true); + } + + if (isScrolled) { + if (isNestedScrollingEnabled()) { + dispatchNestedScroll((int) distanceX, (int) distanceY, 0, 0, null); + } + } else { + if (isNestedScrollingEnabled()) { + dispatchNestedScroll(0, 0, (int) distanceX, (int) distanceY, null); + } + } + + return isScrolled; + } + + public boolean onSingleTapUp(MotionEvent e) { + + if (onTableCellClickListener != null) { + + final float x = e.getX(); + final float y = e.getY(); + + boolean isEndLoop = false; + + for (int i = 0; i < rectEachCellBoundData.length; i++) { + + if (rectEachCellBoundData[i][0].top <= y && rectEachCellBoundData[i][0].bottom >= y) { + + for (int j = 0; j < rectEachCellBoundData[0].length; j++) { + + if (rectEachCellBoundData[i][j].left <= x && rectEachCellBoundData[i][j].right >= x) { + isEndLoop = true; + onTableCellClickListener.onTableCellClicked(i, j); + break; + } + } + } + if (isEndLoop) { + break; + } + } + } + + return super.onSingleTapUp(e); + } + + public void onLongPress(MotionEvent e) { + super.onLongPress(e); + } + + public boolean onDoubleTapEvent(MotionEvent e) { + return super.onDoubleTapEvent(e); + } + + }; + gestureDetector = new GestureDetector(getContext(), simpleOnGestureListener); + } + + /** + * This will start fling animation + * + * @return true if fling animation consumed + */ + private boolean onFlingAnimateStep() { + + boolean isScrolled = false; + + long curTime = System.currentTimeMillis(); + float percentTime = (float) (curTime - startTime) / (float) (endTime - startTime); + float percentDistance = animateInterpolator.getInterpolation(percentTime); + float curDx = percentDistance * totalAnimDx; + float curDy = percentDistance * totalAnimDy; + + float distanceX = curDx - lastAnimDx; + float distanceY = curDy - lastAnimDy; + lastAnimDx = curDx; + lastAnimDy = curDy; + + if (is2DScrollingEnabled) { + isScrolled = scroll2D(-distanceX, -distanceY); + } else if (isScrollingHorizontally) { + isScrolled = scrollHorizontal(-distanceX); + } else if (isScrollingVertically) { + isScrolled = scrollVertical(-distanceY); + } + + // This will stop fling animation if user has touch intercepted + if (!isFlinging) { + return false; + } + + if (percentTime < 1.0f) { + // fling animation running + post(this::onFlingAnimateStep); + } else { + // fling animation ended + isFlinging = false; + isScrollingVertically = false; + isScrollingHorizontally = false; + } + return isScrolled; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int desiredWidth = 0; + int desiredHeight = 0; + + if (data != null) { + updateMaxWidthHeightOfCell(); + if (isWrapHeightOfEachRow) { + + for (int i = 0; i < maxHeightSparseIntArray.size(); i++) { + desiredHeight = desiredHeight + maxHeightSparseIntArray.get(i, 0); + } + desiredHeight = desiredHeight + (dividerThickness / 2); + } else { + desiredHeight = maxHeightOfCell * data.length + (dividerThickness / 2); + } + + if (isWrapWidthOfEachColumn) { + + for (int i = 0; i < maxWidthSparseIntArray.size(); i++) { + desiredWidth = desiredWidth + maxWidthSparseIntArray.get(i, 0); + } + desiredWidth = desiredWidth + (dividerThickness / 2); + + } else { + desiredWidth = maxWidthOfCell * data[0].length + (dividerThickness / 2); + } + + scrolledRect.set(0, 0, desiredWidth, desiredHeight); + actualContentRect.set(0, 0, desiredWidth, desiredHeight); + } + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + int width; + int height; + + //Measure Width + if (widthMode == MeasureSpec.EXACTLY) { + //Must be this size + width = widthSize; + } else if (widthMode == MeasureSpec.AT_MOST) { + //Can't be bigger than... + width = Math.min(desiredWidth, widthSize); + } else { + //Be whatever you want + width = desiredWidth; + } + + //Measure Height + if (heightMode == MeasureSpec.EXACTLY) { + //Must be this size + height = heightSize; + } else if (heightMode == MeasureSpec.AT_MOST) { + //Can't be bigger than... + height = Math.min(desiredHeight, heightSize); + } else { + //Be whatever you want + height = desiredHeight; + } + + //MUST CALL THIS + setMeasuredDimension(width, height); + } + + /** + * Calculate and update max width height of cell
+ * Required for onMeasure() method + */ + private void updateMaxWidthHeightOfCell() { + // call only once otherwise it is very cpu time consuming + if (maxMeasure > 0) { + return; + } + maxMeasure++; + + maxWidthOfCell = 0; + maxHeightOfCell = 0; + maxHeightSparseIntArray.clear(); + maxWidthSparseIntArray.clear(); + + final int doubleCellPadding = cellPadding + cellPadding; + + for (int i = 0; i < data.length; i++) { + + for (int j = 0; j < data[0].length; j++) { + + if (i == 0 && j == 0) { +// data[0][0] = "xx"; + + if (data[i][j] instanceof String) { + String str = (String)data[i][j]; + paintHeaderText.getTextBounds(str, 0, str.length(), textRectBounds); + } else if (data[i][j] instanceof Drawable) { + Drawable icon = (Drawable) data[i][j]; + textRectBounds.set(0,0, icon.getIntrinsicWidth() + 30, icon.getIntrinsicHeight()); + } + + if (maxWidthOfCell < textRectBounds.width()) { + maxWidthOfCell = textRectBounds.width(); + } + if (maxHeightOfCell < textRectBounds.height()) { + maxHeightOfCell = textRectBounds.height(); + } + + if (maxWidthSparseIntArray.get(j, 0) < textRectBounds.width()) { + maxWidthSparseIntArray.put(j, textRectBounds.width()); + } + if (maxHeightSparseIntArray.get(i, 0) < textRectBounds.height()) { + maxHeightSparseIntArray.put(i, textRectBounds.height()); + } + } else if (i == 0) { + // Top headers cells + + if (data[i][j] instanceof String) { + String str = (String)data[i][j]; + paintHeaderText.getTextBounds(str, 0, str.length(), textRectBounds); + } else if (data[i][j] instanceof Drawable) { + Drawable icon = (Drawable) data[i][j]; + textRectBounds.set(0,0,icon.getIntrinsicWidth() + 30, icon.getIntrinsicHeight()); + } + if (maxWidthOfCell < textRectBounds.width()) { + maxWidthOfCell = textRectBounds.width(); + } + if (maxHeightOfCell < textRectBounds.height()) { + maxHeightOfCell = textRectBounds.height(); + } + + if (maxWidthSparseIntArray.get(j, 0) < textRectBounds.width()) { + maxWidthSparseIntArray.put(j, textRectBounds.width()); + } + if (maxHeightSparseIntArray.get(i, 0) < textRectBounds.height()) { + maxHeightSparseIntArray.put(i, textRectBounds.height()); + } + } else if (j == 0) { + // Left headers cells + if (data[i][j] instanceof String) { + String str = (String)data[i][j]; + if (str.indexOf("\n") != -1) { + String[] split = str.split("\n"); + + if (split[0].length() >= split[1].length()) { + str = split[0]; + } else { + str = split[1]; + } + } + paintHeaderText.getTextBounds(str, 0, str.length(), textRectBounds); + } else if (data[i][j] instanceof Drawable) { + Drawable icon = (Drawable) data[i][j]; + textRectBounds.set(0,0,icon.getIntrinsicWidth(), icon.getIntrinsicHeight() / 2); + } + + if (isDisplayLeftHeadersVertically) { + + if (maxWidthOfCell < textRectBounds.height()) { + maxWidthOfCell = textRectBounds.height(); + } + if (maxHeightOfCell < textRectBounds.width()) { + maxHeightOfCell = textRectBounds.width(); + } + + if (maxWidthSparseIntArray.get(j, 0) < textRectBounds.height()) { + maxWidthSparseIntArray.put(j, textRectBounds.height()); + } + if (maxHeightSparseIntArray.get(i, 0) < textRectBounds.width()) { + maxHeightSparseIntArray.put(i, textRectBounds.width()); + } + + } else { + + if (maxWidthOfCell < textRectBounds.width()) { + maxWidthOfCell = textRectBounds.width(); + } + if (maxHeightOfCell < textRectBounds.height()) { + maxHeightOfCell = textRectBounds.height(); + } + + if (maxWidthSparseIntArray.get(j, 0) < textRectBounds.width()) { + maxWidthSparseIntArray.put(j, textRectBounds.width()); + } + if (maxHeightSparseIntArray.get(i, 0) < textRectBounds.height()) { + maxHeightSparseIntArray.put(i, textRectBounds.height()); + } + } + } else { + // Other content cells + if (data[i][j] instanceof String) { + String str = (String)data[i][j]; + + if (str.indexOf("\n") != -1) { + String[] split = str.split("\n"); + + if (split[0].length() >= split[1].length()) { + str = split[0]; + } else { + str = split[1]; + } + } + paintLabelText.getTextBounds(str, 0, str.length(), textRectBounds); + } else if (data[i][j] instanceof Drawable) { + Drawable icon = (Drawable) data[i][j]; + textRectBounds.set(0,0,icon.getIntrinsicWidth(), icon.getIntrinsicHeight() / 2); + } + + if (maxWidthOfCell < textRectBounds.width()) { + maxWidthOfCell = textRectBounds.width(); + } + if (maxHeightOfCell < textRectBounds.height()) { + maxHeightOfCell = textRectBounds.height(); + } + + if (maxWidthSparseIntArray.get(j, 0) < textRectBounds.width()) { + maxWidthSparseIntArray.put(j, textRectBounds.width()); + } + if (maxHeightSparseIntArray.get(i, 0) < textRectBounds.height()) { + maxHeightSparseIntArray.put(i, textRectBounds.height()); + } + } + } + } + maxWidthOfCell = maxWidthOfCell + doubleCellPadding; + maxHeightOfCell = maxHeightOfCell + doubleCellPadding; + + for (int i = 0; i < maxHeightSparseIntArray.size(); i++) { + maxHeightSparseIntArray.put(i, maxHeightSparseIntArray.get(i, 0) + doubleCellPadding); + } + + for (int i = 0; i < maxWidthSparseIntArray.size(); i++) { + maxWidthSparseIntArray.put(i, maxWidthSparseIntArray.get(i, 0) + doubleCellPadding); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldW, int oldH) { + super.onSizeChanged(w, h, oldW, oldH); + + visibleContentRect.set(0, 0, w, h); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (data == null) { + return; + } + + + int cellLeftX; + int cellTopY = scrolledRect.top; + int cellRightX; + int cellBottomY = scrolledRect.top + getHeightOfRow(0); + int halfDividerThickness = dividerThickness / 2; + + float drawTextX; + float drawTextY; + String textToDraw; + Drawable iconToDraw; + + // *************************** Calculate each cells to draw ************************** + // This is top-left most cell (0,0) + updateRectPointData(0, 0, halfDividerThickness, halfDividerThickness, getWidthOfColumn(0), getHeightOfRow(0)); + + for (int i = 0; i < data.length; i++) { + cellRightX = scrolledRect.left; + int heightOfRowI = getHeightOfRow(i); + if (i == 0) { + cellTopY = halfDividerThickness; + for (int j = 0; j < data[i].length; j++) { + cellLeftX = cellRightX - halfDividerThickness; + cellRightX += getWidthOfColumn(j); + if (j != 0) { + // This are top header cells (0,*) + updateRectPointData(i, j, cellLeftX, cellTopY, cellRightX, heightOfRowI); + } + } + cellBottomY = scrolledRect.top + getHeightOfRow(i); + } else { + // These are content cells + for (int j = 0; j < data[0].length; j++) { + cellLeftX = cellRightX - halfDividerThickness; + cellRightX += getWidthOfColumn(j); + if (j != 0) { + updateRectPointData(i, j, cellLeftX, cellTopY, cellRightX, cellBottomY); + } + } + + // This are left header cells (*,0) + cellRightX = 0; + cellLeftX = cellRightX + halfDividerThickness; + cellRightX += getWidthOfColumn(0); + updateRectPointData(i, 0, cellLeftX, cellTopY, cellRightX, cellBottomY); + } + cellTopY = cellBottomY - halfDividerThickness; + cellBottomY = cellBottomY + getHeightOfRow(i + 1); + } + + // ******************** Draw contents & left headers ******************** + boolean isLeftVisible; + boolean isTopVisible; + boolean isRightVisible; + boolean isBottomVisible; + + for (int i = 1; i < data.length; i++) { + isTopVisible = rectEachCellBoundData[i][0].top >= rectEachCellBoundData[0][0].bottom + && rectEachCellBoundData[i][0].top <= visibleContentRect.bottom; + isBottomVisible = rectEachCellBoundData[i][0].bottom >= rectEachCellBoundData[0][0].bottom + && rectEachCellBoundData[i][0].bottom <= visibleContentRect.bottom; + + if (isTopVisible || isBottomVisible) { + + // ******************** Draw contents ******************** + for (int j = 1; j < data[i].length; j++) { + isLeftVisible = rectEachCellBoundData[i][j].left >= rectEachCellBoundData[i][0].right + && rectEachCellBoundData[i][j].left <= visibleContentRect.right; + isRightVisible = rectEachCellBoundData[i][j].right >= rectEachCellBoundData[i][0].right + && rectEachCellBoundData[i][j].right <= visibleContentRect.right; + + if (isLeftVisible || isRightVisible) { + canvas.drawRect(rectEachCellBoundData[i][j].left, rectEachCellBoundData[i][j].top, rectEachCellBoundData[i][j].right, rectEachCellBoundData[i][j].bottom, paintContentCellFillRect); + if (dividerThickness != 0) { + canvas.drawRect(rectEachCellBoundData[i][j].left, rectEachCellBoundData[i][j].top, rectEachCellBoundData[i][j].right, rectEachCellBoundData[i][j].bottom, paintStrokeRect); + } + + textToDraw = (String)data[i][j]; + // paintLabelText.getTextBounds(textToDraw, 0, textToDraw.length(), textRectBounds); + + drawTextX = rectEachCellBoundData[i][j].right - getWidthOfColumn(j) + getCellPadding(); + drawTextY = rectEachCellBoundData[i][j].bottom - (getHeightOfRow(i)) + (textRectBounds.height() / 2f); + + StaticLayout staticLayout = StaticLayout.Builder.obtain(textToDraw, 0, textToDraw.length(), paintLabelText, getWidthOfColumn(j)).build(); + + canvas.save(); + canvas.translate(drawTextX, drawTextY); + staticLayout.draw(canvas); + canvas.restore(); + + //canvas.drawText(textToDraw, 0, textToDraw.length(), drawTextX, drawTextY, paintLabelText); + } + } + + // ******************** Draw left header (*,0) ******************** + canvas.drawRect(rectEachCellBoundData[i][0].left, rectEachCellBoundData[i][0].top, rectEachCellBoundData[i][0].right, rectEachCellBoundData[i][0].bottom, paintHeaderCellFillRect); + if (dividerThickness != 0) { + canvas.drawRect(rectEachCellBoundData[i][0].left, rectEachCellBoundData[i][0].top, rectEachCellBoundData[i][0].right, rectEachCellBoundData[i][0].bottom, paintStrokeRect); + } + + textToDraw = (String)data[i][0]; + // paintHeaderText.getTextBounds(textToDraw, 0, textToDraw.length(), textRectBounds); + + if (isDisplayLeftHeadersVertically) { + drawTextX = rectEachCellBoundData[i][0].right - (getWidthOfColumn(0)) + (textRectBounds.height()); + drawTextY = rectEachCellBoundData[i][0].bottom - getCellPadding(); + + StaticLayout staticLayout = StaticLayout.Builder.obtain(textToDraw, 0, textToDraw.length(), paintHeaderText, getHeightOfRow(i)).build(); + + canvas.save(); + canvas.translate(drawTextX, drawTextY); + canvas.rotate(-90); + staticLayout.draw(canvas); + //canvas.drawText(textToDraw, 0, textToDraw.length(), drawTextX, drawTextY, paintHeaderText); + canvas.restore(); + } else { + drawTextX = rectEachCellBoundData[i][0].right - getWidthOfColumn(0) + getCellPadding(); + drawTextY = rectEachCellBoundData[i][0].bottom - (getHeightOfRow(i)) + (textRectBounds.height() / 2f); + + StaticLayout staticLayout = StaticLayout.Builder.obtain(textToDraw, 0, textToDraw.length(), paintLabelText, getWidthOfColumn(0)).build(); + + canvas.save(); + canvas.translate(drawTextX, drawTextY); + staticLayout.draw(canvas); + canvas.restore(); + + //canvas.drawText(textToDraw, 0, textToDraw.length(), drawTextX, drawTextY, paintHeaderText); + } + } + } + + // ******************** Draw top headers (0,*) ******************** + for (int j = 1; j < data[0].length; j++) { + isLeftVisible = rectEachCellBoundData[0][j].left >= rectEachCellBoundData[0][0].right + && rectEachCellBoundData[0][j].left <= visibleContentRect.right; + isRightVisible = rectEachCellBoundData[0][j].right >= rectEachCellBoundData[0][0].right + && rectEachCellBoundData[0][j].right <= visibleContentRect.right; + + if (isLeftVisible || isRightVisible) { + canvas.drawRect(rectEachCellBoundData[0][j].left, rectEachCellBoundData[0][j].top, rectEachCellBoundData[0][j].right, rectEachCellBoundData[0][j].bottom, paintHeaderCellFillRect); + if (dividerThickness != 0) { + canvas.drawRect(rectEachCellBoundData[0][j].left, rectEachCellBoundData[0][j].top, rectEachCellBoundData[0][j].right, rectEachCellBoundData[0][j].bottom, paintStrokeRect); + } + + if (data[0][j] instanceof String) { + textToDraw = (String)data[0][j]; + // paintHeaderText.getTextBounds(textToDraw, 0, textToDraw.length(), textRectBounds); + + drawTextX = rectEachCellBoundData[0][j].right - (getWidthOfColumn(j) / 2f) - (textRectBounds.width() / 2f); + drawTextY = rectEachCellBoundData[0][j].bottom - (getHeightOfRow(0) / 2f) + (textRectBounds.height() / 2f); + + canvas.drawText(textToDraw, 0, textToDraw.length(), drawTextX, drawTextY, paintHeaderText); + } else if (data[0][j] instanceof Drawable) { + iconToDraw = (Drawable) data[0][j]; + + drawTextX = rectEachCellBoundData[0][j].right - (getWidthOfColumn(j) / 2f) - (iconToDraw.getIntrinsicWidth() / 2f); + //drawTextY = rectEachCellBoundData[0][j].bottom - (getHeightOfRow(0) / 2f) + (iconToDraw.getIntrinsicHeight() / 2f); + + iconToDraw.setBounds((int)drawTextX, 25, (int)drawTextX + iconToDraw.getIntrinsicWidth(), 25 + iconToDraw.getIntrinsicHeight()); + + // draw circle with the tinted icon color and tint the icon with black + paintDrawable.setColorFilter(iconToDraw.getColorFilter()); + iconToDraw.setColorFilter(ColorUtil.COLOR_BLACK, PorterDuff.Mode.SRC_ATOP); + canvas.drawOval((int)drawTextX-25, 10, drawTextX+ iconToDraw.getIntrinsicWidth()+25, 45 + iconToDraw.getIntrinsicHeight(), paintDrawable); + + iconToDraw.draw(canvas); + + // save the tinted icon color back to the icon + iconToDraw.setColorFilter(paintDrawable.getColorFilter()); + } + } + } + + // ******************** Draw top-left most cell (0,0) ******************** + canvas.drawRect(rectEachCellBoundData[0][0].left, rectEachCellBoundData[0][0].top, rectEachCellBoundData[0][0].right, rectEachCellBoundData[0][0].bottom, paintHeaderCellFillRect); + + if (dividerThickness != 0) { + canvas.drawRect(rectEachCellBoundData[0][0].left, rectEachCellBoundData[0][0].top, rectEachCellBoundData[0][0].right, rectEachCellBoundData[0][0].bottom, paintStrokeRect); + } + + if (data[0][0] instanceof String) { + textToDraw = (String)data[0][0]; + + // paintHeaderText.getTextBounds(textToDraw, 0, textToDraw.length(), textRectBounds); + + drawTextX = getWidthOfColumn(0) - (getWidthOfColumn(0) / 2f) - (textRectBounds.width()/ 2f); + drawTextY = getHeightOfRow(0) - (getHeightOfRow(0) / 2f) + (textRectBounds.height() / 2f); + + canvas.drawText(textToDraw, 0, textToDraw.length(), drawTextX, drawTextY, paintHeaderText); + } else if (data[0][0] instanceof Drawable) { + iconToDraw = (Drawable) data[0][0]; + + drawTextX = getWidthOfColumn(0) - (getWidthOfColumn(0) / 2f) - (iconToDraw.getIntrinsicWidth()/ 2f); + //drawTextY = getHeightOfRow(0) - (getHeightOfRow(0) / 2f) + (iconToDraw.getIntrinsicHeight() / 2f); + iconToDraw.setBounds((int)drawTextX, 25, (int)drawTextX + iconToDraw.getIntrinsicWidth(), 25 + iconToDraw.getIntrinsicHeight()); + + // draw circle with the tinted icon color and tint the icon with black + paintDrawable.setColorFilter(iconToDraw.getColorFilter()); + iconToDraw.setColorFilter(ColorUtil.COLOR_BLACK, PorterDuff.Mode.SRC_ATOP); + canvas.drawOval((int)drawTextX-25, 10, drawTextX+ iconToDraw.getIntrinsicWidth()+25, 45 + iconToDraw.getIntrinsicHeight(), paintDrawable); + + iconToDraw.draw(canvas); + + // save the tinted icon color back to the icon + iconToDraw.setColorFilter(paintDrawable.getColorFilter()); + } + + + // ******************** Draw whole view border same as cell border ******************** + if (dividerThickness != 0) { + canvas.drawRect(visibleContentRect.left, visibleContentRect.top, visibleContentRect.right - halfDividerThickness, visibleContentRect.bottom - halfDividerThickness, paintStrokeRect); + } + } + + private int getWidthOfColumn(int key) { + if (isWrapWidthOfEachColumn) { + return maxWidthSparseIntArray.get(key, 0); + } else { + return maxWidthOfCell; + } + } + + private int getHeightOfRow(int key) { + if (isWrapHeightOfEachRow) { + return maxHeightSparseIntArray.get(key, 0); + } else { + return maxHeightOfCell; + } + } + + /** + * This will update cell bound rect data, which is used for handling cell click event + * + * @param i row position + * @param j column position + * @param cellLeftX leftX + * @param cellTopY topY + * @param cellRightX rightX + * @param cellBottomY bottomY + */ + private void updateRectPointData(int i, int j, int cellLeftX, int cellTopY, int cellRightX, int cellBottomY) { + if (rectEachCellBoundData[i][j] == null) { + rectEachCellBoundData[i][j] = new Rect(cellLeftX, cellTopY, cellRightX, cellBottomY); + } else { + rectEachCellBoundData[i][j].left = cellLeftX; + rectEachCellBoundData[i][j].top = cellTopY; + rectEachCellBoundData[i][j].right = cellRightX; + rectEachCellBoundData[i][j].bottom = cellBottomY; + } + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + + switch (event.getActionMasked()) { + + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + isScrollingHorizontally = false; + isScrollingVertically = false; + break; + } + + return gestureDetector.onTouchEvent(event); + //return true; + } + + private void updateLayoutChanges() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (!isInLayout()) { + requestLayout(); + } else { + invalidate(); + } + } else { + requestLayout(); + } + } + + /** + * Check if content width is bigger than view width + * + * @return true if content width is bigger than view width + */ + public boolean canScrollHorizontally() { + return actualContentRect.right > visibleContentRect.right; + } + + /** + * Check if content height is bigger than view height + * + * @return true if content height is bigger than view height + */ + public boolean canScrollVertically() { + return actualContentRect.bottom > visibleContentRect.bottom; + } + + /** + * Scroll horizontally + * + * @param distanceX distance to scroll + * @return true if horizontally scrolled, false otherwise + */ + public boolean scrollHorizontal(float distanceX) { + + if (!canScrollHorizontally() || distanceX == 0) { + return false; + } + + int newScrolledLeft = scrolledRect.left - (int) distanceX; + int newScrolledRight = scrolledRect.right - (int) distanceX; + + if (newScrolledLeft > 0) { + newScrolledLeft = 0; + newScrolledRight = actualContentRect.right; + } else if (newScrolledLeft < -(actualContentRect.right - visibleContentRect.right)) { + newScrolledLeft = -(actualContentRect.right - visibleContentRect.right); + newScrolledRight = visibleContentRect.right; + } + + if (scrolledRect.left == newScrolledLeft) { + return false; + } + scrolledRect.set(newScrolledLeft, scrolledRect.top, newScrolledRight, scrolledRect.bottom); + invalidate(); + return true; + } + + /** + * Scroll vertically + * + * @param distanceY distance to scroll + * @return true if vertically scrolled, false otherwise + */ + public boolean scrollVertical(float distanceY) { + + if (!canScrollVertically() || distanceY == 0) { + return false; + } + + int newScrolledTop = scrolledRect.top - (int) distanceY; + int newScrolledBottom = scrolledRect.bottom - (int) distanceY; + + if (newScrolledTop > 0) { + newScrolledTop = 0; + newScrolledBottom = actualContentRect.bottom; + } else if (newScrolledTop < -(actualContentRect.bottom - visibleContentRect.bottom)) { + newScrolledTop = -(actualContentRect.bottom - visibleContentRect.bottom); + newScrolledBottom = visibleContentRect.bottom; + } + + if (scrolledRect.top == newScrolledTop) { + return false; + } + scrolledRect.set(scrolledRect.left, newScrolledTop, scrolledRect.right, newScrolledBottom); + invalidate(); + return true; + } + + /** + * Scroll vertically & horizontal both side + * + * @param distanceX distance to scroll + * @param distanceY distance to scroll + * @return true if scrolled, false otherwise + */ + public boolean scroll2D(float distanceX, float distanceY) { + + boolean isScrollHappened = false; + int newScrolledLeft; + int newScrolledTop; + int newScrolledRight; + int newScrolledBottom; + + if (canScrollHorizontally()) { + newScrolledLeft = scrolledRect.left - (int) distanceX; + newScrolledRight = scrolledRect.right - (int) distanceX; + + if (newScrolledLeft > 0) { + newScrolledLeft = 0; + } + if (newScrolledLeft < -(actualContentRect.right - visibleContentRect.right)) { + newScrolledLeft = -(actualContentRect.right - visibleContentRect.right); + } + isScrollHappened = true; + } else { + newScrolledLeft = scrolledRect.left; + newScrolledRight = scrolledRect.right; + } + + if (canScrollVertically()) { + newScrolledTop = scrolledRect.top - (int) distanceY; + newScrolledBottom = scrolledRect.bottom - (int) distanceY; + + if (newScrolledTop > 0) { + newScrolledTop = 0; + } + if (newScrolledTop < -(actualContentRect.bottom - visibleContentRect.bottom)) { + newScrolledTop = -(actualContentRect.bottom - visibleContentRect.bottom); + } + isScrollHappened = true; + } else { + newScrolledTop = scrolledRect.top; + newScrolledBottom = scrolledRect.bottom; + } + + if (!isScrollHappened) { + return false; + } + + scrolledRect.set(newScrolledLeft, newScrolledTop, newScrolledRight, newScrolledBottom); + invalidate(); + return true; + } + + /** + * @return true if content are scrollable from top to bottom side + */ + public boolean canScrollTop() { + return scrolledRect.top < visibleContentRect.top; + } + + /** + * @return true if content are scrollable from bottom to top side + */ + public boolean canScrollBottom() { + return scrolledRect.bottom > visibleContentRect.bottom; + } + + /** + * @return true if content are scrollable from left to right side + */ + public boolean canScrollRight() { + return scrolledRect.right > visibleContentRect.right; + } + + /** + * @return true if content are scrollable from right to left side + */ + public boolean canScrollLeft() { + return scrolledRect.left < visibleContentRect.left; + } + + + // *************************** implemented NestedScrollChild methods ******************************************* + + @Override + public boolean isNestedScrollingEnabled() { + return nestedScrollingChildHelper.isNestedScrollingEnabled(); + } + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + nestedScrollingChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean hasNestedScrollingParent() { + return nestedScrollingChildHelper.hasNestedScrollingParent(); + } + + /** + * default Nested scroll axis is ViewCompat.SCROLL_AXIS_NONE
+ * Nested scroll axis must be one of the
ViewCompat.SCROLL_AXIS_NONE
or ViewCompat.SCROLL_AXIS_HORIZONTAL
or ViewCompat.SCROLL_AXIS_VERTICAL + * + * @param nestedScrollAxis value of nested scroll direction + */ + public void setNestedScrollAxis(int nestedScrollAxis) { + switch (nestedScrollAxis) { + + case ViewCompat.SCROLL_AXIS_HORIZONTAL: + NESTED_SCROLL_AXIS = ViewCompat.SCROLL_AXIS_HORIZONTAL; + break; + case ViewCompat.SCROLL_AXIS_VERTICAL: + NESTED_SCROLL_AXIS = ViewCompat.SCROLL_AXIS_VERTICAL; + break; + default: + NESTED_SCROLL_AXIS = ViewCompat.SCROLL_AXIS_NONE; + break; + } + } + + @Override + public boolean startNestedScroll(int axes) { + return nestedScrollingChildHelper.startNestedScroll(axes); + } + + @Override + public void stopNestedScroll() { + nestedScrollingChildHelper.stopNestedScroll(); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + return nestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return nestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + nestedScrollingChildHelper.onDetachedFromWindow(); + } + + // *************************** Getter/Setter methods ******************************************* + + /** + * @return data which is previously set by setData(data) method. otherwise null. + */ + public Object[][] getData() { + return data; + } + + /** + * Set you table content data + * + * @param data table content data + */ + public void setData(Object[][] data) { + this.data = data; + rectEachCellBoundData = new Rect[data.length][data[0].length]; + updateLayoutChanges(); + } + + /** + * set the cell click event + * + * @param onTableCellClickListener tableCellClickListener + */ + public void setOnTableCellClickListener(OnTableCellClickListener onTableCellClickListener) { + this.onTableCellClickListener = onTableCellClickListener; + } + + /** + * enable or disable 2 directional scroll + * + * @param is2DScrollingEnabled true if you wants to enable 2 directional scroll + */ + public void setIs2DScrollingEnabled(boolean is2DScrollingEnabled) { + this.is2DScrollingEnabled = is2DScrollingEnabled; + } + + /** + * Check whether is 2 directional scroll is enabled or not + * + * @return true if 2 directional scroll is enabled + */ + public boolean is2DScrollingEnabled() { + return is2DScrollingEnabled; + } + + /** + * @return text color of the content cells + */ + public int getTextLabelColor() { + return textLabelColor; + } + + /** + * Set text color for content cells + * + * @param textLabelColor color + */ + public void setTextLabelColor(int textLabelColor) { + this.textLabelColor = textLabelColor; + invalidate(); + } + + /** + * @return text color of the header cells + */ + public int getTextHeaderColor() { + return textHeaderColor; + } + + /** + * Set text color for header cells + * + * @param textHeaderColor color + */ + public void setTextHeaderColor(int textHeaderColor) { + this.textHeaderColor = textHeaderColor; + invalidate(); + } + + /** + * @return color of the cell divider or cell border + */ + public int getDividerColor() { + return dividerColor; + } + + /** + * Set divider or border color for cell + * + * @param dividerColor color + */ + public void setDividerColor(int dividerColor) { + this.dividerColor = dividerColor; + invalidate(); + } + + /** + * @return text size in pixels of content cells + */ + public int getTextLabelSize() { + return textLabelSize; + } + + /** + * Set text size in pixels for content cells
+ * You can use {@link DisplayMatrixHelper#dpToPixels(Context, float)} method to convert dp to pixel + * + * @param textLabelSize text size in pixels + */ + public void setTextLabelSize(int textLabelSize) { + this.textLabelSize = textLabelSize; + updateLayoutChanges(); + } + + /** + * @return text header size in pixels of header cells + */ + public int getTextHeaderSize() { + return textHeaderSize; + } + + /** + * Set text header size in pixels for header cells
+ * You can use {@link DisplayMatrixHelper#dpToPixels(Context, float)} method to convert dp to pixel + * + * @param textHeaderSize text header size in pixels + */ + public void setTextHeaderSize(int textHeaderSize) { + this.textHeaderSize = textHeaderSize; + updateLayoutChanges(); + } + + /** + * @return divider thickness in pixels + */ + public int getDividerThickness() { + return dividerThickness; + } + + /** + * Set divider thickness size in pixels for all cells
+ * You can use {@link DisplayMatrixHelper#dpToPixels(Context, float)} method to convert dp to pixel + * + * @param dividerThickness divider thickness size in pixels + */ + public void setDividerThickness(int dividerThickness) { + this.dividerThickness = dividerThickness; + invalidate(); + } + + /** + * @return header cell's fill color + */ + public int getHeaderCellFillColor() { + return headerCellFillColor; + } + + /** + * Set header cell fill color + * + * @param headerCellFillColor color to fill in header cell + */ + public void setHeaderCellFillColor(int headerCellFillColor) { + this.headerCellFillColor = headerCellFillColor; + invalidate(); + } + + /** + * @return content cell's fill color + */ + public int getContentCellFillColor() { + return contentCellFillColor; + } + + /** + * Set content cell fill color + * + * @param contentCellFillColor color to fill in content cell + */ + public void setContentCellFillColor(int contentCellFillColor) { + this.contentCellFillColor = contentCellFillColor; + invalidate(); + } + + /** + * @return cell padding in pixels + */ + public int getCellPadding() { + return cellPadding; + } + + /** + * Set padding for all cell of table
+ * You can use {@link DisplayMatrixHelper#dpToPixels(Context, float)} method to convert dp to pixel + * + * @param cellPadding cell padding in pixels + */ + public void setCellPadding(int cellPadding) { + this.cellPadding = cellPadding; + updateLayoutChanges(); + } + + /** + * @return true if left header cell text are displayed vertically enabled + */ + public boolean isDisplayLeftHeadersVertically() { + return isDisplayLeftHeadersVertically; + } + + /** + * Set left header text display vertically or horizontal + * + * @param displayLeftHeadersVertically true if you wants to set left header text display vertically + */ + public void setDisplayLeftHeadersVertically(boolean displayLeftHeadersVertically) { + isDisplayLeftHeadersVertically = displayLeftHeadersVertically; + updateLayoutChanges(); + } + + /** + * @return true if you settled true for wrap height of each row + */ + public boolean isWrapHeightOfEachRow() { + return isWrapHeightOfEachRow; + } + + /** + * Set whether height of each row should wrap or not + * + * @param wrapHeightOfEachRow pass true if you wants to set each row should wrap the height + */ + public void setWrapHeightOfEachRow(boolean wrapHeightOfEachRow) { + isWrapHeightOfEachRow = wrapHeightOfEachRow; + updateLayoutChanges(); + } + + /** + * @return true if you settled true for wrap width of each column + */ + public boolean isWrapWidthOfEachColumn() { + return isWrapWidthOfEachColumn; + } + + /** + * Set whether width of each column should wrap or not + * + * @param wrapWidthOfEachColumn pass true if you wants to set each column should wrap the width + */ + public void setWrapWidthOfEachColumn(boolean wrapWidthOfEachColumn) { + isWrapWidthOfEachColumn = wrapWidthOfEachColumn; + updateLayoutChanges(); + } + + /** + * @return the Rect object which is visible area on screen + */ + public Rect getVisibleContentRect() { + return visibleContentRect; + } + + /** + * @return the Rect object which is last scrolled area from actual content rectangle + */ + public Rect getScrolledRect() { + return scrolledRect; + } + + /** + * @return the Rect object which is actual content area + */ + public Rect getActualContentRect() { + return actualContentRect; + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/gui/table/TableFragment.java b/android_app/app/src/main/java/com/health/openscale/gui/table/TableFragment.java index 2cb252e3..7a8ac115 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/table/TableFragment.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/table/TableFragment.java @@ -15,50 +15,47 @@ */ package com.health.openscale.gui.table; -import android.content.res.Configuration; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.SpannableStringBuilder; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TableRow; -import android.widget.TextView; +import android.widget.ProgressBar; import androidx.activity.OnBackPressedCallback; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; import androidx.navigation.Navigation; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import com.health.openscale.R; import com.health.openscale.core.OpenScale; import com.health.openscale.core.datatypes.ScaleMeasurement; +import com.health.openscale.gui.measurement.DateMeasurementView; import com.health.openscale.gui.measurement.MeasurementEntryFragment; import com.health.openscale.gui.measurement.MeasurementView; +import com.health.openscale.gui.measurement.TimeMeasurementView; import com.health.openscale.gui.measurement.UserMeasurementView; -import com.health.openscale.gui.utils.ColorUtil; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; import java.util.List; -import static android.util.TypedValue.COMPLEX_UNIT_DIP; - public class TableFragment extends Fragment { private View tableView; - private LinearLayout tableHeaderView; - private RecyclerView recyclerView; - private MeasurementsAdapter adapter; - private LinearLayoutManager layoutManager; + private ProgressBar progressBar; + private StickyHeaderTableView tableDataView; private List measurementViews; + private List scaleMeasurementList; + private ArrayList iconList; + private final DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT); + private final DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT); + private final DateFormat dayFormat = new SimpleDateFormat("EE"); + private final SpannableStringBuilder contentFormat = new SpannableStringBuilder(); public TableFragment() { @@ -68,25 +65,36 @@ public class TableFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { tableView = inflater.inflate(R.layout.fragment_table, container, false); - tableHeaderView = tableView.findViewById(R.id.tableHeaderView); - recyclerView = tableView.findViewById(R.id.tableDataView); + progressBar = tableView.findViewById(R.id.progressBarTable); + tableDataView = tableView.findViewById(R.id.tableDataView); + progressBar.setVisibility(View.VISIBLE); - recyclerView.setHasFixedSize(true); - - layoutManager = new LinearLayoutManager(getContext()); - recyclerView.setLayoutManager(layoutManager); - - recyclerView.addItemDecoration(new DividerItemDecoration( - recyclerView.getContext(), layoutManager.getOrientation())); - - adapter = new MeasurementsAdapter(); - recyclerView.setAdapter(adapter); + tableDataView.setOnTableCellClickListener(new StickyHeaderTableView.OnTableCellClickListener() { + @Override + public void onTableCellClicked(int rowPosition, int columnPosition) { + if (rowPosition > 0) { + TableFragmentDirections.ActionNavTableToNavDataentry action = TableFragmentDirections.actionNavTableToNavDataentry(); + action.setMeasurementId(scaleMeasurementList.get(rowPosition-1).getId()); + action.setMode(MeasurementEntryFragment.DATA_ENTRY_MODE.VIEW); + Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(action); + } + } + }); measurementViews = MeasurementView.getMeasurementList( getContext(), MeasurementView.DateTimeOrder.FIRST); - for (MeasurementView measurement : measurementViews) { - measurement.setUpdateViews(false); + iconList = new ArrayList<>(); + + for (MeasurementView measurementView : measurementViews) { + if (!measurementView.isVisible() || measurementView instanceof UserMeasurementView || measurementView instanceof TimeMeasurementView) { + continue; + } + + measurementView.setUpdateViews(false); + + measurementView.getIcon().setColorFilter(measurementView.getColor(), PorterDuff.Mode.SRC_ATOP); + iconList.add(measurementView.getIcon()); } OpenScale.getInstance().getScaleMeasurementsLiveData().observe(getViewLifecycleOwner(), new Observer>() { @@ -105,183 +113,57 @@ public class TableFragment extends Fragment { requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), onBackPressedCallback); - return tableView; } public void updateOnView(List scaleMeasurementList) { - tableHeaderView.removeAllViews(); + this.scaleMeasurementList = scaleMeasurementList; - final int iconHeight = pxImageDp(20); - ArrayList visibleMeasurements = new ArrayList<>(); + Object[][] tableData = new Object[scaleMeasurementList.size()+1][iconList.size()]; - for (MeasurementView measurement : measurementViews) { - if (!measurement.isVisible() || measurement instanceof UserMeasurementView) { - continue; - } - - - ImageView headerIcon = new ImageView(tableView.getContext()); - headerIcon.setImageDrawable(measurement.getIcon()); - headerIcon.setColorFilter(ColorUtil.getTintColor(tableView.getContext())); - headerIcon.setLayoutParams(new TableRow.LayoutParams(0, iconHeight, 1)); - headerIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - - tableHeaderView.addView(headerIcon); - - visibleMeasurements.add(measurement); + // add header icons to the first table data row + for (int j=0; j { - public static final int VIEW_TYPE_MEASUREMENT = 0; - public static final int VIEW_TYPE_YEAR = 1; - public class ViewHolder extends RecyclerView.ViewHolder { - public LinearLayout measurementView; - public ViewHolder(LinearLayout view) { - super(view); - measurementView = view; - } - } - - private List visibleMeasurements; - private List scaleMeasurements; - - public void setMeasurements(List visibleMeasurements, - List scaleMeasurements) { - this.visibleMeasurements = visibleMeasurements; - this.scaleMeasurements = new ArrayList<>(scaleMeasurements.size() + 10); - - Calendar calendar = Calendar.getInstance(); - if (!scaleMeasurements.isEmpty()) { - calendar.setTime(scaleMeasurements.get(0).getDateTime()); - } - calendar.set(calendar.get(Calendar.YEAR), 0, 1, 0, 0, 0); - calendar.set(calendar.MILLISECOND, 0); - - // Copy all measurements from input parameter to member variable and insert - // an extra "null" entry when the year changes. - Date yearStart = calendar.getTime(); - for (int i = 0; i < scaleMeasurements.size(); ++i) { - final ScaleMeasurement measurement = scaleMeasurements.get(i); - - if (measurement.getDateTime().before(yearStart)) { - this.scaleMeasurements.add(null); - - Calendar newCalendar = Calendar.getInstance(); - newCalendar.setTime(measurement.getDateTime()); - calendar.set(Calendar.YEAR, newCalendar.get(Calendar.YEAR)); - yearStart = calendar.getTime(); - } - - this.scaleMeasurements.add(measurement); - } - - notifyDataSetChanged(); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - LinearLayout row = new LinearLayout(getContext()); - row.setLayoutParams(new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT)); - - final int screenSize = getResources().getConfiguration() - .screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK; - final boolean isSmallScreen = - screenSize != Configuration.SCREENLAYOUT_SIZE_XLARGE - && screenSize != Configuration.SCREENLAYOUT_SIZE_LARGE; - - final int count = viewType == VIEW_TYPE_YEAR ? 1 : visibleMeasurements.size(); - for (int i = 0; i < count; ++i) { - TextView column = new TextView(getContext()); - column.setLayoutParams(new LinearLayout.LayoutParams( - 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); - - if (viewType == VIEW_TYPE_MEASUREMENT) { - column.setMinLines(2); - column.setGravity(Gravity.CENTER_HORIZONTAL); - - if (isSmallScreen) { - column.setTextSize(COMPLEX_UNIT_DIP, 9); - } - } - else { - column.setPadding(0, 10, 0, 10); - column.setGravity(Gravity.CENTER); - column.setTextSize(COMPLEX_UNIT_DIP, 16); - } - - row.addView(column); - } - - return new ViewHolder(row); - } - - @Override - public void onBindViewHolder(ViewHolder holder, int position) { - LinearLayout row = holder.measurementView; - - final ScaleMeasurement measurement = scaleMeasurements.get(position); - if (measurement == null) { - ScaleMeasurement nextMeasurement = scaleMeasurements.get(position + 1); - Calendar calendar = Calendar.getInstance(); - calendar.setTime(nextMeasurement.getDateTime()); - - TextView column = (TextView) row.getChildAt(0); - column.setText(String.format("%d", calendar.get(Calendar.YEAR))); - return; - } - - ScaleMeasurement prevMeasurement = null; - if (position + 1 < scaleMeasurements.size()) { - prevMeasurement = scaleMeasurements.get(position + 1); - if (prevMeasurement == null) { - prevMeasurement = scaleMeasurements.get(position + 2); - } - } - - // Fill view with data - for (int i = 0; i < visibleMeasurements.size(); ++i) { - final MeasurementView view = visibleMeasurements.get(i); - view.loadFrom(measurement, prevMeasurement); - - SpannableStringBuilder string = new SpannableStringBuilder(); - string.append(view.getValueAsString(false)); - view.appendDiffValue(string, true); - - TextView column = (TextView) row.getChildAt(i); - column.setText(string); - } - - row.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - TableFragmentDirections.ActionNavTableToNavDataentry action = TableFragmentDirections.actionNavTableToNavDataentry(); - action.setMeasurementId(measurement.getId()); - action.setMode(MeasurementEntryFragment.DATA_ENTRY_MODE.VIEW); - Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(action); - } - }); - } - - @Override - public int getItemCount() { - return scaleMeasurements == null ? 0 : scaleMeasurements.size(); - } - - @Override - public int getItemViewType(int position) { - return scaleMeasurements.get(position) != null ? VIEW_TYPE_MEASUREMENT : VIEW_TYPE_YEAR; - } - } } diff --git a/android_app/app/src/main/res/drawable/ic_expand_less.xml b/android_app/app/src/main/res/drawable/ic_expand_less.xml new file mode 100644 index 00000000..a55069f0 --- /dev/null +++ b/android_app/app/src/main/res/drawable/ic_expand_less.xml @@ -0,0 +1,10 @@ + + + diff --git a/android_app/app/src/main/res/drawable/ic_expand_more.xml b/android_app/app/src/main/res/drawable/ic_expand_more.xml new file mode 100644 index 00000000..adc215c4 --- /dev/null +++ b/android_app/app/src/main/res/drawable/ic_expand_more.xml @@ -0,0 +1,10 @@ + + + diff --git a/android_app/app/src/main/res/layout/fragment_overview.xml b/android_app/app/src/main/res/layout/fragment_overview.xml index 41c7f9e0..770a770c 100644 --- a/android_app/app/src/main/res/layout/fragment_overview.xml +++ b/android_app/app/src/main/res/layout/fragment_overview.xml @@ -1,113 +1,94 @@ - + android:layout_width="match_parent" + android:layout_height="match_parent"> - - + android:spinnerMode="dialog" + android:textAlignment="center" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + - + - + - - + - + - + - - - - - - - - - - - - - - + - + app:layout_constraintBottom_toBottomOf="@+id/chartView" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/chartActionBar" /> + + diff --git a/android_app/app/src/main/res/layout/fragment_table.xml b/android_app/app/src/main/res/layout/fragment_table.xml index d21ccced..99962a03 100644 --- a/android_app/app/src/main/res/layout/fragment_table.xml +++ b/android_app/app/src/main/res/layout/fragment_table.xml @@ -1,24 +1,38 @@ - + android:layout_height="match_parent"> - - - - - - + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:shtv_cellPadding="16dp" + app:shtv_dividerColor="?android:colorBackground" + app:shtv_dividerThickness="1dp" + app:shtv_headerCellFillColor="?android:colorBackground" + app:shtv_contentCellFillColor="?android:colorBackground" + app:shtv_is2DScrollEnabled="true" + app:shtv_isDisplayLeftHeadersVertically="false" + app:shtv_isWrapHeightOfEachRow="true" + app:shtv_isWrapWidthOfEachColumn="true" + app:shtv_textHeaderColor="?android:colorForeground" + app:shtv_textHeaderSize="14dp" + app:shtv_textLabelColor="?android:colorForeground" + app:shtv_textLabelSize="14dp" /> + + + diff --git a/android_app/app/src/main/res/layout/item_overview.xml b/android_app/app/src/main/res/layout/item_overview.xml new file mode 100644 index 00000000..9bc14b2d --- /dev/null +++ b/android_app/app/src/main/res/layout/item_overview.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android_app/app/src/main/res/layout/spinner_item.xml b/android_app/app/src/main/res/layout/spinner_item.xml index 6ba21ec6..8043c265 100644 --- a/android_app/app/src/main/res/layout/spinner_item.xml +++ b/android_app/app/src/main/res/layout/spinner_item.xml @@ -2,9 +2,7 @@ + android:gravity="center" /> diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index cc2b2035..6f4fa390 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -197,6 +197,7 @@ Set default order Overwrite previous export \"%s\"? Is on right axis + Is sticky Measurement in % Estimate measurement Estimation formula diff --git a/android_app/app/src/main/res/values/styles.xml b/android_app/app/src/main/res/values/styles.xml index 57015550..fd89d2fe 100644 --- a/android_app/app/src/main/res/values/styles.xml +++ b/android_app/app/src/main/res/values/styles.xml @@ -37,4 +37,26 @@ @android:string/ok @android:string/cancel + + + + + + + + + + + + + + + + + + + + + +