summaryrefslogtreecommitdiff
path: root/ui/common
diff options
context:
space:
mode:
authorByteHamster <info@bytehamster.com>2021-02-12 21:00:39 +0100
committerByteHamster <info@bytehamster.com>2021-02-12 21:00:39 +0100
commit010ed376cd4b8935736dec6a3be052f93ed18b20 (patch)
tree3c34bd6af92ba87a794e2f4eb1e6a5b15a8a8ee2 /ui/common
parent87b149b7647d61f52a57f67a2519e248bf1e7880 (diff)
downloadAntennaPod-010ed376cd4b8935736dec6a3be052f93ed18b20.zip
Move basic views to new module
Diffstat (limited to 'ui/common')
-rw-r--r--ui/common/README.md3
-rw-r--r--ui/common/build.gradle50
-rw-r--r--ui/common/src/main/AndroidManifest.xml1
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java95
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/PlaybackSpeedIndicatorView.java113
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java67
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/SquareImageView.java61
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java25
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/WrappingGridView.java35
-rw-r--r--ui/common/src/main/res/values/styleable.xml18
10 files changed, 468 insertions, 0 deletions
diff --git a/ui/common/README.md b/ui/common/README.md
new file mode 100644
index 000000000..d96f1cf55
--- /dev/null
+++ b/ui/common/README.md
@@ -0,0 +1,3 @@
+# :ui:common
+
+This module provides basic UI functionality that is needed in multiple modules. UI elements that are only used in a single module should not be defined here.
diff --git a/ui/common/build.gradle b/ui/common/build.gradle
new file mode 100644
index 000000000..fabd8937f
--- /dev/null
+++ b/ui/common/build.gradle
@@ -0,0 +1,50 @@
+apply plugin: "com.android.library"
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+
+ multiDexEnabled false
+
+ testApplicationId "de.danoeh.antennapod.core.tests"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile("proguard-android.txt")
+ }
+ debug {
+ // debug build has method count over 64k single-dex threshold.
+ // For building debug build to use on Android < 21 (pre-Android 5) devices,
+ // you need to manually change class
+ // de.danoeh.antennapod.PodcastApp to extend MultiDexApplication .
+ // See Issue #2813
+ multiDexEnabled true
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+
+ lintOptions {
+ warningsAsErrors true
+ abortOnError true
+ }
+}
+
+dependencies {
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+}
diff --git a/ui/common/src/main/AndroidManifest.xml b/ui/common/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..bae316f55
--- /dev/null
+++ b/ui/common/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.ui.common" />
diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java
new file mode 100644
index 000000000..a693c28b0
--- /dev/null
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java
@@ -0,0 +1,95 @@
+package de.danoeh.antennapod.ui.common;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+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 {
+ public static final float MINIMUM_PERCENTAGE = 0.005f;
+ public static final float MAXIMUM_PERCENTAGE = 1 - MINIMUM_PERCENTAGE;
+
+ private final Paint paintBackground = new Paint();
+ private final Paint paintProgress = new Paint();
+ private float percentage = 0;
+ private float targetPercentage = 0;
+ private Object tag = null;
+ private final RectF bounds = new RectF();
+
+ public CircularProgressBar(Context context) {
+ super(context);
+ setup(null);
+ }
+
+ public CircularProgressBar(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ setup(attrs);
+ }
+
+ public CircularProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setup(attrs);
+ }
+
+ private void setup(@Nullable AttributeSet attrs) {
+ paintBackground.setAntiAlias(true);
+ paintBackground.setStyle(Paint.Style.STROKE);
+
+ paintProgress.setAntiAlias(true);
+ paintProgress.setStyle(Paint.Style.STROKE);
+ paintProgress.setStrokeCap(Paint.Cap.ROUND);
+
+ TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircularProgressBar);
+ int color = typedArray.getColor(R.styleable.CircularProgressBar_foregroundColor, Color.GREEN);
+ typedArray.recycle();
+ paintProgress.setColor(color);
+ paintBackground.setColor(color);
+ }
+
+ /**
+ * 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.07f;
+ paintBackground.setStrokeWidth(getHeight() * 0.02f);
+ paintProgress.setStrokeWidth(padding);
+ bounds.set(padding, padding, getWidth() - padding, getHeight() - padding);
+ canvas.drawArc(bounds, 0, 360, false, paintBackground);
+
+ if (MINIMUM_PERCENTAGE <= percentage && percentage <= MAXIMUM_PERCENTAGE) {
+ canvas.drawArc(bounds, -90, percentage * 360, false, paintProgress);
+ }
+
+ if (Math.abs(percentage - targetPercentage) > MINIMUM_PERCENTAGE) {
+ float speed = 0.02f;
+ if (Math.abs(targetPercentage - percentage) < 0.1 && targetPercentage > percentage) {
+ speed = 0.006f;
+ }
+ float delta = Math.min(speed, Math.abs(targetPercentage - percentage));
+ float direction = ((targetPercentage - percentage) > 0 ? 1f : -1f);
+ percentage += delta * direction;
+ invalidate();
+ }
+ }
+}
diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PlaybackSpeedIndicatorView.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PlaybackSpeedIndicatorView.java
new file mode 100644
index 000000000..c93ca01f5
--- /dev/null
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/PlaybackSpeedIndicatorView.java
@@ -0,0 +1,113 @@
+package de.danoeh.antennapod.ui.common;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+import androidx.annotation.Nullable;
+
+public class PlaybackSpeedIndicatorView extends View {
+ private static final float DEG_2_RAD = (float) (Math.PI / 180);
+ private static final float PADDING_ANGLE = 30;
+ private static final float VALUE_UNSET = -4242;
+
+ private final Paint arcPaint = new Paint();
+ private final Paint indicatorPaint = new Paint();
+ private final Path trianglePath = new Path();
+ private final RectF arcBounds = new RectF();
+ private float angle = VALUE_UNSET;
+ private float targetAngle = VALUE_UNSET;
+ private float degreePerFrame = 2;
+ private float paddingArc = 20;
+ private float paddingIndicator = 10;
+
+ public PlaybackSpeedIndicatorView(Context context) {
+ super(context);
+ setup(null);
+ }
+
+ public PlaybackSpeedIndicatorView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ setup(attrs);
+ }
+
+ public PlaybackSpeedIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setup(attrs);
+ }
+
+ private void setup(@Nullable AttributeSet attrs) {
+ setSpeed(1.0f); // Set default angle to 1.0
+ targetAngle = VALUE_UNSET; // Do not move to first value that is set externally
+
+ TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.PlaybackSpeedIndicatorView);
+ int color = typedArray.getColor(R.styleable.PlaybackSpeedIndicatorView_foregroundColor, Color.GREEN);
+ typedArray.recycle();
+ arcPaint.setColor(color);
+ indicatorPaint.setColor(color);
+
+ arcPaint.setAntiAlias(true);
+ arcPaint.setStyle(Paint.Style.STROKE);
+ arcPaint.setStrokeCap(Paint.Cap.ROUND);
+
+ indicatorPaint.setAntiAlias(true);
+ indicatorPaint.setStyle(Paint.Style.FILL);
+
+ trianglePath.setFillType(Path.FillType.EVEN_ODD);
+ }
+
+ public void setSpeed(float value) {
+ float maxAnglePerDirection = 90 + 45 - 2 * paddingArc;
+ // Speed values above 3 are probably not too common. Cap at 3 for better differentiation
+ float normalizedValue = Math.min(2.5f, value - 0.5f) / 2.5f; // Linear between 0 and 1
+ float target = -maxAnglePerDirection + 2 * maxAnglePerDirection * normalizedValue;
+ if (targetAngle == VALUE_UNSET) {
+ angle = target;
+ }
+ targetAngle = target;
+ degreePerFrame = Math.abs(targetAngle - angle) / 20;
+ invalidate();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ paddingArc = getMeasuredHeight() / 4.5f;
+ paddingIndicator = getMeasuredHeight() / 6f;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ float radiusInnerCircle = getWidth() / 10f;
+ canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, radiusInnerCircle, indicatorPaint);
+
+ trianglePath.rewind();
+ float bigRadius = getHeight() / 2f - paddingIndicator;
+ trianglePath.moveTo(getWidth() / 2f + (float) (bigRadius * Math.sin((-angle + 180) * DEG_2_RAD)),
+ getHeight() / 2f + (float) (bigRadius * Math.cos((-angle + 180) * DEG_2_RAD)));
+ trianglePath.lineTo(getWidth() / 2f + (float) (radiusInnerCircle * Math.sin((-angle + 180 - 90) * DEG_2_RAD)),
+ getHeight() / 2f + (float) (radiusInnerCircle * Math.cos((-angle + 180 - 90) * DEG_2_RAD)));
+ trianglePath.lineTo(getWidth() / 2f + (float) (radiusInnerCircle * Math.sin((-angle + 180 + 90) * DEG_2_RAD)),
+ getHeight() / 2f + (float) (radiusInnerCircle * Math.cos((-angle + 180 + 90) * DEG_2_RAD)));
+ trianglePath.close();
+ canvas.drawPath(trianglePath, indicatorPaint);
+
+ arcPaint.setStrokeWidth(getHeight() / 15f);
+ arcBounds.set(paddingArc, paddingArc, getWidth() - paddingArc, getHeight() - paddingArc);
+ canvas.drawArc(arcBounds, -180 - 45, 90 + 45 + angle - PADDING_ANGLE, false, arcPaint);
+ canvas.drawArc(arcBounds, -90 + PADDING_ANGLE + angle, 90 + 45 - PADDING_ANGLE - angle, false, arcPaint);
+
+ if (Math.abs(angle - targetAngle) > 0.5 && targetAngle != VALUE_UNSET) {
+ angle += Math.signum(targetAngle - angle) * Math.min(degreePerFrame, Math.abs(targetAngle - angle));
+ invalidate();
+ }
+ }
+}
diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java
new file mode 100644
index 000000000..94ef73ffc
--- /dev/null
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java
@@ -0,0 +1,67 @@
+package de.danoeh.antennapod.ui.common;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import java.util.ArrayList;
+
+/**
+ * An alternative to {@link android.widget.RadioGroup} that allows to nest children.
+ * Basend on https://stackoverflow.com/a/14309274.
+ */
+public class RecursiveRadioGroup extends LinearLayout {
+ private final ArrayList<RadioButton> radioButtons = new ArrayList<>();
+ private RadioButton checkedButton = null;
+
+ public RecursiveRadioGroup(Context context) {
+ super(context);
+ }
+
+ public RecursiveRadioGroup(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RecursiveRadioGroup(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+ parseChild(child);
+ }
+
+ public void parseChild(final View child) {
+ if (child instanceof RadioButton) {
+ RadioButton button = (RadioButton) child;
+ radioButtons.add(button);
+ button.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (!isChecked) {
+ return;
+ }
+ checkedButton = (RadioButton) buttonView;
+
+ for (RadioButton view : radioButtons) {
+ if (view != buttonView) {
+ view.setChecked(false);
+ }
+ }
+ });
+ } else if (child instanceof ViewGroup) {
+ parseChildren((ViewGroup) child);
+ }
+ }
+
+ public void parseChildren(final ViewGroup child) {
+ for (int i = 0; i < child.getChildCount(); i++) {
+ parseChild(child.getChildAt(i));
+ }
+ }
+
+ public RadioButton getCheckedButton() {
+ return checkedButton;
+ }
+}
diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/SquareImageView.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/SquareImageView.java
new file mode 100644
index 000000000..dce15af18
--- /dev/null
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/SquareImageView.java
@@ -0,0 +1,61 @@
+package de.danoeh.antennapod.ui.common;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import androidx.appcompat.widget.AppCompatImageView;
+import android.util.AttributeSet;
+
+/**
+ * From http://stackoverflow.com/a/19449488/6839
+ */
+public class SquareImageView extends AppCompatImageView {
+ public static final int DIRECTION_WIDTH = 0;
+ public static final int DIRECTION_HEIGHT = 1;
+ public static final int DIRECTION_MINIMUM = 2;
+
+ private int direction = DIRECTION_WIDTH;
+
+ public SquareImageView(Context context) {
+ super(context);
+ }
+
+ 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, R.styleable.SquareImageView);
+ direction = a.getInt(R.styleable.SquareImageView_direction, DIRECTION_WIDTH);
+ a.recycle();
+ }
+
+ public void setDirection(int direction) {
+ this.direction = direction;
+ requestLayout();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ switch (direction) {
+ case DIRECTION_MINIMUM:
+ int size = Math.min(getMeasuredWidth(), getMeasuredHeight());
+ setMeasuredDimension(size, size);
+ break;
+ case DIRECTION_HEIGHT:
+ setMeasuredDimension(getMeasuredHeight(), getMeasuredHeight());
+ break;
+ default:
+ setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
+ break;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java
new file mode 100644
index 000000000..392d09e07
--- /dev/null
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java
@@ -0,0 +1,25 @@
+package de.danoeh.antennapod.ui.common;
+
+import android.content.Context;
+import androidx.annotation.AttrRes;
+import androidx.annotation.ColorInt;
+import android.util.TypedValue;
+import androidx.annotation.DrawableRes;
+
+public class ThemeUtils {
+ private ThemeUtils() {
+
+ }
+
+ public static @ColorInt int getColorFromAttr(Context context, @AttrRes int attr) {
+ TypedValue typedValue = new TypedValue();
+ context.getTheme().resolveAttribute(attr, typedValue, true);
+ return typedValue.data;
+ }
+
+ public static @DrawableRes int getDrawableFromAttr(Context context, @AttrRes int attr) {
+ TypedValue typedValue = new TypedValue();
+ context.getTheme().resolveAttribute(attr, typedValue, true);
+ return typedValue.resourceId;
+ }
+}
diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/WrappingGridView.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/WrappingGridView.java
new file mode 100644
index 000000000..4c8bb994c
--- /dev/null
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/WrappingGridView.java
@@ -0,0 +1,35 @@
+package de.danoeh.antennapod.ui.common;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.GridView;
+
+/**
+ * Source: https://stackoverflow.com/a/46350213/
+ */
+public class WrappingGridView extends GridView {
+
+ public WrappingGridView(Context context) {
+ super(context);
+ }
+
+ public WrappingGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WrappingGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int heightSpec = heightMeasureSpec;
+ if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
+ // The great Android "hackatlon", the love, the magic.
+ // The two leftmost bits in the height measure spec have
+ // a special meaning, hence we can't use them to describe height.
+ heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
+ }
+ super.onMeasure(widthMeasureSpec, heightSpec);
+ }
+}
diff --git a/ui/common/src/main/res/values/styleable.xml b/ui/common/src/main/res/values/styleable.xml
new file mode 100644
index 000000000..3542cc1b5
--- /dev/null
+++ b/ui/common/src/main/res/values/styleable.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <declare-styleable name="SquareImageView">
+ <attr name="direction" format="enum">
+ <enum name="width" value="0"/>
+ <enum name="height" value="1"/>
+ <enum name="minimum" value="2"/>
+ </attr>
+ </declare-styleable>
+
+ <declare-styleable name="CircularProgressBar">
+ <attr name="foregroundColor" format="color" />
+ </declare-styleable>
+
+ <declare-styleable name="PlaybackSpeedIndicatorView">
+ <attr name="foregroundColor" /> <!-- format omitted to avoid double definition -->
+ </declare-styleable>
+</resources>