Merge pull request #3485 from ByteHamster/statistics-chart
Added pie chart to statistics
This commit is contained in:
commit
5e31ecb253
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue