diff options
author | ByteHamster <info@bytehamster.com> | 2020-02-21 19:02:53 +0100 |
---|---|---|
committer | ByteHamster <info@bytehamster.com> | 2020-02-21 19:02:53 +0100 |
commit | b3ea96e7b3d3409a02da64f9e93511cc3400709a (patch) | |
tree | 529b4b72831a8c9e2daac74839d71f70636e095c /app/src/main/java/de/danoeh/antennapod/view | |
parent | 7b5435082042dc77de6e3fb5f59bf55fc71d3aa8 (diff) | |
parent | 657d19ccc2c1e3de6555c5c220605bb59225e450 (diff) | |
download | AntennaPod-b3ea96e7b3d3409a02da64f9e93511cc3400709a.zip |
Merge branch 'develop' into speed-indicator-view
Diffstat (limited to 'app/src/main/java/de/danoeh/antennapod/view')
10 files changed, 758 insertions, 71 deletions
diff --git a/app/src/main/java/de/danoeh/antennapod/view/CircularProgressBar.java b/app/src/main/java/de/danoeh/antennapod/view/CircularProgressBar.java new file mode 100644 index 000000000..4b3c51cfc --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/CircularProgressBar.java @@ -0,0 +1,87 @@ +package de.danoeh.antennapod.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; +import androidx.annotation.Nullable; + +public class CircularProgressBar extends View { + private static final float EPSILON = 0.005f; + + private final Paint paintBackground = new Paint(); + private final Paint paintProgress = new Paint(); + private float percentage = 0; + private float targetPercentage = 0; + private Object tag = null; + + public CircularProgressBar(Context context) { + super(context); + setup(); + } + + public CircularProgressBar(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + setup(); + } + + public CircularProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setup(); + } + + private void setup() { + paintBackground.setAntiAlias(true); + paintBackground.setStyle(Paint.Style.STROKE); + + paintProgress.setAntiAlias(true); + paintProgress.setStyle(Paint.Style.STROKE); + paintProgress.setStrokeCap(Paint.Cap.ROUND); + + int[] colorAttrs = new int[] { android.R.attr.textColorPrimary, android.R.attr.textColorSecondary }; + TypedArray a = getContext().obtainStyledAttributes(colorAttrs); + paintProgress.setColor(a.getColor(0, 0xffffffff)); + paintBackground.setColor(a.getColor(1, 0xffffffff)); + a.recycle(); + } + + /** + * Sets the percentage to be displayed. + * @param percentage Number from 0 to 1 + * @param tag When the tag is the same as last time calling setPercentage, the update is animated + */ + public void setPercentage(float percentage, Object tag) { + targetPercentage = percentage; + + if (tag == null || !tag.equals(this.tag)) { + // Do not animate + this.percentage = percentage; + this.tag = tag; + } + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + float padding = getHeight() * 0.06f; + paintBackground.setStrokeWidth(getHeight() * 0.02f); + paintProgress.setStrokeWidth(padding); + RectF bounds = new RectF(padding, padding, getWidth() - padding, getHeight() - padding); + canvas.drawArc(bounds, 0, 360, false, paintBackground); + + if (percentage > EPSILON && 1 - percentage > EPSILON) { + canvas.drawArc(bounds, -90, percentage * 360, false, paintProgress); + } + + if (Math.abs(percentage - targetPercentage) > EPSILON) { + float delta = Math.min(0.02f, Math.abs(targetPercentage - percentage)); + percentage += delta * ((targetPercentage - percentage) > 0 ? 1f : -1f); + invalidate(); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/PagerIndicatorView.java b/app/src/main/java/de/danoeh/antennapod/view/PagerIndicatorView.java new file mode 100644 index 000000000..60ef820a9 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/PagerIndicatorView.java @@ -0,0 +1,105 @@ +package de.danoeh.antennapod.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; +import androidx.annotation.Nullable; +import androidx.vectordrawable.graphics.drawable.ArgbEvaluator; +import androidx.viewpager.widget.ViewPager; + +public class PagerIndicatorView extends View { + private final Paint paint = new Paint(); + private float position = 0; + private int numPages = 0; + private int disabledPage = -1; + private int circleColor = 0; + private int circleColorHighlight = -1; + + public PagerIndicatorView(Context context) { + super(context); + setup(); + } + + public PagerIndicatorView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + setup(); + } + + public PagerIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setup(); + } + + private void setup() { + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.FILL); + + int[] colorAttrs = new int[] { android.R.attr.textColorSecondary }; + TypedArray a = getContext().obtainStyledAttributes(colorAttrs); + circleColorHighlight = a.getColor(0, 0xffffffff); + circleColor = (Integer) new ArgbEvaluator().evaluate(0.8f, 0x00ffffff, circleColorHighlight); + a.recycle(); + } + + public void setViewPager(ViewPager pager) { + numPages = pager.getAdapter().getCount(); + pager.getAdapter().registerDataSetObserver(new DataSetObserver() { + @Override + public void onChanged() { + numPages = pager.getAdapter().getCount(); + invalidate(); + } + }); + pager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + PagerIndicatorView.this.position = position + positionOffset; + invalidate(); + } + }); + } + + public void setDisabledPage(int disabledPage) { + this.disabledPage = disabledPage; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + for (int i = 0; i < numPages; i++) { + if ((int) Math.floor(position) == i) { + // This is the current dot + drawCircle(canvas, i, (float) (1 - (position - Math.floor(position)))); + } else if ((int) Math.ceil(position) == i) { + // This is the next dot + drawCircle(canvas, i, (float) (position - Math.floor(position))); + } else { + drawCircle(canvas, i, 0); + } + } + } + + private void drawCircle(Canvas canvas, int position, float frac) { + float circleRadiusSmall = canvas.getHeight() * 0.26f; + float circleRadiusBig = canvas.getHeight() * 0.35f; + float circleRadiusDelta = (circleRadiusBig - circleRadiusSmall); + float start = 0.5f * (canvas.getWidth() - numPages * 1.5f * canvas.getHeight()); + paint.setStrokeWidth(canvas.getHeight() * 0.3f); + + if (position == disabledPage) { + paint.setStyle(Paint.Style.STROKE); + } else { + paint.setStyle(Paint.Style.FILL_AND_STROKE); + } + + paint.setColor((Integer) new ArgbEvaluator().evaluate(frac, circleColor, circleColorHighlight)); + canvas.drawCircle(start + (position * 1.5f + 0.75f) * canvas.getHeight(), 0.5f * canvas.getHeight(), + circleRadiusSmall + frac * circleRadiusDelta, paint); + } +}
\ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/view/PieChartView.java b/app/src/main/java/de/danoeh/antennapod/view/PieChartView.java index d1b2abf23..ab4920119 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/PieChartView.java +++ b/app/src/main/java/de/danoeh/antennapod/view/PieChartView.java @@ -39,14 +39,10 @@ public class PieChartView extends AppCompatImageView { } /** - * Set array od names, array of values and array of colors. + * Set of data values to display. */ - public void setData(float[] dataValues) { - drawable.dataValues = dataValues; - drawable.valueSum = 0; - for (float datum : dataValues) { - drawable.valueSum += datum; - } + public void setData(PieChartData data) { + drawable.data = data; } @Override @@ -56,15 +52,49 @@ public class PieChartView extends AppCompatImageView { 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; + public static class PieChartData { 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 float valueSum; + private final float[] values; + + public PieChartData(float[] values) { + this.values = values; + float valueSum = 0; + for (float datum : values) { + valueSum += datum; + } + this.valueSum = valueSum; + } + + public float getSum() { + return valueSum; + } + + public float getPercentageOfItem(int index) { + if (valueSum == 0) { + return 0; + } + return values[index] / valueSum; + } + + public boolean isLargeEnoughToDisplay(int index) { + return getPercentageOfItem(index) > 0.04; + } + + public int getColorOfItem(int index) { + if (!isLargeEnoughToDisplay(index)) { + return Color.GRAY; + } + return COLOR_VALUES[index % COLOR_VALUES.length]; + } + } + + private static class PieChartDrawable extends Drawable { + private static final float PADDING_DEGREES = 3f; + private PieChartData data; private final Paint paint; private PieChartDrawable() { @@ -73,27 +103,25 @@ public class PieChartView extends AppCompatImageView { 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; + final float strokeSize = getBounds().height() / 30f; + paint.setStrokeWidth(strokeSize); + + float radius = getBounds().height() - strokeSize; float center = getBounds().width() / 2.f; - RectF arcBounds = new RectF(center - radius, STROKE_SIZE, center + radius, STROKE_SIZE + radius * 2); + RectF arcBounds = new RectF(center - radius, strokeSize, center + radius, strokeSize + radius * 2); float startAngle = 180; - for (int i = 0; i < dataValues.length; i++) { - float datum = dataValues[i]; - float sweepAngle = (180f - PADDING_DEGREES) * (datum / valueSum); - if (sweepAngle < MIN_DEGREES) { + for (int i = 0; i < data.values.length; i++) { + if (!data.isLargeEnoughToDisplay(i)) { break; } - paint.setColor(COLOR_VALUES[i % COLOR_VALUES.length]); + paint.setColor(data.getColorOfItem(i)); float padding = i == 0 ? PADDING_DEGREES / 2 : PADDING_DEGREES; + float sweepAngle = (180f - PADDING_DEGREES) * data.getPercentageOfItem(i); canvas.drawArc(arcBounds, startAngle + padding, sweepAngle - padding, false, paint); startAngle = startAngle + sweepAngle; } diff --git a/app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java b/app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java new file mode 100644 index 000000000..3ea57eb5e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/ShownotesWebView.java @@ -0,0 +1,167 @@ +package de.danoeh.antennapod.view; + +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.core.content.ContextCompat; +import com.google.android.material.snackbar.Snackbar; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.Consumer; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.ShareUtils; +import de.danoeh.antennapod.core.util.playback.Timeline; + +public class ShownotesWebView extends WebView implements View.OnLongClickListener { + private static final String TAG = "ShownotesWebView"; + + /** + * URL that was selected via long-press. + */ + private String selectedUrl; + private Consumer<Integer> timecodeSelectedListener; + private Runnable pageFinishedListener; + + public ShownotesWebView(Context context) { + super(context); + setup(); + } + + public ShownotesWebView(Context context, AttributeSet attrs) { + super(context, attrs); + setup(); + } + + public ShownotesWebView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setup(); + } + + private void setup() { + setBackgroundColor(Color.TRANSPARENT); + if (!NetworkUtils.networkAvailable()) { + getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); + // Use cached resources, even if they have expired + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + } + getSettings().setUseWideViewPort(false); + getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS); + getSettings().setLoadWithOverviewMode(true); + setOnLongClickListener(this); + + setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (Timeline.isTimecodeLink(url) && timecodeSelectedListener != null) { + timecodeSelectedListener.accept(Timeline.getTimecodeLinkTime(selectedUrl)); + } else { + IntentUtils.openInBrowser(getContext(), url); + } + return true; + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + Log.d(TAG, "Page finished"); + if (pageFinishedListener != null) { + pageFinishedListener.run(); + } + } + }); + } + + @Override + public boolean onLongClick(View v) { + WebView.HitTestResult r = getHitTestResult(); + if (r != null && r.getType() == WebView.HitTestResult.SRC_ANCHOR_TYPE) { + Log.d(TAG, "Link of webview was long-pressed. Extra: " + r.getExtra()); + selectedUrl = r.getExtra(); + showContextMenu(); + return true; + } + selectedUrl = null; + return false; + } + + public boolean onContextItemSelected(MenuItem item) { + if (selectedUrl == null) { + return false; + } + + switch (item.getItemId()) { + case R.id.open_in_browser_item: + IntentUtils.openInBrowser(getContext(), selectedUrl); + break; + case R.id.share_url_item: + ShareUtils.shareLink(getContext(), selectedUrl); + break; + case R.id.copy_url_item: + ClipData clipData = ClipData.newPlainText(selectedUrl, selectedUrl); + android.content.ClipboardManager cm = (android.content.ClipboardManager) getContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(clipData); + Snackbar.make(this, R.string.copied_url_msg, Snackbar.LENGTH_LONG).show(); + break; + case R.id.go_to_position_item: + if (Timeline.isTimecodeLink(selectedUrl) && timecodeSelectedListener != null) { + timecodeSelectedListener.accept(Timeline.getTimecodeLinkTime(selectedUrl)); + } else { + Log.e(TAG, "Selected go_to_position_item, but URL was no timecode link: " + selectedUrl); + } + break; + default: + selectedUrl = null; + return false; + + } + selectedUrl = null; + return true; + } + + @Override + protected void onCreateContextMenu(ContextMenu menu) { + super.onCreateContextMenu(menu); + if (selectedUrl == null) { + return; + } + + if (Timeline.isTimecodeLink(selectedUrl)) { + menu.add(Menu.NONE, R.id.go_to_position_item, Menu.NONE, R.string.go_to_position_label); + menu.setHeaderTitle(Converter.getDurationStringLong(Timeline.getTimecodeLinkTime(selectedUrl))); + } else { + Uri uri = Uri.parse(selectedUrl); + final Intent intent = new Intent(Intent.ACTION_VIEW, uri); + if (IntentUtils.isCallable(getContext(), intent)) { + menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE, R.string.open_in_browser_label); + } + menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE, R.string.copy_url_label); + menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE, R.string.share_url_label); + menu.setHeaderTitle(selectedUrl); + } + } + + public void setTimecodeSelectedListener(Consumer<Integer> timecodeSelectedListener) { + this.timecodeSelectedListener = timecodeSelectedListener; + } + + public void setPageFinishedListener(Runnable pageFinishedListener) { + this.pageFinishedListener = pageFinishedListener; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/SquareImageView.java b/app/src/main/java/de/danoeh/antennapod/view/SquareImageView.java index f82309c4a..dcf1edbe7 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/SquareImageView.java +++ b/app/src/main/java/de/danoeh/antennapod/view/SquareImageView.java @@ -1,13 +1,16 @@ package de.danoeh.antennapod.view; import android.content.Context; +import android.content.res.TypedArray; import androidx.appcompat.widget.AppCompatImageView; import android.util.AttributeSet; +import de.danoeh.antennapod.core.R; /** * From http://stackoverflow.com/a/19449488/6839 */ public class SquareImageView extends AppCompatImageView { + private boolean useMinimum = false; public SquareImageView(Context context) { super(context); @@ -15,19 +18,29 @@ public class SquareImageView extends AppCompatImageView { public SquareImageView(Context context, AttributeSet attrs) { super(context, attrs); + loadAttrs(context, attrs); } public SquareImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + loadAttrs(context, attrs); + } + + private void loadAttrs(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, new int[]{R.styleable.SquareImageView_useMinimum}); + useMinimum = a.getBoolean(0, false); + a.recycle(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int width = getMeasuredWidth(); - //noinspection SuspiciousNameCombination - setMeasuredDimension(width, width); + int size = getMeasuredWidth(); + if (useMinimum) { + size = Math.min(getMeasuredWidth(), getMeasuredHeight()); + } + setMeasuredDimension(size, size); } }
\ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/view/SwipeGestureDetector.java b/app/src/main/java/de/danoeh/antennapod/view/SwipeGestureDetector.java deleted file mode 100644 index f4ee092df..000000000 --- a/app/src/main/java/de/danoeh/antennapod/view/SwipeGestureDetector.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.danoeh.antennapod.view; - -import android.util.Log; -import android.view.GestureDetector; -import android.view.MotionEvent; - -public class SwipeGestureDetector extends GestureDetector.SimpleOnGestureListener { - - private static final String TAG = "SwipeGestureDetector"; - - private static final int SWIPE_MIN_DISTANCE = 120; - private static final int SWIPE_MAX_OFF_PATH = 250; - private static final int SWIPE_THRESHOLD_VELOCITY = 200; - - private final OnSwipeGesture callback; - - public SwipeGestureDetector(OnSwipeGesture callback) { - this.callback = callback; - } - - @Override - public boolean onDown(MotionEvent e) { - return true; - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - try { - if (Math.abs(e1.getY() - e2.getY()) > SWIPE_MAX_OFF_PATH) - return false; - if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE - && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { - return callback.onSwipeRightToLeft(); - } else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE - && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { - return callback.onSwipeLeftToRight(); - } - } catch (Exception e) { - Log.d(TAG, Log.getStackTraceString(e)); - } - return false; - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java new file mode 100644 index 000000000..d48db196f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/DownloadItemViewHolder.java @@ -0,0 +1,41 @@ +package de.danoeh.antennapod.view.viewholder; + +import android.content.Context; +import android.os.Build; +import android.text.Layout; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import com.joanzapata.iconify.widget.IconTextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.view.CircularProgressBar; + +public class DownloadItemViewHolder extends RecyclerView.ViewHolder { + public final View secondaryActionButton; + public final ImageView secondaryActionIcon; + public final CircularProgressBar secondaryActionProgress; + public final IconTextView icon; + public final TextView title; + public final TextView type; + public final TextView date; + public final TextView reason; + + public DownloadItemViewHolder(Context context, ViewGroup parent) { + super(LayoutInflater.from(context).inflate(R.layout.downloadlog_item, parent, false)); + date = itemView.findViewById(R.id.txtvDate); + type = itemView.findViewById(R.id.txtvType); + icon = itemView.findViewById(R.id.txtvIcon); + reason = itemView.findViewById(R.id.txtvReason); + secondaryActionProgress = itemView.findViewById(R.id.secondaryActionProgress); + secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); + secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); + title = itemView.findViewById(R.id.txtvTitle); + if (Build.VERSION.SDK_INT >= 23) { + title.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); + } + itemView.setTag(this); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java new file mode 100644 index 000000000..369574190 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java @@ -0,0 +1,213 @@ +package de.danoeh.antennapod.view.viewholder; + +import android.graphics.Color; +import android.os.Build; +import android.text.Layout; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; +import com.joanzapata.iconify.Iconify; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.CoverLoader; +import de.danoeh.antennapod.adapter.QueueRecyclerAdapter; +import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.DateUtils; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.ThemeUtils; +import de.danoeh.antennapod.view.CircularProgressBar; + +/** + * Holds the view which shows FeedItems. + */ +public class EpisodeItemViewHolder extends FeedComponentViewHolder + implements QueueRecyclerAdapter.ItemTouchHelperViewHolder { + private static final String TAG = "EpisodeItemViewHolder"; + + private final View container; + public final ImageView dragHandle; + private final TextView placeholder; + private final ImageView cover; + private final TextView title; + private final TextView pubDate; + private final TextView position; + private final TextView duration; + private final TextView size; + public final TextView isNew; + public final ImageView isInQueue; + private final ImageView isVideo; + public final ImageView isFavorite; + private final ProgressBar progressBar; + public final View secondaryActionButton; + public final ImageView secondaryActionIcon; + private final CircularProgressBar secondaryActionProgress; + private final TextView separatorIcons; + public final CardView coverHolder; + + private final MainActivity activity; + private FeedItem item; + + public EpisodeItemViewHolder(MainActivity activity, ViewGroup parent) { + super(LayoutInflater.from(activity).inflate(R.layout.feeditemlist_item, parent, false)); + this.activity = activity; + container = itemView.findViewById(R.id.container); + dragHandle = itemView.findViewById(R.id.drag_handle); + placeholder = itemView.findViewById(R.id.txtvPlaceholder); + cover = itemView.findViewById(R.id.imgvCover); + title = itemView.findViewById(R.id.txtvTitle); + if (Build.VERSION.SDK_INT >= 23) { + title.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); + } + pubDate = itemView.findViewById(R.id.txtvPubDate); + position = itemView.findViewById(R.id.txtvPosition); + duration = itemView.findViewById(R.id.txtvDuration); + progressBar = itemView.findViewById(R.id.progressBar); + isInQueue = itemView.findViewById(R.id.ivInPlaylist); + isVideo = itemView.findViewById(R.id.ivIsVideo); + isNew = itemView.findViewById(R.id.statusUnread); + isFavorite = itemView.findViewById(R.id.isFavorite); + size = itemView.findViewById(R.id.size); + separatorIcons = itemView.findViewById(R.id.separatorIcons); + secondaryActionProgress = itemView.findViewById(R.id.secondaryActionProgress); + secondaryActionButton = itemView.findViewById(R.id.secondaryActionButton); + secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); + coverHolder = itemView.findViewById(R.id.coverHolder); + itemView.setTag(this); + } + + @Override + public void onItemSelected() { + itemView.setAlpha(0.5f); + } + + @Override + public void onItemClear() { + itemView.setAlpha(1.0f); + } + + public void bind(FeedItem item) { + this.item = item; + placeholder.setText(item.getFeed().getTitle()); + title.setText(item.getTitle()); + pubDate.setText(DateUtils.formatAbbrev(activity, item.getPubDate())); + isNew.setVisibility(item.isNew() ? View.VISIBLE : View.GONE); + isFavorite.setVisibility(item.isTagged(FeedItem.TAG_FAVORITE) ? View.VISIBLE : View.GONE); + isInQueue.setVisibility(item.isTagged(FeedItem.TAG_QUEUE) ? View.VISIBLE : View.GONE); + itemView.setAlpha(item.isPlayed() ? 0.5f : 1.0f); + + ItemActionButton actionButton = ItemActionButton.forItem(item, true, true); + actionButton.configure(secondaryActionButton, secondaryActionIcon, activity); + secondaryActionButton.setFocusable(false); + + if (item.getMedia() != null) { + bind(item.getMedia()); + } else { + secondaryActionProgress.setPercentage(0, item); + } + + if (coverHolder.getVisibility() == View.VISIBLE) { + new CoverLoader(activity) + .withUri(ImageResourceUtils.getImageLocation(item)) + .withFallbackUri(item.getFeed().getImageLocation()) + .withPlaceholderView(placeholder) + .withCoverView(cover) + .load(); + } + } + + private void bind(FeedMedia media) { + isVideo.setVisibility(media.getMediaType() == MediaType.VIDEO ? View.VISIBLE : View.GONE); + duration.setText(Converter.getDurationStringLong(media.getDuration())); + + if (media.isCurrentlyPlaying()) { + container.setBackgroundColor(ThemeUtils.getColorFromAttr(activity, R.attr.currently_playing_background)); + } else { + container.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground)); + } + + if (DownloadRequester.getInstance().isDownloadingFile(media)) { + final DownloadRequest downloadRequest = DownloadRequester.getInstance().getRequestFor(media); + float percent = 0.01f * downloadRequest.getProgressPercent(); + secondaryActionProgress.setPercentage(Math.max(percent, 0.01f), item); + } else if (media.isDownloaded()) { + secondaryActionProgress.setPercentage(1, item); // Do not animate 100% -> 0% + } else { + secondaryActionProgress.setPercentage(0, item); // Animate X% -> 0% + } + + if (media.getDuration() > 0 + && (item.getState() == FeedItem.State.PLAYING || item.getState() == FeedItem.State.IN_PROGRESS)) { + int progress = (int) (100.0 * media.getPosition() / media.getDuration()); + progressBar.setProgress(progress); + position.setText(Converter.getDurationStringLong(media.getPosition())); + duration.setText(Converter.getDurationStringLong(media.getDuration())); + progressBar.setVisibility(View.VISIBLE); + position.setVisibility(View.VISIBLE); + } else { + progressBar.setVisibility(View.GONE); + position.setVisibility(View.GONE); + } + + if (media.getSize() > 0) { + size.setText(Converter.byteToString(media.getSize())); + } else if (NetworkUtils.isEpisodeHeadDownloadAllowed() && !media.checkedOnSizeButUnknown()) { + size.setText("{fa-spinner}"); + Iconify.addIcons(size); + NetworkUtils.getFeedMediaSizeObservable(media).subscribe( + sizeValue -> { + if (sizeValue > 0) { + size.setText(Converter.byteToString(sizeValue)); + } else { + size.setText(""); + } + }, error -> { + size.setText(""); + Log.e(TAG, Log.getStackTraceString(error)); + }); + } else { + size.setText(""); + } + } + + public FeedItem getFeedItem() { + return item; + } + + public boolean isCurrentlyPlayingItem() { + return item.getMedia() != null && item.getMedia().isCurrentlyPlaying(); + } + + public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { + progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); + position.setText(Converter.getDurationStringLong(event.getPosition())); + duration.setText(Converter.getDurationStringLong(event.getDuration())); + } + + /** + * Hides the separator dot between icons and text if there are no icons. + */ + public void hideSeparatorIfNecessary() { + boolean hasIcons = isNew.getVisibility() == View.VISIBLE + || isInQueue.getVisibility() == View.VISIBLE + || isVideo.getVisibility() == View.VISIBLE + || isFavorite.getVisibility() == View.VISIBLE + || isNew.getVisibility() == View.VISIBLE; + separatorIcons.setVisibility(hasIcons ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/FeedComponentViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/FeedComponentViewHolder.java new file mode 100644 index 000000000..f55ea9bc8 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/FeedComponentViewHolder.java @@ -0,0 +1,15 @@ +package de.danoeh.antennapod.view.viewholder; + +import android.view.View; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Holds the view which shows FeedComponents. + */ +public class FeedComponentViewHolder extends RecyclerView.ViewHolder { + + public FeedComponentViewHolder(@NonNull View itemView) { + super(itemView); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/FeedViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/FeedViewHolder.java new file mode 100644 index 000000000..83250bbfa --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/FeedViewHolder.java @@ -0,0 +1,62 @@ +package de.danoeh.antennapod.view.viewholder; + +import android.os.Build; +import android.text.Layout; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.cardview.widget.CardView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.CoverLoader; +import de.danoeh.antennapod.core.feed.Feed; + +/** + * Holds the view which shows feeds. + */ +public class FeedViewHolder extends FeedComponentViewHolder { + private static final String TAG = "FeedViewHolder"; + + private final TextView placeholder; + private final ImageView cover; + private final TextView title; + public final CardView coverHolder; + + private final MainActivity activity; + private Feed feed; + + public FeedViewHolder(MainActivity activity, ViewGroup parent) { + super(LayoutInflater.from(activity).inflate(R.layout.feeditemlist_item, parent, false)); + this.activity = activity; + placeholder = itemView.findViewById(R.id.txtvPlaceholder); + cover = itemView.findViewById(R.id.imgvCover); + coverHolder = itemView.findViewById(R.id.coverHolder); + title = itemView.findViewById(R.id.txtvTitle); + if (Build.VERSION.SDK_INT >= 23) { + title.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL); + } + + itemView.findViewById(R.id.secondaryActionButton).setVisibility(View.GONE); + itemView.findViewById(R.id.status).setVisibility(View.GONE); + itemView.findViewById(R.id.progress).setVisibility(View.GONE); + itemView.findViewById(R.id.drag_handle).setVisibility(View.GONE); + itemView.setTag(this); + } + + public void bind(Feed feed) { + this.feed = feed; + placeholder.setText(feed.getTitle()); + title.setText(feed.getTitle()); + + if (coverHolder.getVisibility() == View.VISIBLE) { + new CoverLoader(activity) + .withUri(feed.getImageLocation()) + .withPlaceholderView(placeholder) + .withCoverView(cover) + .load(); + } + } + +} |