From 010ed376cd4b8935736dec6a3be052f93ed18b20 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Fri, 12 Feb 2021 21:00:39 +0100 Subject: Move basic views to new module --- ui/common/src/main/AndroidManifest.xml | 1 + .../antennapod/ui/common/CircularProgressBar.java | 95 +++++++++++++++++ .../ui/common/PlaybackSpeedIndicatorView.java | 113 +++++++++++++++++++++ .../antennapod/ui/common/RecursiveRadioGroup.java | 67 ++++++++++++ .../antennapod/ui/common/SquareImageView.java | 61 +++++++++++ .../de/danoeh/antennapod/ui/common/ThemeUtils.java | 25 +++++ .../antennapod/ui/common/WrappingGridView.java | 35 +++++++ ui/common/src/main/res/values/styleable.xml | 18 ++++ 8 files changed, 415 insertions(+) create mode 100644 ui/common/src/main/AndroidManifest.xml create mode 100644 ui/common/src/main/java/de/danoeh/antennapod/ui/common/CircularProgressBar.java create mode 100644 ui/common/src/main/java/de/danoeh/antennapod/ui/common/PlaybackSpeedIndicatorView.java create mode 100644 ui/common/src/main/java/de/danoeh/antennapod/ui/common/RecursiveRadioGroup.java create mode 100644 ui/common/src/main/java/de/danoeh/antennapod/ui/common/SquareImageView.java create mode 100644 ui/common/src/main/java/de/danoeh/antennapod/ui/common/ThemeUtils.java create mode 100644 ui/common/src/main/java/de/danoeh/antennapod/ui/common/WrappingGridView.java create mode 100644 ui/common/src/main/res/values/styleable.xml (limited to 'ui/common/src') 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 @@ + 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 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 @@ + + + + + + + + + + + + + + + + + + -- cgit v1.2.3