From 3b47deb7052aee1036416401eada6b680fd8c713 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Tue, 15 Mar 2022 20:52:15 +0100 Subject: [PATCH] Make yearly statistics a bar chart instead (#5759) --- .../ui/statistics/years/BarChartView.java | 135 +++++++++++++++++ .../ui/statistics/years/LineChartView.java | 138 ------------------ .../years/YearStatisticsListAdapter.java | 37 ++--- ...t.xml => statistics_listitem_barchart.xml} | 8 +- 4 files changed, 157 insertions(+), 161 deletions(-) create mode 100644 ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/BarChartView.java delete mode 100644 ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/LineChartView.java rename ui/statistics/src/main/res/layout/{statistics_listitem_linechart.xml => statistics_listitem_barchart.xml} (70%) diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/BarChartView.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/BarChartView.java new file mode 100644 index 000000000..eadbb29ee --- /dev/null +++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/BarChartView.java @@ -0,0 +1,135 @@ +package de.danoeh.antennapod.ui.statistics.years; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.statistics.R; + +import java.util.List; + +public class BarChartView extends AppCompatImageView { + private BarChartDrawable drawable; + + public BarChartView(Context context) { + super(context); + setup(); + } + + public BarChartView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + setup(); + } + + public BarChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setup(); + } + + @SuppressLint("ClickableViewAccessibility") + private void setup() { + drawable = new BarChartDrawable(); + setImageDrawable(drawable); + } + + /** + * Set of data values to display. + */ + public void setData(List data) { + drawable.data = data; + drawable.maxValue = 1; + for (DBReader.MonthlyStatisticsItem item : data) { + drawable.maxValue = Math.max(drawable.maxValue, item.timePlayed); + } + } + + private class BarChartDrawable extends Drawable { + private static final long ONE_HOUR = 3600000L; + private List data; + private long maxValue = 1; + private final Paint paintBars; + private final Paint paintGridLines; + private final Paint paintGridText; + private final int[] colors = {0, 0xff9c27b0}; + + private BarChartDrawable() { + colors[0] = ThemeUtils.getColorFromAttr(getContext(), R.attr.colorAccent); + paintBars = new Paint(); + paintBars.setStyle(Paint.Style.FILL); + paintBars.setAntiAlias(true); + paintGridLines = new Paint(); + paintGridLines.setStyle(Paint.Style.STROKE); + paintGridLines.setPathEffect(new DashPathEffect(new float[] {10f, 10f}, 0f)); + paintGridLines.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorSecondary)); + paintGridText = new Paint(); + paintGridText.setAntiAlias(true); + paintGridText.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorSecondary)); + } + + @Override + public void draw(@NonNull Canvas canvas) { + final float width = getBounds().width(); + final float height = getBounds().height(); + final float barHeight = height * 0.9f; + final float textPadding = width * 0.05f; + final float stepSize = (width - textPadding) / (data.size() + 2); + final float textSize = height * 0.06f; + paintGridText.setTextSize(textSize); + + paintBars.setStrokeWidth(height * 0.015f); + paintBars.setColor(colors[0]); + int colorIndex = 0; + int lastYear = data.size() > 0 ? data.get(0).year : 0; + for (int i = 0; i < data.size(); i++) { + float x = textPadding + (i + 1) * stepSize; + if (lastYear != data.get(i).year) { + lastYear = data.get(i).year; + colorIndex++; + paintBars.setColor(colors[colorIndex % 2]); + if (i < data.size() - 2) { + canvas.drawText(String.valueOf(data.get(i).year), x + stepSize, + barHeight + (height - barHeight + textSize) / 2, paintGridText); + } + canvas.drawLine(x, height, x, barHeight, paintGridText); + } + + float valuePercentage = (float) Math.max(0.005, (float) data.get(i).timePlayed / maxValue); + float y = (1 - valuePercentage) * barHeight; + canvas.drawRect(x, y, x + stepSize * 0.95f, barHeight, paintBars); + } + + float maxLine = (float) (Math.floor(maxValue / (10.0 * ONE_HOUR)) * 10 * ONE_HOUR); + float y = (1 - (maxLine / maxValue)) * barHeight; + canvas.drawLine(0, y, width, y, paintGridLines); + canvas.drawText(String.valueOf((long) maxLine / ONE_HOUR), 0, y + 1.2f * textSize, paintGridText); + + float midLine = maxLine / 2; + y = (1 - (midLine / maxValue)) * barHeight; + canvas.drawLine(0, y, width, y, paintGridLines); + canvas.drawText(String.valueOf((long) midLine / ONE_HOUR), 0, y + 1.2f * textSize, paintGridText); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter cf) { + } + } +} diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/LineChartView.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/LineChartView.java deleted file mode 100644 index e56bb4f56..000000000 --- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/LineChartView.java +++ /dev/null @@ -1,138 +0,0 @@ -package de.danoeh.antennapod.ui.statistics.years; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.DashPathEffect; -import android.graphics.LinearGradient; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.PixelFormat; -import android.graphics.Shader; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import androidx.annotation.NonNull; -import androidx.appcompat.widget.AppCompatImageView; -import de.danoeh.antennapod.ui.common.ThemeUtils; -import de.danoeh.antennapod.ui.statistics.R; -import io.reactivex.annotations.Nullable; - -public class LineChartView extends AppCompatImageView { - private LineChartDrawable drawable; - - public LineChartView(Context context) { - super(context); - setup(); - } - - public LineChartView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - setup(); - } - - public LineChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setup(); - } - - @SuppressLint("ClickableViewAccessibility") - private void setup() { - drawable = new LineChartDrawable(); - setImageDrawable(drawable); - } - - /** - * Set of data values to display. - */ - public void setData(LineChartData data) { - drawable.data = data; - } - - public static class LineChartData { - private final long valueMax; - private final long[] values; - private final long[] verticalLines; - - public LineChartData(long[] values, long[] verticalLines) { - this.values = values; - long valueMax = 0; - for (long datum : values) { - valueMax = Math.max(datum, valueMax); - } - this.valueMax = valueMax; - this.verticalLines = verticalLines; - } - - public float getHeight(int item) { - return (float) values[item] / valueMax; - } - } - - private class LineChartDrawable extends Drawable { - private LineChartData data; - private final Paint paintLine; - private final Paint paintBackground; - private final Paint paintVerticalLines; - - private LineChartDrawable() { - paintLine = new Paint(); - paintLine.setFlags(Paint.ANTI_ALIAS_FLAG); - paintLine.setStyle(Paint.Style.STROKE); - paintLine.setStrokeJoin(Paint.Join.ROUND); - paintLine.setStrokeCap(Paint.Cap.ROUND); - paintLine.setColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorAccent)); - paintBackground = new Paint(); - paintBackground.setStyle(Paint.Style.FILL); - paintVerticalLines = new Paint(); - paintVerticalLines.setStyle(Paint.Style.STROKE); - paintVerticalLines.setPathEffect(new DashPathEffect(new float[] {10f, 10f}, 0f)); - paintVerticalLines.setColor(0x66777777); - } - - @Override - public void draw(@NonNull Canvas canvas) { - float width = getBounds().width(); - float height = getBounds().height(); - float usableHeight = height * 0.9f; - float stepSize = width / (data.values.length + 1); - - paintVerticalLines.setStrokeWidth(height * 0.005f); - for (long line : data.verticalLines) { - canvas.drawLine((line + 1) * stepSize, 0, (line + 1) * stepSize, height, paintVerticalLines); - } - - paintLine.setStrokeWidth(height * 0.015f); - Path path = new Path(); - for (int i = 0; i < data.values.length; i++) { - if (i == 0) { - path.moveTo((i + 1) * stepSize, (1 - data.getHeight(i)) * usableHeight + height * 0.05f); - } else { - path.lineTo((i + 1) * stepSize, (1 - data.getHeight(i)) * usableHeight + height * 0.05f); - } - } - canvas.drawPath(path, paintLine); - - path.lineTo(data.values.length * stepSize, height); - path.lineTo(stepSize, height); - paintBackground.setShader(new LinearGradient(0, 0, 0, height, - (ThemeUtils.getColorFromAttr(getContext(), R.attr.colorAccent) & 0xffffff) + 0x66000000, - Color.TRANSPARENT, Shader.TileMode.CLAMP)); - canvas.drawPath(path, paintBackground); - } - - @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; - } - - @Override - public void setAlpha(int alpha) { - } - - @Override - public void setColorFilter(ColorFilter cf) { - } - } -} diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java index ed5a7a4f1..e3251a96b 100644 --- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java +++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/years/YearStatisticsListAdapter.java @@ -8,7 +8,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.ui.statistics.R; import java.util.ArrayList; @@ -23,8 +22,8 @@ public class YearStatisticsListAdapter extends RecyclerView.Adapter statisticsData = new ArrayList<>(); - LineChartView.LineChartData lineChartData; + private final List statisticsData = new ArrayList<>(); + private final List yearlyAggregate = new ArrayList<>(); public YearStatisticsListAdapter(Context context) { this.context = context; @@ -32,7 +31,7 @@ public class YearStatisticsListAdapter extends RecyclerView.Adapter 0 ? statistics.get(0).year : 0; int lastDataPoint = statistics.size() > 0 ? (statistics.get(0).month - 1) + lastYear * 12 : 0; long yearSum = 0; + yearlyAggregate.clear(); statisticsData.clear(); - LongList lineChartValues = new LongList(); - LongList lineChartHorizontalLines = new LongList(); for (DBReader.MonthlyStatisticsItem statistic : statistics) { if (statistic.year != lastYear) { DBReader.MonthlyStatisticsItem yearAggregate = new DBReader.MonthlyStatisticsItem(); yearAggregate.year = lastYear; yearAggregate.timePlayed = yearSum; - statisticsData.add(yearAggregate); + yearlyAggregate.add(yearAggregate); yearSum = 0; lastYear = statistic.year; - lineChartHorizontalLines.add(lineChartValues.size()); } yearSum += statistic.timePlayed; while (lastDataPoint + 1 < (statistic.month - 1) + statistic.year * 12) { - lineChartValues.add(0); // Compensate for months without playback lastDataPoint++; + DBReader.MonthlyStatisticsItem item = new DBReader.MonthlyStatisticsItem(); + item.year = lastDataPoint / 12; + item.month = lastDataPoint % 12 + 1; + statisticsData.add(item); // Compensate for months without playback + System.out.println("aaaaa extra:" + item.month + "/" + item.year); } - lineChartValues.add(statistic.timePlayed); + System.out.println("aaaaa add:" + statistic.month + "/" + statistic.year); + statisticsData.add(statistic); lastDataPoint = (statistic.month - 1) + statistic.year * 12; } DBReader.MonthlyStatisticsItem yearAggregate = new DBReader.MonthlyStatisticsItem(); yearAggregate.year = lastYear; yearAggregate.timePlayed = yearSum; - statisticsData.add(yearAggregate); - Collections.reverse(statisticsData); - lineChartData = new LineChartView.LineChartData(lineChartValues.toArray(), lineChartHorizontalLines.toArray()); + yearlyAggregate.add(yearAggregate); + Collections.reverse(yearlyAggregate); notifyDataSetChanged(); } static class HeaderHolder extends RecyclerView.ViewHolder { - LineChartView lineChart; + BarChartView barChart; HeaderHolder(View itemView) { super(itemView); - lineChart = itemView.findViewById(R.id.lineChart); + barChart = itemView.findViewById(R.id.barChart); } } diff --git a/ui/statistics/src/main/res/layout/statistics_listitem_linechart.xml b/ui/statistics/src/main/res/layout/statistics_listitem_barchart.xml similarity index 70% rename from ui/statistics/src/main/res/layout/statistics_listitem_linechart.xml rename to ui/statistics/src/main/res/layout/statistics_listitem_barchart.xml index e7d4052de..d70e1da07 100644 --- a/ui/statistics/src/main/res/layout/statistics_listitem_linechart.xml +++ b/ui/statistics/src/main/res/layout/statistics_listitem_barchart.xml @@ -6,12 +6,10 @@ android:orientation="vertical" android:padding="16dp"> - + android:layout_height="200dp" />