Merge pull request #3485 from ByteHamster/statistics-chart

Added pie chart to statistics
This commit is contained in:
H. Lehmann 2019-10-04 10:54:00 +02:00 committed by GitHub
commit 5e31ecb253
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 271 additions and 125 deletions

View File

@ -1,31 +1,30 @@
package de.danoeh.antennapod.adapter;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import java.util.List;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.view.PieChartView;
/**
* Adapter for the statistics list
*/
public class StatisticsListAdapter extends BaseAdapter {
public class StatisticsListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 0;
private static final int TYPE_FEED = 1;
private final Context context;
private List<DBReader.StatisticsItem> feedTime = new ArrayList<>();
private DBReader.StatisticsData statisticsData;
private boolean countAll = true;
public StatisticsListAdapter(Context context) {
@ -37,66 +36,102 @@ public class StatisticsListAdapter extends BaseAdapter {
}
@Override
public int getCount() {
return feedTime.size();
public int getItemCount() {
return statisticsData.feedTime.size() + 1;
}
@Override
public DBReader.StatisticsItem getItem(int position) {
return feedTime.get(position);
}
@Override
public long getItemId(int position) {
return feedTime.get(position).feed.getId();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
StatisticsHolder holder;
Feed feed = feedTime.get(position).feed;
if (convertView == null) {
holder = new StatisticsHolder();
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.statistics_listitem, parent, false);
holder.image = convertView.findViewById(R.id.imgvCover);
holder.title = convertView.findViewById(R.id.txtvTitle);
holder.time = convertView.findViewById(R.id.txtvTime);
convertView.setTag(holder);
} else {
holder = (StatisticsHolder) convertView.getTag();
if (position == 0) {
return null;
}
Glide.with(context)
.load(feed.getImageLocation())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate())
.into(holder.image);
holder.title.setText(feed.getTitle());
holder.time.setText(Converter.shortLocalizedDuration(context,
countAll ? feedTime.get(position).timePlayedCountAll
: feedTime.get(position).timePlayed));
return convertView;
return statisticsData.feedTime.get(position - 1);
}
public void update(List<DBReader.StatisticsItem> feedTime) {
this.feedTime = feedTime;
@Override
public int getItemViewType(int position) {
return position == 0 ? TYPE_HEADER : TYPE_FEED;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(context);
if (viewType == TYPE_HEADER) {
return new HeaderHolder(inflater.inflate(R.layout.statistics_listitem_total_time, parent, false));
}
return new StatisticsHolder(inflater.inflate(R.layout.statistics_listitem, parent, false));
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder h, int position) {
if (getItemViewType(position) == TYPE_HEADER) {
HeaderHolder holder = (HeaderHolder) h;
long time = countAll ? statisticsData.totalTimeCountAll : statisticsData.totalTime;
holder.totalTime.setText(Converter.shortLocalizedDuration(context, time));
float[] dataValues = new float[statisticsData.feedTime.size()];
for (int i = 0; i < statisticsData.feedTime.size(); i++) {
DBReader.StatisticsItem item = statisticsData.feedTime.get(i);
dataValues[i] = countAll ? item.timePlayedCountAll : item.timePlayed;
}
holder.pieChart.setData(dataValues);
} else {
StatisticsHolder holder = (StatisticsHolder) h;
DBReader.StatisticsItem statsItem = statisticsData.feedTime.get(position - 1);
Glide.with(context)
.load(statsItem.feed.getImageLocation())
.apply(new RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
.dontAnimate())
.into(holder.image);
holder.title.setText(statsItem.feed.getTitle());
long time = countAll ? statsItem.timePlayedCountAll : statsItem.timePlayed;
holder.time.setText(Converter.shortLocalizedDuration(context, time));
holder.itemView.setOnClickListener(v -> {
AlertDialog.Builder dialog = new AlertDialog.Builder(context);
dialog.setTitle(statsItem.feed.getTitle());
dialog.setMessage(context.getString(R.string.statistics_details_dialog,
countAll ? statsItem.episodesStartedIncludingMarked : statsItem.episodesStarted,
statsItem.episodes, Converter.shortLocalizedDuration(context,
countAll ? statsItem.timePlayedCountAll : statsItem.timePlayed),
Converter.shortLocalizedDuration(context, statsItem.time)));
dialog.setPositiveButton(android.R.string.ok, null);
dialog.show();
});
}
}
public void update(DBReader.StatisticsData statistics) {
this.statisticsData = statistics;
notifyDataSetChanged();
}
static class StatisticsHolder {
static class HeaderHolder extends RecyclerView.ViewHolder {
TextView totalTime;
PieChartView pieChart;
HeaderHolder(View itemView) {
super(itemView);
totalTime = itemView.findViewById(R.id.total_time);
pieChart = itemView.findViewById(R.id.pie_chart);
}
}
static class StatisticsHolder extends RecyclerView.ViewHolder {
ImageView image;
TextView title;
TextView time;
StatisticsHolder(View itemView) {
super(itemView);
image = itemView.findViewById(R.id.imgvCover);
title = itemView.findViewById(R.id.txtvTitle);
time = itemView.findViewById(R.id.txtvTime);
}
}
}

View File

@ -7,6 +7,8 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@ -32,14 +34,13 @@ import io.reactivex.schedulers.Schedulers;
/**
* Displays the 'statistics' screen
*/
public class StatisticsFragment extends Fragment implements AdapterView.OnItemClickListener {
public class StatisticsFragment extends Fragment {
private static final String TAG = StatisticsFragment.class.getSimpleName();
private static final String PREF_NAME = "StatisticsActivityPrefs";
private static final String PREF_COUNT_ALL = "countAll";
private Disposable disposable;
private TextView totalTimeTextView;
private ListView feedStatisticsList;
private RecyclerView feedStatisticsList;
private ProgressBar progressBar;
private StatisticsListAdapter listAdapter;
private boolean countAll = false;
@ -57,13 +58,12 @@ public class StatisticsFragment extends Fragment implements AdapterView.OnItemCl
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.statistics_activity, container, false);
totalTimeTextView = root.findViewById(R.id.total_time);
feedStatisticsList = root.findViewById(R.id.statistics_list);
progressBar = root.findViewById(R.id.progressBar);
listAdapter = new StatisticsListAdapter(getContext());
listAdapter.setCountAll(countAll);
feedStatisticsList.setLayoutManager(new LinearLayoutManager(getContext()));
feedStatisticsList.setAdapter(listAdapter);
feedStatisticsList.setOnItemClickListener(this);
return root;
}
@ -112,7 +112,6 @@ public class StatisticsFragment extends Fragment implements AdapterView.OnItemCl
private void refreshStatistics() {
progressBar.setVisibility(View.VISIBLE);
totalTimeTextView.setVisibility(View.GONE);
feedStatisticsList.setVisibility(View.GONE);
loadStatistics();
}
@ -125,27 +124,9 @@ public class StatisticsFragment extends Fragment implements AdapterView.OnItemCl
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
totalTimeTextView.setText(Converter.shortLocalizedDuration(getContext(),
countAll ? result.totalTimeCountAll : result.totalTime));
listAdapter.update(result.feedTime);
listAdapter.update(result);
progressBar.setVisibility(View.GONE);
totalTimeTextView.setVisibility(View.VISIBLE);
feedStatisticsList.setVisibility(View.VISIBLE);
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
DBReader.StatisticsItem stats = listAdapter.getItem(position);
AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
dialog.setTitle(stats.feed.getTitle());
dialog.setMessage(getString(R.string.statistics_details_dialog,
countAll ? stats.episodesStartedIncludingMarked : stats.episodesStarted,
stats.episodes, Converter.shortLocalizedDuration(getContext(),
countAll ? stats.timePlayedCountAll : stats.timePlayed),
Converter.shortLocalizedDuration(getContext(), stats.time)));
dialog.setPositiveButton(android.R.string.ok, null);
dialog.show();
}
}

View File

@ -0,0 +1,121 @@
package de.danoeh.antennapod.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;
import io.reactivex.annotations.Nullable;
public class PieChartView extends AppCompatImageView {
private PieChartDrawable drawable;
public PieChartView(Context context) {
super(context);
setup();
}
public PieChartView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setup();
}
public PieChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setup();
}
@SuppressLint("ClickableViewAccessibility")
private void setup() {
drawable = new PieChartDrawable();
setImageDrawable(drawable);
}
/**
* Set array od names, array of values and array of colors.
*/
public void setData(float[] dataValues) {
drawable.dataValues = dataValues;
drawable.valueSum = 0;
for (float datum : dataValues) {
drawable.valueSum += datum;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
setMeasuredDimension(width, width / 2);
}
private static class PieChartDrawable extends Drawable {
private static final float MIN_DEGREES = 10f;
private static final float PADDING_DEGREES = 3f;
private static final float STROKE_SIZE = 15f;
private static final int[] COLOR_VALUES = new int[]{0xFF3775E6, 0xffe51c23, 0xffff9800, 0xff259b24, 0xff9c27b0,
0xff0099c6, 0xffdd4477, 0xff66aa00, 0xffb82e2e, 0xff316395,
0xff994499, 0xff22aa99, 0xffaaaa11, 0xff6633cc, 0xff0073e6};
private float[] dataValues;
private float valueSum;
private final Paint paint;
private PieChartDrawable() {
paint = new Paint();
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(STROKE_SIZE);
}
@Override
public void draw(@NonNull Canvas canvas) {
if (valueSum == 0) {
return;
}
float radius = getBounds().height() - STROKE_SIZE;
float center = getBounds().width() / 2.f;
RectF arcBounds = new RectF(center - radius, STROKE_SIZE, center + radius, STROKE_SIZE + radius * 2);
float startAngle = 180;
for (int i = 0; i < dataValues.length; i++) {
float datum = dataValues[i];
float sweepAngle = 180 * datum / valueSum;
if (sweepAngle < MIN_DEGREES) {
break;
}
paint.setColor(COLOR_VALUES[i % COLOR_VALUES.length]);
float padding = i == 0 ? PADDING_DEGREES / 2 : PADDING_DEGREES;
canvas.drawArc(arcBounds, startAngle + padding, sweepAngle - padding, false, paint);
startAngle = startAngle + sweepAngle;
}
paint.setColor(Color.GRAY);
float sweepAngle = 360 - startAngle - PADDING_DEGREES / 2;
if (sweepAngle > PADDING_DEGREES) {
canvas.drawArc(arcBounds, startAngle + PADDING_DEGREES, sweepAngle - PADDING_DEGREES, false, paint);
}
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(ColorFilter cf) {
}
}
}

View File

@ -1,58 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:id="@+id/progressBar"
android:layout_gravity="center"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:text="@string/total_time_listened_to_podcasts"
android:gravity="center_horizontal"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/total_time"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center_horizontal"
android:textSize="28sp"
tools:text="10.0 hours"/>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progressBar"
android:layout_gravity="center_horizontal"
style="?android:attr/progressBarStyleSmall"/>
</LinearLayout>
<View
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/dividerVertical"/>
<ListView
<android.support.v7.widget.RecyclerView
android:id="@+id/statistics_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:choiceMode="singleChoice"
android:layout_height="match_parent"
android:clipToPadding="false"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:paddingBottom="@dimen/list_vertical_padding"
android:paddingTop="@dimen/list_vertical_padding"
android:scrollbarStyle="outsideOverlay"
tools:listitem="@layout/statistics_listitem"/>
</LinearLayout>
</FrameLayout>

View File

@ -7,7 +7,8 @@
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
android:paddingBottom="8dp"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/imgvCover"

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<de.danoeh.antennapod.view.PieChartView
android:id="@+id/pie_chart"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/total_time_description"
android:textSize="14sp"
android:text="@string/total_time_listened_to_podcasts"
android:gravity="center_horizontal"
android:layout_above="@+id/total_time"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/total_time"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center_horizontal"
android:textSize="28sp"
android:layout_marginBottom="16dp"
android:layout_alignBottom="@id/pie_chart"
tools:text="10.0 hours"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:background="?android:attr/dividerVertical"
android:layout_below="@+id/pie_chart"/>
</RelativeLayout>