commit
e7d391b050
43 changed files with 885 additions and 702 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,5 +1,20 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 3.1.3
|
||||||
|
|
||||||
|
#### What's New
|
||||||
|
- Updated to Android 14
|
||||||
|
- Added option to re-enable old album cover cropping behavior
|
||||||
|
|
||||||
|
#### What's Improved
|
||||||
|
- `album artists` and `(album)artists sort` are now recognized
|
||||||
|
- Increased distinction from shuffle on/off icons
|
||||||
|
|
||||||
|
#### What's Fixed
|
||||||
|
- Fixed an issue where the queue sheet would not collapse when scrolling
|
||||||
|
the song list in some cases
|
||||||
|
- Fixed music loading hanging if it encountered an error in certain places
|
||||||
|
|
||||||
## 3.1.2
|
## 3.1.2
|
||||||
|
|
||||||
#### What's Improved
|
#### What's Improved
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<h1 align="center"><b>Auxio</b></h1>
|
<h1 align="center"><b>Auxio</b></h1>
|
||||||
<h4 align="center">A simple, rational music player for android.</h4>
|
<h4 align="center">A simple, rational music player for android.</h4>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.1.2">
|
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.1.3">
|
||||||
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.1.2&color=64B5F6&style=flat">
|
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.1.3&color=64B5F6&style=flat">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/oxygencobalt/Auxio/releases/">
|
<a href="https://github.com/oxygencobalt/Auxio/releases/">
|
||||||
<img alt="Releases" src="https://img.shields.io/github/downloads/OxygenCobalt/Auxio/total.svg?color=4B95DE&style=flat">
|
<img alt="Releases" src="https://img.shields.io/github/downloads/OxygenCobalt/Auxio/total.svg?color=4B95DE&style=flat">
|
||||||
|
|
|
@ -10,7 +10,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
// NDK is not used in Auxio explicitly (used in the ffmpeg extension), but we need to specify
|
// NDK is not used in Auxio explicitly (used in the ffmpeg extension), but we need to specify
|
||||||
// it here so that binary stripping will work.
|
// it here so that binary stripping will work.
|
||||||
// TODO: Eventually you might just want to start vendoring the FFMpeg extension so the
|
// TODO: Eventually you might just want to start vendoring the FFMpeg extension so the
|
||||||
|
@ -20,11 +20,11 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId namespace
|
applicationId namespace
|
||||||
versionName "3.1.2"
|
versionName "3.1.3"
|
||||||
versionCode 32
|
versionCode 33
|
||||||
|
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 33
|
targetSdk 34
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
@ -84,16 +84,19 @@ dependencies {
|
||||||
// --- SUPPORT ---
|
// --- SUPPORT ---
|
||||||
|
|
||||||
// General
|
// General
|
||||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
|
||||||
implementation "androidx.core:core-ktx:1.10.1"
|
implementation "androidx.core:core-ktx:1.10.1"
|
||||||
|
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||||
implementation "androidx.activity:activity-ktx:1.7.2"
|
implementation "androidx.activity:activity-ktx:1.7.2"
|
||||||
implementation "androidx.fragment:fragment-ktx:1.5.7"
|
implementation "androidx.fragment:fragment-ktx:1.6.0"
|
||||||
|
|
||||||
// UI
|
// Components
|
||||||
implementation "androidx.recyclerview:recyclerview:1.3.0"
|
// Deliberately kept on 1.2.1 to prevent a bug where the queue sheet will not collapse on
|
||||||
|
// certain upwards scrolling events
|
||||||
|
// TODO: Report this issue and hope for a timely fix
|
||||||
|
// noinspection GradleDependency
|
||||||
|
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||||
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
|
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||||
implementation 'androidx.core:core-ktx:1.10.1'
|
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
def lifecycle_version = "2.6.1"
|
def lifecycle_version = "2.6.1"
|
||||||
|
@ -128,11 +131,9 @@ dependencies {
|
||||||
implementation 'io.coil-kt:coil-base:2.4.0'
|
implementation 'io.coil-kt:coil-base:2.4.0'
|
||||||
|
|
||||||
// Material
|
// Material
|
||||||
// TODO: Stuck on 1.8.0-alpha01 until ripple bug with tab layout is actually available
|
|
||||||
// in a version that I can build with
|
|
||||||
// TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just
|
// TODO: Exactly figure out the conditions that the 1.7.0 ripple bug occurred so you can just
|
||||||
// PR a fix.
|
// PR a fix.
|
||||||
implementation "com.google.android.material:material:1.8.0-alpha01"
|
implementation "com.google.android.material:material:1.10.0-alpha04"
|
||||||
|
|
||||||
// Dependency Injection
|
// Dependency Injection
|
||||||
implementation "com.google.dagger:dagger:$hilt_version"
|
implementation "com.google.dagger:dagger:$hilt_version"
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<!-- Bluetooth auto-connect functionality (Disabled until permission workflow can be made) -->
|
<!-- Bluetooth auto-connect functionality (Disabled until permission workflow can be made) -->
|
||||||
|
|
|
@ -16,10 +16,14 @@
|
||||||
|
|
||||||
package com.google.android.material.bottomsheet;
|
package com.google.android.material.bottomsheet;
|
||||||
|
|
||||||
|
import com.google.android.material.R;
|
||||||
|
|
||||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
||||||
import static java.lang.Math.max;
|
import static java.lang.Math.max;
|
||||||
import static java.lang.Math.min;
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
import android.animation.ValueAnimator;
|
import android.animation.ValueAnimator;
|
||||||
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
@ -32,8 +36,10 @@ import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.SparseIntArray;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
import android.view.RoundedCorner;
|
||||||
import android.view.VelocityTracker;
|
import android.view.VelocityTracker;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.MeasureSpec;
|
import android.view.View.MeasureSpec;
|
||||||
|
@ -41,13 +47,15 @@ import android.view.ViewConfiguration;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewGroup.MarginLayoutParams;
|
import android.view.ViewGroup.MarginLayoutParams;
|
||||||
import android.view.ViewParent;
|
import android.view.ViewParent;
|
||||||
|
import android.view.WindowInsets;
|
||||||
import android.view.accessibility.AccessibilityEvent;
|
import android.view.accessibility.AccessibilityEvent;
|
||||||
|
import androidx.activity.BackEventCompat;
|
||||||
import androidx.annotation.FloatRange;
|
import androidx.annotation.FloatRange;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.Px;
|
import androidx.annotation.Px;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.annotation.RestrictTo;
|
import androidx.annotation.RestrictTo;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
@ -62,14 +70,13 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.Accessibilit
|
||||||
import androidx.core.view.accessibility.AccessibilityViewCommand;
|
import androidx.core.view.accessibility.AccessibilityViewCommand;
|
||||||
import androidx.customview.view.AbsSavedState;
|
import androidx.customview.view.AbsSavedState;
|
||||||
import androidx.customview.widget.ViewDragHelper;
|
import androidx.customview.widget.ViewDragHelper;
|
||||||
|
|
||||||
import com.google.android.material.R;
|
|
||||||
import com.google.android.material.internal.ViewUtils;
|
import com.google.android.material.internal.ViewUtils;
|
||||||
import com.google.android.material.internal.ViewUtils.RelativePadding;
|
import com.google.android.material.internal.ViewUtils.RelativePadding;
|
||||||
|
import com.google.android.material.motion.MaterialBackHandler;
|
||||||
|
import com.google.android.material.motion.MaterialBottomContainerBackHelper;
|
||||||
import com.google.android.material.resources.MaterialResources;
|
import com.google.android.material.resources.MaterialResources;
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable;
|
import com.google.android.material.shape.MaterialShapeDrawable;
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||||
|
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
@ -84,13 +91,11 @@ import java.util.Map;
|
||||||
* <p>To send useful accessibility events, set a title on bottom sheets that are windows or are
|
* <p>To send useful accessibility events, set a title on bottom sheets that are windows or are
|
||||||
* window-like. For BottomSheetDialog use {@link BottomSheetDialog#setTitle(int)}, and for
|
* window-like. For BottomSheetDialog use {@link BottomSheetDialog#setTitle(int)}, and for
|
||||||
* BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}.
|
* BottomSheetDialogFragment use {@link ViewCompat#setAccessibilityPaneTitle(View, CharSequence)}.
|
||||||
*
|
|
||||||
* Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently
|
|
||||||
* obtainable in the currently used MDC library.
|
|
||||||
*/
|
*/
|
||||||
public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
|
public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V>
|
||||||
|
implements MaterialBackHandler {
|
||||||
|
|
||||||
/** Listener for monitoring events about bottom sheets. */
|
/** Callback for monitoring events about bottom sheets. */
|
||||||
public abstract static class BottomSheetCallback {
|
public abstract static class BottomSheetCallback {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -203,11 +208,11 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
public @interface SaveFlags {}
|
public @interface SaveFlags {}
|
||||||
|
|
||||||
private static final String TAG = "BottomSheetBehavior";
|
private static final String TAG = "BackportBottomSheetBehavior";
|
||||||
|
|
||||||
@SaveFlags private int saveFlags = SAVE_NONE;
|
@SaveFlags private int saveFlags = SAVE_NONE;
|
||||||
|
|
||||||
private static final int SIGNIFICANT_VEL_THRESHOLD = 500;
|
@VisibleForTesting static final int DEFAULT_SIGNIFICANT_VEL_THRESHOLD = 500;
|
||||||
|
|
||||||
private static final float HIDE_THRESHOLD = 0.5f;
|
private static final float HIDE_THRESHOLD = 0.5f;
|
||||||
|
|
||||||
|
@ -217,12 +222,21 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
|
|
||||||
private static final int NO_MAX_SIZE = -1;
|
private static final int NO_MAX_SIZE = -1;
|
||||||
|
|
||||||
|
private static final int VIEW_INDEX_BOTTOM_SHEET = 0;
|
||||||
|
|
||||||
|
private static final int INVALID_POSITION = -1;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final int VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW = 1;
|
||||||
|
|
||||||
private boolean fitToContents = true;
|
private boolean fitToContents = true;
|
||||||
|
|
||||||
private boolean updateImportantForAccessibilityOnSiblings = false;
|
private boolean updateImportantForAccessibilityOnSiblings = false;
|
||||||
|
|
||||||
private float maximumVelocity;
|
private float maximumVelocity;
|
||||||
|
|
||||||
|
private int significantVelocityThreshold;
|
||||||
|
|
||||||
/** Peek height set by the user. */
|
/** Peek height set by the user. */
|
||||||
private int peekHeight;
|
private int peekHeight;
|
||||||
|
|
||||||
|
@ -256,10 +270,12 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
private int insetBottom;
|
private int insetBottom;
|
||||||
private int insetTop;
|
private int insetTop;
|
||||||
|
|
||||||
|
private boolean shouldRemoveExpandedCorners;
|
||||||
|
|
||||||
/** Default Shape Appearance to be used in bottomsheet */
|
/** Default Shape Appearance to be used in bottomsheet */
|
||||||
private ShapeAppearanceModel shapeAppearanceModelDefault;
|
private ShapeAppearanceModel shapeAppearanceModelDefault;
|
||||||
|
|
||||||
private boolean isShapeExpanded;
|
private boolean expandedCornersRemoved;
|
||||||
|
|
||||||
private final StateSettlingTracker stateSettlingTracker = new StateSettlingTracker();
|
private final StateSettlingTracker stateSettlingTracker = new StateSettlingTracker();
|
||||||
|
|
||||||
|
@ -304,22 +320,25 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
int parentHeight;
|
int parentHeight;
|
||||||
|
|
||||||
@Nullable WeakReference<V> viewRef;
|
@Nullable WeakReference<V> viewRef;
|
||||||
|
@Nullable WeakReference<View> accessibilityDelegateViewRef;
|
||||||
|
|
||||||
@Nullable WeakReference<View> nestedScrollingChildRef;
|
@Nullable WeakReference<View> nestedScrollingChildRef;
|
||||||
|
|
||||||
@NonNull private final ArrayList<BottomSheetCallback> callbacks = new ArrayList<>();
|
@NonNull private final ArrayList<BottomSheetCallback> callbacks = new ArrayList<>();
|
||||||
|
|
||||||
@Nullable private VelocityTracker velocityTracker;
|
@Nullable private VelocityTracker velocityTracker;
|
||||||
|
@Nullable MaterialBottomContainerBackHelper bottomContainerBackHelper;
|
||||||
|
|
||||||
int activePointerId;
|
int activePointerId;
|
||||||
|
|
||||||
private int initialY;
|
private int initialY = INVALID_POSITION;
|
||||||
|
|
||||||
boolean touchingScrollingChild;
|
boolean touchingScrollingChild;
|
||||||
|
|
||||||
@Nullable private Map<View, Integer> importantForAccessibilityMap;
|
@Nullable private Map<View, Integer> importantForAccessibilityMap;
|
||||||
|
|
||||||
private int expandHalfwayActionId = View.NO_ID;
|
@VisibleForTesting
|
||||||
|
final SparseIntArray expandHalfwayActionIds = new SparseIntArray();
|
||||||
|
|
||||||
public BackportBottomSheetBehavior() {}
|
public BackportBottomSheetBehavior() {}
|
||||||
|
|
||||||
|
@ -387,6 +406,11 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
R.styleable.BottomSheetBehavior_Layout_behavior_expandedOffset, 0));
|
R.styleable.BottomSheetBehavior_Layout_behavior_expandedOffset, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSignificantVelocityThreshold(
|
||||||
|
a.getInt(
|
||||||
|
R.styleable.BottomSheetBehavior_Layout_behavior_significantVelocityThreshold,
|
||||||
|
DEFAULT_SIGNIFICANT_VEL_THRESHOLD));
|
||||||
|
|
||||||
// Reading out if we are handling padding, so we can apply it to the content.
|
// Reading out if we are handling padding, so we can apply it to the content.
|
||||||
paddingBottomSystemWindowInsets =
|
paddingBottomSystemWindowInsets =
|
||||||
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_paddingBottomSystemWindowInsets, false);
|
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_paddingBottomSystemWindowInsets, false);
|
||||||
|
@ -404,6 +428,8 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginRightSystemWindowInsets, false);
|
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginRightSystemWindowInsets, false);
|
||||||
marginTopSystemWindowInsets =
|
marginTopSystemWindowInsets =
|
||||||
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginTopSystemWindowInsets, false);
|
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_marginTopSystemWindowInsets, false);
|
||||||
|
shouldRemoveExpandedCorners =
|
||||||
|
a.getBoolean(R.styleable.BottomSheetBehavior_Layout_shouldRemoveExpandedCorners, true);
|
||||||
|
|
||||||
a.recycle();
|
a.recycle();
|
||||||
ViewConfiguration configuration = ViewConfiguration.get(context);
|
ViewConfiguration configuration = ViewConfiguration.get(context);
|
||||||
|
@ -440,6 +466,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
// first time we layout with this behavior by checking (viewRef == null).
|
// first time we layout with this behavior by checking (viewRef == null).
|
||||||
viewRef = null;
|
viewRef = null;
|
||||||
viewDragHelper = null;
|
viewDragHelper = null;
|
||||||
|
bottomContainerBackHelper = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -448,6 +475,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
// Release references so we don't run unnecessary codepaths while not attached to a view.
|
// Release references so we don't run unnecessary codepaths while not attached to a view.
|
||||||
viewRef = null;
|
viewRef = null;
|
||||||
viewDragHelper = null;
|
viewDragHelper = null;
|
||||||
|
bottomContainerBackHelper = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -515,7 +543,9 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
peekHeightMin =
|
peekHeightMin =
|
||||||
parent.getResources().getDimensionPixelSize(R.dimen.design_bottom_sheet_peek_height_min);
|
parent.getResources().getDimensionPixelSize(R.dimen.design_bottom_sheet_peek_height_min);
|
||||||
setWindowInsetsListener(child);
|
setWindowInsetsListener(child);
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(child, new InsetsAnimationCallback(child));
|
||||||
viewRef = new WeakReference<>(child);
|
viewRef = new WeakReference<>(child);
|
||||||
|
bottomContainerBackHelper = new MaterialBottomContainerBackHelper(child);
|
||||||
// Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will
|
// Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will
|
||||||
// default to android:background declared in styles or layout.
|
// default to android:background declared in styles or layout.
|
||||||
if (materialShapeDrawable != null) {
|
if (materialShapeDrawable != null) {
|
||||||
|
@ -523,9 +553,6 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
// Use elevation attr if set on bottomsheet; otherwise, use elevation of child view.
|
// Use elevation attr if set on bottomsheet; otherwise, use elevation of child view.
|
||||||
materialShapeDrawable.setElevation(
|
materialShapeDrawable.setElevation(
|
||||||
elevation == -1 ? ViewCompat.getElevation(child) : elevation);
|
elevation == -1 ? ViewCompat.getElevation(child) : elevation);
|
||||||
// Update the material shape based on initial state.
|
|
||||||
isShapeExpanded = state == STATE_EXPANDED;
|
|
||||||
materialShapeDrawable.setInterpolation(isShapeExpanded ? 0f : 1f);
|
|
||||||
} else if (backgroundTint != null) {
|
} else if (backgroundTint != null) {
|
||||||
ViewCompat.setBackgroundTintList(child, backgroundTint);
|
ViewCompat.setBackgroundTintList(child, backgroundTint);
|
||||||
}
|
}
|
||||||
|
@ -549,11 +576,12 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
if (parentHeight - childHeight < insetTop) {
|
if (parentHeight - childHeight < insetTop) {
|
||||||
if (paddingTopSystemWindowInsets) {
|
if (paddingTopSystemWindowInsets) {
|
||||||
// If the bottomsheet would land in the middle of the status bar when fully expanded add
|
// If the bottomsheet would land in the middle of the status bar when fully expanded add
|
||||||
// extra space to make sure it goes all the way.
|
// extra space to make sure it goes all the way up or up to max height if it is specified.
|
||||||
childHeight = parentHeight;
|
childHeight = (maxHeight == NO_MAX_SIZE) ? parentHeight : min(parentHeight, maxHeight);
|
||||||
} else {
|
} else {
|
||||||
// If we don't want the bottomsheet to go under the status bar we cap its height
|
// If we don't want the bottomsheet to go under the status bar we cap its height
|
||||||
childHeight = parentHeight - insetTop;
|
int insetHeight = parentHeight - insetTop;
|
||||||
|
childHeight = (maxHeight == NO_MAX_SIZE) ? insetHeight : min(insetHeight, maxHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fitToContentsOffset = max(0, parentHeight - childHeight);
|
fitToContentsOffset = max(0, parentHeight - childHeight);
|
||||||
|
@ -571,6 +599,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
} else if (state == STATE_DRAGGING || state == STATE_SETTLING) {
|
} else if (state == STATE_DRAGGING || state == STATE_SETTLING) {
|
||||||
ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
|
ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
|
||||||
}
|
}
|
||||||
|
updateDrawableForTargetState(state, /* animate= */ false);
|
||||||
|
|
||||||
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
|
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
|
||||||
|
|
||||||
|
@ -640,6 +669,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
&& state != STATE_DRAGGING
|
&& state != STATE_DRAGGING
|
||||||
&& !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY())
|
&& !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY())
|
||||||
&& viewDragHelper != null
|
&& viewDragHelper != null
|
||||||
|
&& initialY != INVALID_POSITION
|
||||||
&& Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop();
|
&& Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,8 +753,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
}
|
}
|
||||||
} else if (dy < 0) { // Downward
|
} else if (dy < 0) { // Downward
|
||||||
if (!target.canScrollVertically(-1)) {
|
if (!target.canScrollVertically(-1)) {
|
||||||
// MODIFICATION: Add isHideableWhenDragging method
|
if (newTop <= collapsedOffset || canBeHiddenByDragging()) {
|
||||||
if (newTop <= collapsedOffset || (hideable && isHideableWhenDragging())) {
|
|
||||||
if (!draggable) {
|
if (!draggable) {
|
||||||
// Prevent dragging
|
// Prevent dragging
|
||||||
return;
|
return;
|
||||||
|
@ -778,7 +807,6 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// MODIFICATION: Add isHideableWhenDragging method
|
|
||||||
} else if (hideable && shouldHide(child, getYVelocity()) && isHideableWhenDragging()) {
|
} else if (hideable && shouldHide(child, getYVelocity()) && isHideableWhenDragging()) {
|
||||||
targetState = STATE_HIDDEN;
|
targetState = STATE_HIDDEN;
|
||||||
} else if (lastNestedScrollDy == 0) {
|
} else if (lastNestedScrollDy == 0) {
|
||||||
|
@ -888,6 +916,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
// Fix incorrect expanded settings depending on whether or not we are fitting sheet to contents.
|
// Fix incorrect expanded settings depending on whether or not we are fitting sheet to contents.
|
||||||
setStateInternal((this.fitToContents && state == STATE_HALF_EXPANDED) ? STATE_EXPANDED : state);
|
setStateInternal((this.fitToContents && state == STATE_HALF_EXPANDED) ? STATE_EXPANDED : state);
|
||||||
|
|
||||||
|
updateDrawableForTargetState(state, /* animate= */ true);
|
||||||
updateAccessibilityActions();
|
updateAccessibilityActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -897,7 +926,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* be adjusted as expected.
|
* be adjusted as expected.
|
||||||
*
|
*
|
||||||
* @param maxWidth The maximum width in pixels to be set
|
* @param maxWidth The maximum width in pixels to be set
|
||||||
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxWidth
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxWidth
|
||||||
* @see #getMaxWidth()
|
* @see #getMaxWidth()
|
||||||
*/
|
*/
|
||||||
public void setMaxWidth(@Px int maxWidth) {
|
public void setMaxWidth(@Px int maxWidth) {
|
||||||
|
@ -907,7 +936,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
/**
|
/**
|
||||||
* Returns the bottom sheet's maximum width, or -1 if no maximum width is set.
|
* Returns the bottom sheet's maximum width, or -1 if no maximum width is set.
|
||||||
*
|
*
|
||||||
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxWidth
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxWidth
|
||||||
* @see #setMaxWidth(int)
|
* @see #setMaxWidth(int)
|
||||||
*/
|
*/
|
||||||
@Px
|
@Px
|
||||||
|
@ -920,7 +949,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* BottomSheetDialog#show()} in order for the height to be adjusted as expected.
|
* BottomSheetDialog#show()} in order for the height to be adjusted as expected.
|
||||||
*
|
*
|
||||||
* @param maxHeight The maximum height in pixels to be set
|
* @param maxHeight The maximum height in pixels to be set
|
||||||
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxHeight
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxHeight
|
||||||
* @see #getMaxHeight()
|
* @see #getMaxHeight()
|
||||||
*/
|
*/
|
||||||
public void setMaxHeight(@Px int maxHeight) {
|
public void setMaxHeight(@Px int maxHeight) {
|
||||||
|
@ -930,7 +959,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
/**
|
/**
|
||||||
* Returns the bottom sheet's maximum height, or -1 if no maximum height is set.
|
* Returns the bottom sheet's maximum height, or -1 if no maximum height is set.
|
||||||
*
|
*
|
||||||
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_android_maxHeight
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_android_maxHeight
|
||||||
* @see #setMaxHeight(int)
|
* @see #setMaxHeight(int)
|
||||||
*/
|
*/
|
||||||
@Px
|
@Px
|
||||||
|
@ -944,7 +973,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* @param peekHeight The height of the collapsed bottom sheet in pixels, or {@link
|
* @param peekHeight The height of the collapsed bottom sheet in pixels, or {@link
|
||||||
* #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline.
|
* #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline.
|
||||||
* @attr ref
|
* @attr ref
|
||||||
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
|
* com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight
|
||||||
*/
|
*/
|
||||||
public void setPeekHeight(int peekHeight) {
|
public void setPeekHeight(int peekHeight) {
|
||||||
setPeekHeight(peekHeight, false);
|
setPeekHeight(peekHeight, false);
|
||||||
|
@ -958,7 +987,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline.
|
* #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline.
|
||||||
* @param animate Whether to animate between the old height and the new height.
|
* @param animate Whether to animate between the old height and the new height.
|
||||||
* @attr ref
|
* @attr ref
|
||||||
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
|
* com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight
|
||||||
*/
|
*/
|
||||||
public final void setPeekHeight(int peekHeight, boolean animate) {
|
public final void setPeekHeight(int peekHeight, boolean animate) {
|
||||||
boolean layout = false;
|
boolean layout = false;
|
||||||
|
@ -1001,7 +1030,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* @return The height of the collapsed bottom sheet in pixels, or {@link #PEEK_HEIGHT_AUTO} if the
|
* @return The height of the collapsed bottom sheet in pixels, or {@link #PEEK_HEIGHT_AUTO} if the
|
||||||
* sheet is configured to peek automatically at 16:9 ratio keyline
|
* sheet is configured to peek automatically at 16:9 ratio keyline
|
||||||
* @attr ref
|
* @attr ref
|
||||||
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
|
* com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_peekHeight
|
||||||
*/
|
*/
|
||||||
public int getPeekHeight() {
|
public int getPeekHeight() {
|
||||||
return peekHeightAuto ? PEEK_HEIGHT_AUTO : peekHeight;
|
return peekHeightAuto ? PEEK_HEIGHT_AUTO : peekHeight;
|
||||||
|
@ -1015,7 +1044,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
*
|
*
|
||||||
* @param ratio a float between 0 and 1, representing the {@link #STATE_HALF_EXPANDED} ratio.
|
* @param ratio a float between 0 and 1, representing the {@link #STATE_HALF_EXPANDED} ratio.
|
||||||
* @attr ref
|
* @attr ref
|
||||||
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio
|
* com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_halfExpandedRatio
|
||||||
*/
|
*/
|
||||||
public void setHalfExpandedRatio(
|
public void setHalfExpandedRatio(
|
||||||
@FloatRange(from = 0.0f, to = 1.0f, fromInclusive = false, toInclusive = false) float ratio) {
|
@FloatRange(from = 0.0f, to = 1.0f, fromInclusive = false, toInclusive = false) float ratio) {
|
||||||
|
@ -1035,7 +1064,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* Gets the ratio for the height of the BottomSheet in the {@link #STATE_HALF_EXPANDED} state.
|
* Gets the ratio for the height of the BottomSheet in the {@link #STATE_HALF_EXPANDED} state.
|
||||||
*
|
*
|
||||||
* @attr ref
|
* @attr ref
|
||||||
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio
|
* com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_halfExpandedRatio
|
||||||
*/
|
*/
|
||||||
@FloatRange(from = 0.0f, to = 1.0f)
|
@FloatRange(from = 0.0f, to = 1.0f)
|
||||||
public float getHalfExpandedRatio() {
|
public float getHalfExpandedRatio() {
|
||||||
|
@ -1050,13 +1079,14 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* @param offset an integer value greater than equal to 0, representing the {@link
|
* @param offset an integer value greater than equal to 0, representing the {@link
|
||||||
* #STATE_EXPANDED} offset. Value must not exceed the offset in the half expanded state.
|
* #STATE_EXPANDED} offset. Value must not exceed the offset in the half expanded state.
|
||||||
* @attr ref
|
* @attr ref
|
||||||
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset
|
* com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_expandedOffset
|
||||||
*/
|
*/
|
||||||
public void setExpandedOffset(int offset) {
|
public void setExpandedOffset(int offset) {
|
||||||
if (offset < 0) {
|
if (offset < 0) {
|
||||||
throw new IllegalArgumentException("offset must be greater than or equal to 0");
|
throw new IllegalArgumentException("offset must be greater than or equal to 0");
|
||||||
}
|
}
|
||||||
this.expandedOffset = offset;
|
this.expandedOffset = offset;
|
||||||
|
updateDrawableForTargetState(state, /* animate= */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1064,7 +1094,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* pick the offset depending on the height of the content.
|
* pick the offset depending on the height of the content.
|
||||||
*
|
*
|
||||||
* @attr ref
|
* @attr ref
|
||||||
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset
|
* com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_expandedOffset
|
||||||
*/
|
*/
|
||||||
public int getExpandedOffset() {
|
public int getExpandedOffset() {
|
||||||
return fitToContents
|
return fitToContents
|
||||||
|
@ -1072,8 +1102,6 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
: Math.max(expandedOffset, paddingTopSystemWindowInsets ? 0 : insetTop);
|
: Math.max(expandedOffset, paddingTopSystemWindowInsets ? 0 : insetTop);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MODIFICATION: Add calculateSlideOffset method
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the current offset of the bottom sheet.
|
* Calculates the current offset of the bottom sheet.
|
||||||
*
|
*
|
||||||
|
@ -1082,26 +1110,21 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* @return The offset of this bottom sheet within [-1,1] range. Offset increases
|
* @return The offset of this bottom sheet within [-1,1] range. Offset increases
|
||||||
* as this bottom sheet is moving upward. From 0 to 1 the sheet is between collapsed and
|
* as this bottom sheet is moving upward. From 0 to 1 the sheet is between collapsed and
|
||||||
* expanded states and from -1 to 0 it is between hidden and collapsed states. Returns
|
* expanded states and from -1 to 0 it is between hidden and collapsed states. Returns
|
||||||
* {@code Float.MIN_VALUE} if the bottom sheet is not laid out.
|
* -1 if the bottom sheet is not laid out (therefore it's hidden).
|
||||||
*/
|
*/
|
||||||
public float calculateSlideOffset() {
|
public float calculateSlideOffset() {
|
||||||
if (viewRef == null) {
|
if (viewRef == null || viewRef.get() == null) {
|
||||||
return Float.MIN_VALUE;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
View bottomSheet = viewRef.get();
|
return calculateSlideOffsetWithTop(viewRef.get().getTop());
|
||||||
if (bottomSheet != null) {
|
|
||||||
return calculateSlideOffset(bottomSheet.getTop());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Float.MIN_VALUE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether this bottom sheet can hide when it is swiped down.
|
* Sets whether this bottom sheet can hide.
|
||||||
*
|
*
|
||||||
* @param hideable {@code true} to make this bottom sheet hideable.
|
* @param hideable {@code true} to make this bottom sheet hideable.
|
||||||
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_hideable
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_hideable
|
||||||
*/
|
*/
|
||||||
public void setHideable(boolean hideable) {
|
public void setHideable(boolean hideable) {
|
||||||
if (this.hideable != hideable) {
|
if (this.hideable != hideable) {
|
||||||
|
@ -1118,7 +1141,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* Gets whether this bottom sheet can hide when it is swiped down.
|
* Gets whether this bottom sheet can hide when it is swiped down.
|
||||||
*
|
*
|
||||||
* @return {@code true} if this bottom sheet can hide.
|
* @return {@code true} if this bottom sheet can hide.
|
||||||
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_hideable
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_hideable
|
||||||
*/
|
*/
|
||||||
public boolean isHideable() {
|
public boolean isHideable() {
|
||||||
return hideable;
|
return hideable;
|
||||||
|
@ -1130,7 +1153,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
*
|
*
|
||||||
* @param skipCollapsed True if the bottom sheet should skip the collapsed state.
|
* @param skipCollapsed True if the bottom sheet should skip the collapsed state.
|
||||||
* @attr ref
|
* @attr ref
|
||||||
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed
|
* com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_skipCollapsed
|
||||||
*/
|
*/
|
||||||
public void setSkipCollapsed(boolean skipCollapsed) {
|
public void setSkipCollapsed(boolean skipCollapsed) {
|
||||||
this.skipCollapsed = skipCollapsed;
|
this.skipCollapsed = skipCollapsed;
|
||||||
|
@ -1142,7 +1165,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
*
|
*
|
||||||
* @return Whether the bottom sheet should skip the collapsed state.
|
* @return Whether the bottom sheet should skip the collapsed state.
|
||||||
* @attr ref
|
* @attr ref
|
||||||
* com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed
|
* com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_skipCollapsed
|
||||||
*/
|
*/
|
||||||
public boolean getSkipCollapsed() {
|
public boolean getSkipCollapsed() {
|
||||||
return skipCollapsed;
|
return skipCollapsed;
|
||||||
|
@ -1153,7 +1176,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* dragging, an app will require to implement a custom way to expand/collapse the bottom sheet
|
* dragging, an app will require to implement a custom way to expand/collapse the bottom sheet
|
||||||
*
|
*
|
||||||
* @param draggable {@code false} to prevent dragging the sheet to collapse and expand
|
* @param draggable {@code false} to prevent dragging the sheet to collapse and expand
|
||||||
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_draggable
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_draggable
|
||||||
*/
|
*/
|
||||||
public void setDraggable(boolean draggable) {
|
public void setDraggable(boolean draggable) {
|
||||||
this.draggable = draggable;
|
this.draggable = draggable;
|
||||||
|
@ -1163,13 +1186,35 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
return draggable;
|
return draggable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sets the velocity threshold considered significant enough to trigger a slide
|
||||||
|
* to the next stable state.
|
||||||
|
*
|
||||||
|
* @param significantVelocityThreshold The velocity threshold that warrants a vertical swipe.
|
||||||
|
* @see #getSignificantVelocityThreshold()
|
||||||
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_significantVelocityThreshold
|
||||||
|
*/
|
||||||
|
public void setSignificantVelocityThreshold(int significantVelocityThreshold) {
|
||||||
|
this.significantVelocityThreshold = significantVelocityThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns the significant velocity threshold.
|
||||||
|
*
|
||||||
|
* @see #setSignificantVelocityThreshold(int)
|
||||||
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_significantVelocityThreshold
|
||||||
|
*/
|
||||||
|
public int getSignificantVelocityThreshold() {
|
||||||
|
return this.significantVelocityThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets save flags to be preserved in bottomsheet on configuration change.
|
* Sets save flags to be preserved in bottomsheet on configuration change.
|
||||||
*
|
*
|
||||||
* @param flags bitwise int of {@link #SAVE_PEEK_HEIGHT}, {@link #SAVE_FIT_TO_CONTENTS}, {@link
|
* @param flags bitwise int of {@link #SAVE_PEEK_HEIGHT}, {@link #SAVE_FIT_TO_CONTENTS}, {@link
|
||||||
* #SAVE_HIDEABLE}, {@link #SAVE_SKIP_COLLAPSED}, {@link #SAVE_ALL} and {@link #SAVE_NONE}.
|
* #SAVE_HIDEABLE}, {@link #SAVE_SKIP_COLLAPSED}, {@link #SAVE_ALL} and {@link #SAVE_NONE}.
|
||||||
* @see #getSaveFlags()
|
* @see #getSaveFlags()
|
||||||
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_saveFlags
|
||||||
*/
|
*/
|
||||||
public void setSaveFlags(@SaveFlags int flags) {
|
public void setSaveFlags(@SaveFlags int flags) {
|
||||||
this.saveFlags = flags;
|
this.saveFlags = flags;
|
||||||
|
@ -1178,7 +1223,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
* Returns the save flags.
|
* Returns the save flags.
|
||||||
*
|
*
|
||||||
* @see #setSaveFlags(int)
|
* @see #setSaveFlags(int)
|
||||||
* @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags
|
* @attr ref com.google.android.material.R.styleable#BackportBottomSheetBehavior_Layout_behavior_saveFlags
|
||||||
*/
|
*/
|
||||||
@SaveFlags
|
@SaveFlags
|
||||||
public int getSaveFlags() {
|
public int getSaveFlags() {
|
||||||
|
@ -1208,9 +1253,9 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a listener to be notified of bottom sheet events.
|
* Sets a callback to be notified of bottom sheet events.
|
||||||
*
|
*
|
||||||
* @param callback The listener to notify when bottom sheet events occur.
|
* @param callback The callback to notify when bottom sheet events occur.
|
||||||
* @deprecated use {@link #addBottomSheetCallback(BottomSheetCallback)} and {@link
|
* @deprecated use {@link #addBottomSheetCallback(BottomSheetCallback)} and {@link
|
||||||
* #removeBottomSheetCallback(BottomSheetCallback)} instead
|
* #removeBottomSheetCallback(BottomSheetCallback)} instead
|
||||||
*/
|
*/
|
||||||
|
@ -1218,7 +1263,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
public void setBottomSheetCallback(BottomSheetCallback callback) {
|
public void setBottomSheetCallback(BottomSheetCallback callback) {
|
||||||
Log.w(
|
Log.w(
|
||||||
TAG,
|
TAG,
|
||||||
"BottomSheetBehavior now supports multiple callbacks. `setBottomSheetCallback()` removes"
|
"BackportBottomSheetBehavior now supports multiple callbacks. `setBottomSheetCallback()` removes"
|
||||||
+ " all existing callbacks, including ones set internally by library authors, which"
|
+ " all existing callbacks, including ones set internally by library authors, which"
|
||||||
+ " may result in unintended behavior. This may change in the future. Please use"
|
+ " may result in unintended behavior. This may change in the future. Please use"
|
||||||
+ " `addBottomSheetCallback()` and `removeBottomSheetCallback()` instead to set your"
|
+ " `addBottomSheetCallback()` and `removeBottomSheetCallback()` instead to set your"
|
||||||
|
@ -1230,9 +1275,9 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a listener to be notified of bottom sheet events.
|
* Adds a callback to be notified of bottom sheet events.
|
||||||
*
|
*
|
||||||
* @param callback The listener to notify when bottom sheet events occur.
|
* @param callback The callback to notify when bottom sheet events occur.
|
||||||
*/
|
*/
|
||||||
public void addBottomSheetCallback(@NonNull BottomSheetCallback callback) {
|
public void addBottomSheetCallback(@NonNull BottomSheetCallback callback) {
|
||||||
if (!callbacks.contains(callback)) {
|
if (!callbacks.contains(callback)) {
|
||||||
|
@ -1241,9 +1286,9 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a previously added listener.
|
* Removes a previously added callback.
|
||||||
*
|
*
|
||||||
* @param callback The listener to remove.
|
* @param callback The callback to remove.
|
||||||
*/
|
*/
|
||||||
public void removeBottomSheetCallback(@NonNull BottomSheetCallback callback) {
|
public void removeBottomSheetCallback(@NonNull BottomSheetCallback callback) {
|
||||||
callbacks.remove(callback);
|
callbacks.remove(callback);
|
||||||
|
@ -1325,6 +1370,26 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
return gestureInsetBottomIgnored;
|
return gestureInsetBottomIgnored;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the bottom sheet should remove its corners when it reaches the expanded state.
|
||||||
|
*
|
||||||
|
* <p>If false, the bottom sheet will only remove its corners if it is expanded and reaches the
|
||||||
|
* top of the screen.
|
||||||
|
*/
|
||||||
|
public void setShouldRemoveExpandedCorners(boolean shouldRemoveExpandedCorners) {
|
||||||
|
if (this.shouldRemoveExpandedCorners != shouldRemoveExpandedCorners) {
|
||||||
|
this.shouldRemoveExpandedCorners = shouldRemoveExpandedCorners;
|
||||||
|
updateDrawableForTargetState(getState(), /* animate= */ true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the bottom sheet will remove its corners when it reaches the expanded state.
|
||||||
|
*/
|
||||||
|
public boolean isShouldRemoveExpandedCorners() {
|
||||||
|
return shouldRemoveExpandedCorners;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current state of the bottom sheet.
|
* Gets the current state of the bottom sheet.
|
||||||
*
|
*
|
||||||
|
@ -1376,33 +1441,91 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
updateImportantForAccessibility(false);
|
updateImportantForAccessibility(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDrawableForTargetState(state);
|
updateDrawableForTargetState(state, /* animate= */ true);
|
||||||
for (int i = 0; i < callbacks.size(); i++) {
|
for (int i = 0; i < callbacks.size(); i++) {
|
||||||
callbacks.get(i).onStateChanged(bottomSheet, state);
|
callbacks.get(i).onStateChanged(bottomSheet, state);
|
||||||
}
|
}
|
||||||
updateAccessibilityActions();
|
updateAccessibilityActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDrawableForTargetState(@State int state) {
|
private void updateDrawableForTargetState(@State int state, boolean animate) {
|
||||||
if (state == STATE_SETTLING) {
|
if (state == STATE_SETTLING) {
|
||||||
// Special case: we want to know which state we're settling to, so wait for another call.
|
// Special case: we want to know which state we're settling to, so wait for another call.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean expand = state == STATE_EXPANDED;
|
boolean removeCorners = isExpandedAndShouldRemoveCorners();
|
||||||
if (isShapeExpanded != expand) {
|
if (expandedCornersRemoved == removeCorners || materialShapeDrawable == null) {
|
||||||
isShapeExpanded = expand;
|
return;
|
||||||
if (materialShapeDrawable != null && interpolatorAnimator != null) {
|
}
|
||||||
|
expandedCornersRemoved = removeCorners;
|
||||||
|
if (animate && interpolatorAnimator != null) {
|
||||||
if (interpolatorAnimator.isRunning()) {
|
if (interpolatorAnimator.isRunning()) {
|
||||||
interpolatorAnimator.reverse();
|
interpolatorAnimator.reverse();
|
||||||
} else {
|
} else {
|
||||||
float to = expand ? 0f : 1f;
|
float to = removeCorners ? calculateInterpolationWithCornersRemoved() : 1f;
|
||||||
float from = 1f - to;
|
float from = 1f - to;
|
||||||
interpolatorAnimator.setFloatValues(from, to);
|
interpolatorAnimator.setFloatValues(from, to);
|
||||||
interpolatorAnimator.start();
|
interpolatorAnimator.start();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (interpolatorAnimator != null && interpolatorAnimator.isRunning()) {
|
||||||
|
interpolatorAnimator.cancel();
|
||||||
|
}
|
||||||
|
materialShapeDrawable.setInterpolation(
|
||||||
|
expandedCornersRemoved ? calculateInterpolationWithCornersRemoved() : 1f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float calculateInterpolationWithCornersRemoved() {
|
||||||
|
if (materialShapeDrawable != null
|
||||||
|
&& viewRef != null
|
||||||
|
&& viewRef.get() != null
|
||||||
|
&& VERSION.SDK_INT >= VERSION_CODES.S) {
|
||||||
|
V view = viewRef.get();
|
||||||
|
// Only use device corner radius if sheet is touching top of screen.
|
||||||
|
if (isAtTopOfScreen()) {
|
||||||
|
final WindowInsets insets = view.getRootWindowInsets();
|
||||||
|
if (insets != null) {
|
||||||
|
float topLeftInterpolation =
|
||||||
|
calculateCornerInterpolation(
|
||||||
|
materialShapeDrawable.getTopLeftCornerResolvedSize(),
|
||||||
|
insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT));
|
||||||
|
float topRightInterpolation =
|
||||||
|
calculateCornerInterpolation(
|
||||||
|
materialShapeDrawable.getTopRightCornerResolvedSize(),
|
||||||
|
insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT));
|
||||||
|
return Math.max(topLeftInterpolation, topRightInterpolation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(VERSION_CODES.S)
|
||||||
|
private float calculateCornerInterpolation(
|
||||||
|
float materialShapeDrawableCornerSize, @Nullable RoundedCorner deviceRoundedCorner) {
|
||||||
|
if (deviceRoundedCorner != null) {
|
||||||
|
float deviceCornerRadius = deviceRoundedCorner.getRadius();
|
||||||
|
if (deviceCornerRadius > 0 && materialShapeDrawableCornerSize > 0) {
|
||||||
|
return deviceCornerRadius / materialShapeDrawableCornerSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAtTopOfScreen() {
|
||||||
|
if (viewRef == null || viewRef.get() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int[] location = new int[2];
|
||||||
|
viewRef.get().getLocationOnScreen(location);
|
||||||
|
return location[1] == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isExpandedAndShouldRemoveCorners() {
|
||||||
|
// Only remove corners when it's full screen.
|
||||||
|
return state == STATE_EXPANDED && (shouldRemoveExpandedCorners || isAtTopOfScreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
private int calculatePeekHeight() {
|
private int calculatePeekHeight() {
|
||||||
|
@ -1432,9 +1555,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
this.halfExpandedOffset = (int) (parentHeight * (1 - halfExpandedRatio));
|
this.halfExpandedOffset = (int) (parentHeight * (1 - halfExpandedRatio));
|
||||||
}
|
}
|
||||||
|
|
||||||
// MODIFICATION: Add calculateSlideOffset method
|
private float calculateSlideOffsetWithTop(int top) {
|
||||||
|
|
||||||
private float calculateSlideOffset(int top) {
|
|
||||||
return
|
return
|
||||||
(top > collapsedOffset || collapsedOffset == getExpandedOffset())
|
(top > collapsedOffset || collapsedOffset == getExpandedOffset())
|
||||||
? (float) (collapsedOffset - top) / (parentHeight - collapsedOffset)
|
? (float) (collapsedOffset - top) / (parentHeight - collapsedOffset)
|
||||||
|
@ -1443,6 +1564,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
|
|
||||||
private void reset() {
|
private void reset() {
|
||||||
activePointerId = ViewDragHelper.INVALID_POINTER;
|
activePointerId = ViewDragHelper.INVALID_POINTER;
|
||||||
|
initialY = INVALID_POSITION;
|
||||||
if (velocityTracker != null) {
|
if (velocityTracker != null) {
|
||||||
velocityTracker.recycle();
|
velocityTracker.recycle();
|
||||||
velocityTracker = null;
|
velocityTracker = null;
|
||||||
|
@ -1473,6 +1595,9 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
if (skipCollapsed) {
|
if (skipCollapsed) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!isHideableWhenDragging()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (child.getTop() < collapsedOffset) {
|
if (child.getTop() < collapsedOffset) {
|
||||||
// It should not hide, but collapse.
|
// It should not hide, but collapse.
|
||||||
return false;
|
return false;
|
||||||
|
@ -1482,9 +1607,73 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
return Math.abs(newTop - collapsedOffset) / (float) peek > HIDE_THRESHOLD;
|
return Math.abs(newTop - collapsedOffset) / (float) peek > HIDE_THRESHOLD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startBackProgress(@NonNull BackEventCompat backEvent) {
|
||||||
|
if (bottomContainerBackHelper == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bottomContainerBackHelper.startBackProgress(backEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateBackProgress(@NonNull BackEventCompat backEvent) {
|
||||||
|
if (bottomContainerBackHelper == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bottomContainerBackHelper.updateBackProgress(backEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleBackInvoked() {
|
||||||
|
if (bottomContainerBackHelper == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BackEventCompat backEvent = bottomContainerBackHelper.onHandleBackInvoked();
|
||||||
|
if (backEvent == null || VERSION.SDK_INT < VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
// If using traditional button system nav or if pre-U, just hide or collapse the bottom sheet.
|
||||||
|
setState(hideable ? STATE_HIDDEN : STATE_COLLAPSED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hideable) {
|
||||||
|
bottomContainerBackHelper.finishBackProgressNotPersistent(
|
||||||
|
backEvent,
|
||||||
|
new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
// Hide immediately following the built-in predictive back slide down animation.
|
||||||
|
setStateInternal(STATE_HIDDEN);
|
||||||
|
if (viewRef != null && viewRef.get() != null) {
|
||||||
|
viewRef.get().requestLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
bottomContainerBackHelper.finishBackProgressPersistent(
|
||||||
|
backEvent, /* animatorListener= */ null);
|
||||||
|
setState(STATE_COLLAPSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelBackProgress() {
|
||||||
|
if (bottomContainerBackHelper == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bottomContainerBackHelper.cancelBackProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@Nullable
|
||||||
|
MaterialBottomContainerBackHelper getBackHelper() {
|
||||||
|
return bottomContainerBackHelper;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
View findScrollingChild(View view) {
|
View findScrollingChild(View view) {
|
||||||
|
if (view.getVisibility() != View.VISIBLE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (ViewCompat.isNestedScrollingEnabled(view)) {
|
if (ViewCompat.isNestedScrollingEnabled(view)) {
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
@ -1524,12 +1713,12 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialShapeDrawable getMaterialShapeDrawable() {
|
protected MaterialShapeDrawable getMaterialShapeDrawable() {
|
||||||
return materialShapeDrawable;
|
return materialShapeDrawable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createShapeValueAnimator() {
|
private void createShapeValueAnimator() {
|
||||||
interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f);
|
interpolatorAnimator = ValueAnimator.ofFloat(calculateInterpolationWithCornersRemoved(), 1f);
|
||||||
interpolatorAnimator.setDuration(CORNER_ANIMATION_DURATION);
|
interpolatorAnimator.setDuration(CORNER_ANIMATION_DURATION);
|
||||||
interpolatorAnimator.addUpdateListener(
|
interpolatorAnimator.addUpdateListener(
|
||||||
new AnimatorUpdateListener() {
|
new AnimatorUpdateListener() {
|
||||||
|
@ -1650,7 +1839,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
if (settling) {
|
if (settling) {
|
||||||
setStateInternal(STATE_SETTLING);
|
setStateInternal(STATE_SETTLING);
|
||||||
// STATE_SETTLING won't animate the material shape, so do that here with the target state.
|
// STATE_SETTLING won't animate the material shape, so do that here with the target state.
|
||||||
updateDrawableForTargetState(state);
|
updateDrawableForTargetState(state, /* animate= */ true);
|
||||||
stateSettlingTracker.continueSettlingToState(state);
|
stateSettlingTracker.continueSettlingToState(state);
|
||||||
} else {
|
} else {
|
||||||
setStateInternal(state);
|
setStateInternal(state);
|
||||||
|
@ -1741,11 +1930,10 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// MODIFICATION: Add isHideableWhenDragging method
|
} else if (hideable && shouldHide(releasedChild, yvel)) {
|
||||||
} else if (hideable && shouldHide(releasedChild, yvel) && isHideableWhenDragging()) {
|
|
||||||
// Hide if the view was either released low or it was a significant vertical swipe
|
// Hide if the view was either released low or it was a significant vertical swipe
|
||||||
// otherwise settle to closest expanded state.
|
// otherwise settle to closest expanded state.
|
||||||
if ((Math.abs(xvel) < Math.abs(yvel) && yvel > SIGNIFICANT_VEL_THRESHOLD)
|
if ((Math.abs(xvel) < Math.abs(yvel) && yvel > significantVelocityThreshold)
|
||||||
|| releasedLow(releasedChild)) {
|
|| releasedLow(releasedChild)) {
|
||||||
targetState = STATE_HIDDEN;
|
targetState = STATE_HIDDEN;
|
||||||
} else if (fitToContents) {
|
} else if (fitToContents) {
|
||||||
|
@ -1814,9 +2002,10 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
|
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
|
||||||
// MODIFICATION: Add isHideableWhenDragging method
|
|
||||||
return MathUtils.clamp(
|
return MathUtils.clamp(
|
||||||
top, getExpandedOffset(), (hideable && isHideableWhenDragging()) ? parentHeight : collapsedOffset);
|
top,
|
||||||
|
getExpandedOffset(),
|
||||||
|
getViewVerticalDragRange(child));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1826,8 +2015,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getViewVerticalDragRange(@NonNull View child) {
|
public int getViewVerticalDragRange(@NonNull View child) {
|
||||||
// MODIFICATION: Add isHideableWhenDragging method
|
if (canBeHiddenByDragging()) {
|
||||||
if (hideable && isHideableWhenDragging()) {
|
|
||||||
return parentHeight;
|
return parentHeight;
|
||||||
} else {
|
} else {
|
||||||
return collapsedOffset;
|
return collapsedOffset;
|
||||||
|
@ -1838,8 +2026,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
void dispatchOnSlide(int top) {
|
void dispatchOnSlide(int top) {
|
||||||
View bottomSheet = viewRef.get();
|
View bottomSheet = viewRef.get();
|
||||||
if (bottomSheet != null && !callbacks.isEmpty()) {
|
if (bottomSheet != null && !callbacks.isEmpty()) {
|
||||||
// MODIFICATION: Add calculateSlideOffset method
|
float slideOffset = calculateSlideOffsetWithTop(top);
|
||||||
float slideOffset = calculateSlideOffset(top);
|
|
||||||
for (int i = 0; i < callbacks.size(); i++) {
|
for (int i = 0; i < callbacks.size(); i++) {
|
||||||
callbacks.get(i).onSlide(bottomSheet, slideOffset);
|
callbacks.get(i).onSlide(bottomSheet, slideOffset);
|
||||||
}
|
}
|
||||||
|
@ -1898,7 +2085,8 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether hiding gestures should be enabled if {@code isHideable} is true.
|
* Checks whether hiding gestures should be enabled while {@code isHideable} is set to true.
|
||||||
|
*
|
||||||
* @hide
|
* @hide
|
||||||
*/
|
*/
|
||||||
@RestrictTo(LIBRARY_GROUP)
|
@RestrictTo(LIBRARY_GROUP)
|
||||||
|
@ -1906,6 +2094,10 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canBeHiddenByDragging() {
|
||||||
|
return isHideable() && isHideableWhenDragging();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the bottom sheet should be expanded after it has been released after dragging.
|
* Checks whether the bottom sheet should be expanded after it has been released after dragging.
|
||||||
*
|
*
|
||||||
|
@ -2067,7 +2259,7 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
CoordinatorLayout.Behavior<?> behavior =
|
CoordinatorLayout.Behavior<?> behavior =
|
||||||
((CoordinatorLayout.LayoutParams) params).getBehavior();
|
((CoordinatorLayout.LayoutParams) params).getBehavior();
|
||||||
if (!(behavior instanceof BackportBottomSheetBehavior)) {
|
if (!(behavior instanceof BackportBottomSheetBehavior)) {
|
||||||
throw new IllegalArgumentException("The view is not associated with BottomSheetBehavior");
|
throw new IllegalArgumentException("The view is not associated with BackportBottomSheetBehavior");
|
||||||
}
|
}
|
||||||
return (BackportBottomSheetBehavior<V>) behavior;
|
return (BackportBottomSheetBehavior<V>) behavior;
|
||||||
}
|
}
|
||||||
|
@ -2139,30 +2331,43 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAccessibilityDelegateView(@Nullable View accessibilityDelegateView) {
|
||||||
|
if (accessibilityDelegateView == null && accessibilityDelegateViewRef != null) {
|
||||||
|
clearAccessibilityAction(
|
||||||
|
accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW);
|
||||||
|
accessibilityDelegateViewRef = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
accessibilityDelegateViewRef = new WeakReference<>(accessibilityDelegateView);
|
||||||
|
updateAccessibilityActions(accessibilityDelegateView, VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateAccessibilityActions() {
|
private void updateAccessibilityActions() {
|
||||||
if (viewRef == null) {
|
if (viewRef != null) {
|
||||||
return;
|
updateAccessibilityActions(viewRef.get(), VIEW_INDEX_BOTTOM_SHEET);
|
||||||
|
}
|
||||||
|
if (accessibilityDelegateViewRef != null) {
|
||||||
|
updateAccessibilityActions(
|
||||||
|
accessibilityDelegateViewRef.get(), VIEW_INDEX_ACCESSIBILITY_DELEGATE_VIEW);
|
||||||
}
|
}
|
||||||
V child = viewRef.get();
|
|
||||||
if (child == null) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_COLLAPSE);
|
|
||||||
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_EXPAND);
|
|
||||||
ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_DISMISS);
|
|
||||||
|
|
||||||
if (expandHalfwayActionId != View.NO_ID) {
|
private void updateAccessibilityActions(View view, int viewIndex) {
|
||||||
ViewCompat.removeAccessibilityAction(child, expandHalfwayActionId);
|
if (view == null) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
clearAccessibilityAction(view, viewIndex);
|
||||||
|
|
||||||
if (!fitToContents && state != STATE_HALF_EXPANDED) {
|
if (!fitToContents && state != STATE_HALF_EXPANDED) {
|
||||||
expandHalfwayActionId =
|
expandHalfwayActionIds.put(
|
||||||
|
viewIndex,
|
||||||
addAccessibilityActionForState(
|
addAccessibilityActionForState(
|
||||||
child, R.string.bottomsheet_action_expand_halfway, STATE_HALF_EXPANDED);
|
view, R.string.bottomsheet_action_expand_halfway, STATE_HALF_EXPANDED));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hideable && state != STATE_HIDDEN) {
|
if ((hideable && isHideableWhenDragging()) && state != STATE_HIDDEN) {
|
||||||
replaceAccessibilityActionForState(
|
replaceAccessibilityActionForState(
|
||||||
child, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN);
|
view, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
@ -2170,36 +2375,54 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
{
|
{
|
||||||
int nextState = fitToContents ? STATE_COLLAPSED : STATE_HALF_EXPANDED;
|
int nextState = fitToContents ? STATE_COLLAPSED : STATE_HALF_EXPANDED;
|
||||||
replaceAccessibilityActionForState(
|
replaceAccessibilityActionForState(
|
||||||
child, AccessibilityActionCompat.ACTION_COLLAPSE, nextState);
|
view, AccessibilityActionCompat.ACTION_COLLAPSE, nextState);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case STATE_HALF_EXPANDED:
|
case STATE_HALF_EXPANDED:
|
||||||
{
|
{
|
||||||
replaceAccessibilityActionForState(
|
replaceAccessibilityActionForState(
|
||||||
child, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED);
|
view, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED);
|
||||||
replaceAccessibilityActionForState(
|
replaceAccessibilityActionForState(
|
||||||
child, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED);
|
view, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case STATE_COLLAPSED:
|
case STATE_COLLAPSED:
|
||||||
{
|
{
|
||||||
int nextState = fitToContents ? STATE_EXPANDED : STATE_HALF_EXPANDED;
|
int nextState = fitToContents ? STATE_EXPANDED : STATE_HALF_EXPANDED;
|
||||||
replaceAccessibilityActionForState(
|
replaceAccessibilityActionForState(
|
||||||
child, AccessibilityActionCompat.ACTION_EXPAND, nextState);
|
view, AccessibilityActionCompat.ACTION_EXPAND, nextState);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: // fall out
|
case STATE_HIDDEN:
|
||||||
|
case STATE_DRAGGING:
|
||||||
|
case STATE_SETTLING:
|
||||||
|
// Accessibility actions are not applicable, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAccessibilityAction(View view, int viewIndex) {
|
||||||
|
if (view == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_COLLAPSE);
|
||||||
|
ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_EXPAND);
|
||||||
|
ViewCompat.removeAccessibilityAction(view, AccessibilityNodeInfoCompat.ACTION_DISMISS);
|
||||||
|
|
||||||
|
int expandHalfwayActionId = expandHalfwayActionIds.get(viewIndex, View.NO_ID);
|
||||||
|
if (expandHalfwayActionId != View.NO_ID) {
|
||||||
|
ViewCompat.removeAccessibilityAction(view, expandHalfwayActionId);
|
||||||
|
expandHalfwayActionIds.delete(viewIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void replaceAccessibilityActionForState(
|
private void replaceAccessibilityActionForState(
|
||||||
V child, AccessibilityActionCompat action, @State int state) {
|
View child, AccessibilityActionCompat action, @State int state) {
|
||||||
ViewCompat.replaceAccessibilityAction(
|
ViewCompat.replaceAccessibilityAction(
|
||||||
child, action, null, createAccessibilityViewCommandForState(state));
|
child, action, null, createAccessibilityViewCommandForState(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
private int addAccessibilityActionForState(
|
private int addAccessibilityActionForState(
|
||||||
V child, @StringRes int stringResId, @State int state) {
|
View child, @StringRes int stringResId, @State int state) {
|
||||||
return ViewCompat.addAccessibilityAction(
|
return ViewCompat.addAccessibilityAction(
|
||||||
child,
|
child,
|
||||||
child.getResources().getString(stringResId),
|
child.getResources().getString(stringResId),
|
||||||
|
@ -2216,4 +2439,3 @@ public class BackportBottomSheetBehavior<V extends View> extends CoordinatorLayo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,413 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2021 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.google.android.material.divider;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.graphics.drawable.ShapeDrawable;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
import androidx.annotation.ColorRes;
|
|
||||||
import androidx.annotation.DimenRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.Px;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.core.graphics.drawable.DrawableCompat;
|
|
||||||
import androidx.core.view.ViewCompat;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
|
|
||||||
|
|
||||||
import com.google.android.material.R;
|
|
||||||
import com.google.android.material.internal.ThemeEnforcement;
|
|
||||||
import com.google.android.material.resources.MaterialResources;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MaterialDividerItemDecoration is a {@link RecyclerView.ItemDecoration}, similar to a {@link
|
|
||||||
* androidx.recyclerview.widget.DividerItemDecoration}, that can be used as a divider between items of
|
|
||||||
* a {@link LinearLayoutManager}. It supports both {@link #HORIZONTAL} and {@link #VERTICAL}
|
|
||||||
* orientations.
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* dividerItemDecoration = new MaterialDividerItemDecoration(recyclerView.getContext(),
|
|
||||||
* layoutManager.getOrientation());
|
|
||||||
* recyclerView.addItemDecoration(dividerItemDecoration);
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* Modified at several points by Alexander Capehart to backport miscellaneous fixes not currently
|
|
||||||
* obtainable in the currently used MDC library.
|
|
||||||
*/
|
|
||||||
public class BackportMaterialDividerItemDecoration extends ItemDecoration {
|
|
||||||
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
|
|
||||||
public static final int VERTICAL = LinearLayout.VERTICAL;
|
|
||||||
|
|
||||||
private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_MaterialDivider;
|
|
||||||
|
|
||||||
@NonNull private Drawable dividerDrawable;
|
|
||||||
private int thickness;
|
|
||||||
@ColorInt private int color;
|
|
||||||
private int orientation;
|
|
||||||
private int insetStart;
|
|
||||||
private int insetEnd;
|
|
||||||
private boolean lastItemDecorated;
|
|
||||||
|
|
||||||
private final Rect tempRect = new Rect();
|
|
||||||
|
|
||||||
public BackportMaterialDividerItemDecoration(@NonNull Context context, int orientation) {
|
|
||||||
this(context, null, orientation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BackportMaterialDividerItemDecoration(
|
|
||||||
@NonNull Context context, @Nullable AttributeSet attrs, int orientation) {
|
|
||||||
this(context, attrs, R.attr.materialDividerStyle, orientation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BackportMaterialDividerItemDecoration(
|
|
||||||
@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int orientation) {
|
|
||||||
TypedArray attributes =
|
|
||||||
ThemeEnforcement.obtainStyledAttributes(
|
|
||||||
context, attrs, R.styleable.MaterialDivider, defStyleAttr, DEF_STYLE_RES);
|
|
||||||
|
|
||||||
color =
|
|
||||||
MaterialResources.getColorStateList(
|
|
||||||
context, attributes, R.styleable.MaterialDivider_dividerColor)
|
|
||||||
.getDefaultColor();
|
|
||||||
thickness =
|
|
||||||
attributes.getDimensionPixelSize(
|
|
||||||
R.styleable.MaterialDivider_dividerThickness,
|
|
||||||
context.getResources().getDimensionPixelSize(R.dimen.material_divider_thickness));
|
|
||||||
insetStart =
|
|
||||||
attributes.getDimensionPixelOffset(R.styleable.MaterialDivider_dividerInsetStart, 0);
|
|
||||||
insetEnd = attributes.getDimensionPixelOffset(R.styleable.MaterialDivider_dividerInsetEnd, 0);
|
|
||||||
lastItemDecorated =
|
|
||||||
attributes.getBoolean(R.styleable.MaterialDivider_lastItemDecorated, true);
|
|
||||||
|
|
||||||
attributes.recycle();
|
|
||||||
|
|
||||||
dividerDrawable = new ShapeDrawable();
|
|
||||||
setDividerColor(color);
|
|
||||||
setOrientation(orientation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the orientation for this divider. This should be called if {@link
|
|
||||||
* RecyclerView.LayoutManager} changes orientation.
|
|
||||||
*
|
|
||||||
* <p>A {@link #HORIZONTAL} orientation will draw a vertical divider, and a {@link #VERTICAL}
|
|
||||||
* orientation a horizontal divider.
|
|
||||||
*
|
|
||||||
* @param orientation The orientation of the {@link RecyclerView} this divider is associated with:
|
|
||||||
* {@link #HORIZONTAL} or {@link #VERTICAL}
|
|
||||||
*/
|
|
||||||
public void setOrientation(int orientation) {
|
|
||||||
if (orientation != HORIZONTAL && orientation != VERTICAL) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Invalid orientation: " + orientation + ". It should be either HORIZONTAL or VERTICAL");
|
|
||||||
}
|
|
||||||
this.orientation = orientation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOrientation() {
|
|
||||||
return orientation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the thickness of the divider.
|
|
||||||
*
|
|
||||||
* @param thickness The thickness value to be set.
|
|
||||||
* @see #getDividerThickness()
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness
|
|
||||||
*/
|
|
||||||
public void setDividerThickness(@Px int thickness) {
|
|
||||||
this.thickness = thickness;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the thickness of the divider.
|
|
||||||
*
|
|
||||||
* @param thicknessId The id of the thickness dimension resource to be set.
|
|
||||||
* @see #getDividerThickness()
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness
|
|
||||||
*/
|
|
||||||
public void setDividerThicknessResource(@NonNull Context context, @DimenRes int thicknessId) {
|
|
||||||
setDividerThickness(context.getResources().getDimensionPixelSize(thicknessId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the thickness set on the divider.
|
|
||||||
*
|
|
||||||
* @see #setDividerThickness(int)
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerThickness
|
|
||||||
*/
|
|
||||||
@Px
|
|
||||||
public int getDividerThickness() {
|
|
||||||
return thickness;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the color of the divider.
|
|
||||||
*
|
|
||||||
* @param color The color to be set.
|
|
||||||
* @see #getDividerColor()
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor
|
|
||||||
*/
|
|
||||||
public void setDividerColor(@ColorInt int color) {
|
|
||||||
this.color = color;
|
|
||||||
dividerDrawable = DrawableCompat.wrap(dividerDrawable);
|
|
||||||
DrawableCompat.setTint(dividerDrawable, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the color of the divider.
|
|
||||||
*
|
|
||||||
* @param colorId The id of the color resource to be set.
|
|
||||||
* @see #getDividerColor()
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor
|
|
||||||
*/
|
|
||||||
public void setDividerColorResource(@NonNull Context context, @ColorRes int colorId) {
|
|
||||||
setDividerColor(ContextCompat.getColor(context, colorId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the divider color.
|
|
||||||
*
|
|
||||||
* @see #setDividerColor(int)
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerColor
|
|
||||||
*/
|
|
||||||
@ColorInt
|
|
||||||
public int getDividerColor() {
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the start inset of the divider.
|
|
||||||
*
|
|
||||||
* @param insetStart The start inset to be set.
|
|
||||||
* @see #getDividerInsetStart()
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart
|
|
||||||
*/
|
|
||||||
public void setDividerInsetStart(@Px int insetStart) {
|
|
||||||
this.insetStart = insetStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the start inset of the divider.
|
|
||||||
*
|
|
||||||
* @param insetStartId The id of the inset dimension resource to be set.
|
|
||||||
* @see #getDividerInsetStart()
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart
|
|
||||||
*/
|
|
||||||
public void setDividerInsetStartResource(@NonNull Context context, @DimenRes int insetStartId) {
|
|
||||||
setDividerInsetStart(context.getResources().getDimensionPixelOffset(insetStartId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the divider's start inset.
|
|
||||||
*
|
|
||||||
* @see #setDividerInsetStart(int)
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetStart
|
|
||||||
*/
|
|
||||||
@Px
|
|
||||||
public int getDividerInsetStart() {
|
|
||||||
return insetStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the end inset of the divider.
|
|
||||||
*
|
|
||||||
* @param insetEnd The end inset to be set.
|
|
||||||
* @see #getDividerInsetEnd()
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd
|
|
||||||
*/
|
|
||||||
public void setDividerInsetEnd(@Px int insetEnd) {
|
|
||||||
this.insetEnd = insetEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the end inset of the divider.
|
|
||||||
*
|
|
||||||
* @param insetEndId The id of the inset dimension resource to be set.
|
|
||||||
* @see #getDividerInsetEnd()
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd
|
|
||||||
*/
|
|
||||||
public void setDividerInsetEndResource(@NonNull Context context, @DimenRes int insetEndId) {
|
|
||||||
setDividerInsetEnd(context.getResources().getDimensionPixelOffset(insetEndId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the divider's end inset.
|
|
||||||
*
|
|
||||||
* @see #setDividerInsetEnd(int)
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_dividerInsetEnd
|
|
||||||
*/
|
|
||||||
@Px
|
|
||||||
public int getDividerInsetEnd() {
|
|
||||||
return insetEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the class should draw a divider after the last item of a {@link RecyclerView}.
|
|
||||||
*
|
|
||||||
* @param lastItemDecorated whether there's a divider after the last item of a recycler view.
|
|
||||||
* @see #isLastItemDecorated()
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_lastItemDecorated
|
|
||||||
*/
|
|
||||||
public void setLastItemDecorated(boolean lastItemDecorated) {
|
|
||||||
this.lastItemDecorated = lastItemDecorated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether there's a divider after the last item of a {@link RecyclerView}.
|
|
||||||
*
|
|
||||||
* @see #setLastItemDecorated(boolean)
|
|
||||||
* @attr ref com.google.android.material.R.styleable#MaterialDivider_shouldDecorateLastItem
|
|
||||||
*/
|
|
||||||
public boolean isLastItemDecorated() {
|
|
||||||
return lastItemDecorated;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDraw(
|
|
||||||
@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
|
|
||||||
if (parent.getLayoutManager() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (orientation == VERTICAL) {
|
|
||||||
drawForVerticalOrientation(canvas, parent);
|
|
||||||
} else {
|
|
||||||
drawForHorizontalOrientation(canvas, parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws a divider for the vertical orientation of the recycler view. The divider itself will be
|
|
||||||
* horizontal.
|
|
||||||
*/
|
|
||||||
private void drawForVerticalOrientation(@NonNull Canvas canvas, @NonNull RecyclerView parent) {
|
|
||||||
canvas.save();
|
|
||||||
int left;
|
|
||||||
int right;
|
|
||||||
if (parent.getClipToPadding()) {
|
|
||||||
left = parent.getPaddingLeft();
|
|
||||||
right = parent.getWidth() - parent.getPaddingRight();
|
|
||||||
canvas.clipRect(
|
|
||||||
left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
|
|
||||||
} else {
|
|
||||||
left = 0;
|
|
||||||
right = parent.getWidth();
|
|
||||||
}
|
|
||||||
boolean isRtl = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
|
||||||
left += isRtl ? insetEnd : insetStart;
|
|
||||||
right -= isRtl ? insetStart : insetEnd;
|
|
||||||
|
|
||||||
int childCount = parent.getChildCount();
|
|
||||||
for (int i = 0; i < childCount; i++) {
|
|
||||||
View child = parent.getChildAt(i);
|
|
||||||
if (shouldDrawDivider(parent, child)) {
|
|
||||||
parent.getDecoratedBoundsWithMargins(child, tempRect);
|
|
||||||
// Take into consideration any translationY added to the view.
|
|
||||||
int bottom = tempRect.bottom + Math.round(child.getTranslationY());
|
|
||||||
int top = bottom - thickness;
|
|
||||||
dividerDrawable.setBounds(left, top, right, bottom);
|
|
||||||
dividerDrawable.draw(canvas);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
canvas.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws a divider for the horizontal orientation of the recycler view. The divider itself will be
|
|
||||||
* vertical.
|
|
||||||
*/
|
|
||||||
private void drawForHorizontalOrientation(@NonNull Canvas canvas, @NonNull RecyclerView parent) {
|
|
||||||
canvas.save();
|
|
||||||
int top;
|
|
||||||
int bottom;
|
|
||||||
if (parent.getClipToPadding()) {
|
|
||||||
top = parent.getPaddingTop();
|
|
||||||
bottom = parent.getHeight() - parent.getPaddingBottom();
|
|
||||||
canvas.clipRect(
|
|
||||||
parent.getPaddingLeft(), top, parent.getWidth() - parent.getPaddingRight(), bottom);
|
|
||||||
} else {
|
|
||||||
top = 0;
|
|
||||||
bottom = parent.getHeight();
|
|
||||||
}
|
|
||||||
top += insetStart;
|
|
||||||
bottom -= insetEnd;
|
|
||||||
|
|
||||||
int childCount = parent.getChildCount();
|
|
||||||
for (int i = 0; i < childCount; i++) {
|
|
||||||
View child = parent.getChildAt(i);
|
|
||||||
if (shouldDrawDivider(parent, child)) {
|
|
||||||
parent.getDecoratedBoundsWithMargins(child, tempRect);
|
|
||||||
// Take into consideration any translationX added to the view.
|
|
||||||
int right = tempRect.right + Math.round(child.getTranslationX());
|
|
||||||
int left = right - thickness;
|
|
||||||
dividerDrawable.setBounds(left, top, right, bottom);
|
|
||||||
dividerDrawable.draw(canvas);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
canvas.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void getItemOffsets(
|
|
||||||
@NonNull Rect outRect,
|
|
||||||
@NonNull View view,
|
|
||||||
@NonNull RecyclerView parent,
|
|
||||||
@NonNull RecyclerView.State state) {
|
|
||||||
outRect.set(0, 0, 0, 0);
|
|
||||||
// Only add offset if there's a divider displayed.
|
|
||||||
if (shouldDrawDivider(parent, view)) {
|
|
||||||
if (orientation == VERTICAL) {
|
|
||||||
outRect.bottom = thickness;
|
|
||||||
} else {
|
|
||||||
outRect.right = thickness;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldDrawDivider(@NonNull RecyclerView parent, @NonNull View child) {
|
|
||||||
int position = parent.getChildAdapterPosition(child);
|
|
||||||
RecyclerView.Adapter<?> adapter = parent.getAdapter();
|
|
||||||
boolean isLastItem = adapter != null && position == adapter.getItemCount() - 1;
|
|
||||||
|
|
||||||
return position != RecyclerView.NO_POSITION
|
|
||||||
&& (!isLastItem || lastItemDecorated)
|
|
||||||
&& shouldDrawDivider(position, adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a divider should be drawn below the current item that is being drawn.
|
|
||||||
*
|
|
||||||
* <p>Note: if lasItemDecorated is false, the divider below the last item will never be drawn even
|
|
||||||
* if this method returns true.
|
|
||||||
*
|
|
||||||
* @param position the position of the current item being drawn.
|
|
||||||
* @param adapter the {@link RecyclerView.Adapter} associated with the item being drawn.
|
|
||||||
*/
|
|
||||||
protected boolean shouldDrawDivider(int position, @Nullable RecyclerView.Adapter<?> adapter) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -48,6 +48,8 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
|
||||||
|
import org.oxycblt.auxio.image.extractor.SquareCropTransformation
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
@ -77,6 +79,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
FrameLayout(context, attrs, defStyleAttr) {
|
FrameLayout(context, attrs, defStyleAttr) {
|
||||||
@Inject lateinit var imageLoader: ImageLoader
|
@Inject lateinit var imageLoader: ImageLoader
|
||||||
@Inject lateinit var uiSettings: UISettings
|
@Inject lateinit var uiSettings: UISettings
|
||||||
|
@Inject lateinit var imageSettings: ImageSettings
|
||||||
|
|
||||||
private val image: ImageView
|
private val image: ImageView
|
||||||
|
|
||||||
|
@ -384,13 +387,19 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(songs)
|
.data(songs)
|
||||||
.error(StyledDrawable(context, context.getDrawableCompat(errorRes), iconSizeRes))
|
.error(StyledDrawable(context, context.getDrawableCompat(errorRes), iconSizeRes))
|
||||||
.transformations(
|
|
||||||
RoundedCornersTransformation(cornerRadiusRes?.let(context::getDimen) ?: 0f))
|
|
||||||
.target(image)
|
.target(image)
|
||||||
.build()
|
|
||||||
|
val cornersTransformation =
|
||||||
|
RoundedRectTransformation(cornerRadiusRes?.let(context::getDimen) ?: 0f)
|
||||||
|
if (imageSettings.forceSquareCovers) {
|
||||||
|
request.transformations(SquareCropTransformation.INSTANCE, cornersTransformation)
|
||||||
|
} else {
|
||||||
|
request.transformations(cornersTransformation)
|
||||||
|
}
|
||||||
|
|
||||||
// Dispose of any previous image request and load a new image.
|
// Dispose of any previous image request and load a new image.
|
||||||
CoilUtils.dispose(image)
|
CoilUtils.dispose(image)
|
||||||
imageLoader.enqueue(request)
|
imageLoader.enqueue(request.build())
|
||||||
contentDescription = desc
|
contentDescription = desc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,8 @@ import org.oxycblt.auxio.util.logD
|
||||||
interface ImageSettings : Settings<ImageSettings.Listener> {
|
interface ImageSettings : Settings<ImageSettings.Listener> {
|
||||||
/** The strategy to use when loading album covers. */
|
/** The strategy to use when loading album covers. */
|
||||||
val coverMode: CoverMode
|
val coverMode: CoverMode
|
||||||
|
/** Whether to force all album covers to have a 1:1 aspect ratio. */
|
||||||
|
val forceSquareCovers: Boolean
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
/** Called when [coverMode] changes. */
|
/** Called when [coverMode] changes. */
|
||||||
|
@ -49,6 +51,9 @@ class ImageSettingsImpl @Inject constructor(@ApplicationContext context: Context
|
||||||
sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
|
sharedPreferences.getInt(getString(R.string.set_key_cover_mode), Int.MIN_VALUE))
|
||||||
?: CoverMode.MEDIA_STORE
|
?: CoverMode.MEDIA_STORE
|
||||||
|
|
||||||
|
override val forceSquareCovers: Boolean
|
||||||
|
get() = sharedPreferences.getBoolean(getString(R.string.set_key_square_covers), false)
|
||||||
|
|
||||||
override fun migrate() {
|
override fun migrate() {
|
||||||
// Show album covers and Ignore MediaStore covers were unified in 3.0.0
|
// Show album covers and Ignore MediaStore covers were unified in 3.0.0
|
||||||
if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) ||
|
if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) ||
|
||||||
|
|
|
@ -43,7 +43,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.min
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.guava.asDeferred
|
import kotlinx.coroutines.guava.asDeferred
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -155,7 +154,7 @@ constructor(
|
||||||
// Get the embedded picture from MediaMetadataRetriever, which will return a full
|
// Get the embedded picture from MediaMetadataRetriever, which will return a full
|
||||||
// ByteArray of the cover without any compression artifacts.
|
// ByteArray of the cover without any compression artifacts.
|
||||||
// If its null [i.e there is no embedded cover], than just ignore it and move on
|
// If its null [i.e there is no embedded cover], than just ignore it and move on
|
||||||
return embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() }
|
embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun extractExoplayerCover(album: Album): InputStream? {
|
private suspend fun extractExoplayerCover(album: Album): InputStream? {
|
||||||
|
@ -212,7 +211,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
|
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
|
||||||
private fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
|
private suspend fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
|
||||||
// Use whatever size coil gives us to create the mosaic.
|
// Use whatever size coil gives us to create the mosaic.
|
||||||
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
|
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
|
||||||
val mosaicFrameSize =
|
val mosaicFrameSize =
|
||||||
|
@ -234,7 +233,9 @@ constructor(
|
||||||
|
|
||||||
// Crop the bitmap down to a square so it leaves no empty space
|
// Crop the bitmap down to a square so it leaves no empty space
|
||||||
// TODO: Work around this
|
// TODO: Work around this
|
||||||
val bitmap = cropBitmap(BitmapFactory.decodeStream(stream), mosaicFrameSize)
|
val bitmap =
|
||||||
|
SquareCropTransformation.INSTANCE.transform(
|
||||||
|
BitmapFactory.decodeStream(stream), mosaicFrameSize)
|
||||||
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
||||||
|
|
||||||
x += bitmap.width
|
x += bitmap.width
|
||||||
|
@ -259,21 +260,4 @@ constructor(
|
||||||
val size = pxOrElse { 512 }
|
val size = pxOrElse { 512 }
|
||||||
return if (size.mod(2) > 0) size + 1 else size
|
return if (size.mod(2) > 0) size + 1 else size
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cropBitmap(input: Bitmap, size: Size): Bitmap {
|
|
||||||
// Find the smaller dimension and then take a center portion of the image that
|
|
||||||
// has that size.
|
|
||||||
val dstSize = min(input.width, input.height)
|
|
||||||
val x = (input.width - dstSize) / 2
|
|
||||||
val y = (input.height - dstSize) / 2
|
|
||||||
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
|
|
||||||
|
|
||||||
val desiredWidth = size.width.pxOrElse { dstSize }
|
|
||||||
val desiredHeight = size.height.pxOrElse { dstSize }
|
|
||||||
if (dstSize != desiredWidth || dstSize != desiredHeight) {
|
|
||||||
// Image is not the desired size, upscale it.
|
|
||||||
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
|
||||||
}
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* Copyright (c) 2023 Auxio Project
|
||||||
* RoundedCornersTransformation.kt is part of Auxio.
|
* RoundedRectTransformation.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.image
|
package org.oxycblt.auxio.image.extractor
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Bitmap.createBitmap
|
import android.graphics.Bitmap.createBitmap
|
||||||
|
@ -43,7 +43,7 @@ import kotlin.math.roundToInt
|
||||||
*
|
*
|
||||||
* @author Coil Team, Alexander Capehart (OxygenCobalt)
|
* @author Coil Team, Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class RoundedCornersTransformation(
|
class RoundedRectTransformation(
|
||||||
@Px private val topLeft: Float = 0f,
|
@Px private val topLeft: Float = 0f,
|
||||||
@Px private val topRight: Float = 0f,
|
@Px private val topRight: Float = 0f,
|
||||||
@Px private val bottomLeft: Float = 0f,
|
@Px private val bottomLeft: Float = 0f,
|
||||||
|
@ -122,7 +122,7 @@ class RoundedCornersTransformation(
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
return other is RoundedCornersTransformation &&
|
return other is RoundedRectTransformation &&
|
||||||
topLeft == other.topLeft &&
|
topLeft == other.topLeft &&
|
||||||
topRight == other.topRight &&
|
topRight == other.topRight &&
|
||||||
bottomLeft == other.bottomLeft &&
|
bottomLeft == other.bottomLeft &&
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* SquareCropTransformation.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.image.extractor
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import coil.size.Size
|
||||||
|
import coil.size.pxOrElse
|
||||||
|
import coil.transform.Transformation
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Transformation] that performs a center crop-style transformation on an image. Allowing this
|
||||||
|
* behavior to be intrinsic without any view configuration.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
class SquareCropTransformation : Transformation {
|
||||||
|
override val cacheKey: String
|
||||||
|
get() = "SquareCropTransformation"
|
||||||
|
|
||||||
|
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
||||||
|
// Find the smaller dimension and then take a center portion of the image that
|
||||||
|
// has that size.
|
||||||
|
val dstSize = min(input.width, input.height)
|
||||||
|
val x = (input.width - dstSize) / 2
|
||||||
|
val y = (input.height - dstSize) / 2
|
||||||
|
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
|
||||||
|
|
||||||
|
val desiredWidth = size.width.pxOrElse { dstSize }
|
||||||
|
val desiredHeight = size.height.pxOrElse { dstSize }
|
||||||
|
if (dstSize != desiredWidth || dstSize != desiredHeight) {
|
||||||
|
// Image is not the desired size, upscale it.
|
||||||
|
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** A re-usable instance. */
|
||||||
|
val INSTANCE = SquareCropTransformation()
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,8 +23,6 @@ import android.content.pm.PackageManager
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -339,19 +337,18 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun index(worker: MusicRepository.IndexingWorker, withCache: Boolean) =
|
override fun index(worker: MusicRepository.IndexingWorker, withCache: Boolean) =
|
||||||
worker.scope.launch {
|
worker.scope.launch { indexWrapper(worker, withCache) }
|
||||||
|
|
||||||
|
private suspend fun indexWrapper(worker: MusicRepository.IndexingWorker, withCache: Boolean) {
|
||||||
try {
|
try {
|
||||||
val start = System.currentTimeMillis()
|
|
||||||
indexImpl(worker, withCache)
|
indexImpl(worker, withCache)
|
||||||
logD(
|
|
||||||
"Music indexing completed successfully in " +
|
|
||||||
"${System.currentTimeMillis() - start}ms")
|
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
// Got cancelled, propagate upwards to top-level co-routine.
|
// Got cancelled, propagate upwards to top-level co-routine.
|
||||||
logD("Loading routine was cancelled")
|
logD("Loading routine was cancelled")
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Music loading process failed due to something we have not handled.
|
// Music loading process failed due to something we have not handled.
|
||||||
|
// TODO: Still want to display this error eventually
|
||||||
logE("Music indexing failed")
|
logE("Music indexing failed")
|
||||||
logE(e.stackTraceToString())
|
logE(e.stackTraceToString())
|
||||||
emitIndexingCompletion(e)
|
emitIndexingCompletion(e)
|
||||||
|
@ -359,20 +356,37 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) {
|
private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) {
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
// Make sure we have permissions before going forward. Theoretically this would be better
|
||||||
|
// done at the UI level, but that intertwines logic and display too much.
|
||||||
if (ContextCompat.checkSelfPermission(worker.context, PERMISSION_READ_AUDIO) ==
|
if (ContextCompat.checkSelfPermission(worker.context, PERMISSION_READ_AUDIO) ==
|
||||||
PackageManager.PERMISSION_DENIED) {
|
PackageManager.PERMISSION_DENIED) {
|
||||||
logE("Permissions were not granted")
|
logE("Permissions were not granted")
|
||||||
// No permissions, signal that we can't do anything.
|
|
||||||
throw NoAudioPermissionException()
|
throw NoAudioPermissionException()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start initializing the extractors. Use an indeterminate state, as there is no ETA on
|
// Begin with querying MediaStore and the music cache. The former is needed for Auxio
|
||||||
// how long a media database query will take.
|
// to figure out what songs are (probably) on the device, and the latter will be needed
|
||||||
emitIndexingProgress(IndexingProgress.Indeterminate)
|
// for discovery (described later). These have no shared state, so they are done in
|
||||||
|
// parallel.
|
||||||
// Do the initial query of the cache and media databases in parallel.
|
|
||||||
logD("Starting MediaStore query")
|
logD("Starting MediaStore query")
|
||||||
val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() }
|
emitIndexingProgress(IndexingProgress.Indeterminate)
|
||||||
|
val mediaStoreQueryJob =
|
||||||
|
worker.scope.async {
|
||||||
|
val query =
|
||||||
|
try {
|
||||||
|
mediaStoreExtractor.query()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Normally, errors in an async call immediately bubble up to the Looper
|
||||||
|
// and crash the app. Thus, we have to wrap any error into a Result
|
||||||
|
// and then manually forward it to the try block that indexImpl is
|
||||||
|
// called from.
|
||||||
|
return@async Result.failure(e)
|
||||||
|
}
|
||||||
|
Result.success(query)
|
||||||
|
}
|
||||||
|
// Since this main thread is a co-routine, we can do operations in parallel in a way
|
||||||
|
// identical to calling async.
|
||||||
val cache =
|
val cache =
|
||||||
if (withCache) {
|
if (withCache) {
|
||||||
logD("Reading cache")
|
logD("Reading cache")
|
||||||
|
@ -383,59 +397,121 @@ constructor(
|
||||||
logD("Awaiting MediaStore query")
|
logD("Awaiting MediaStore query")
|
||||||
val query = mediaStoreQueryJob.await().getOrThrow()
|
val query = mediaStoreQueryJob.await().getOrThrow()
|
||||||
|
|
||||||
// Now start processing the queried song information in parallel. Songs that can't be
|
// We now have all the information required to start the "discovery" process. This
|
||||||
// received from the cache are consisted incomplete and pushed to a separate channel
|
// is the point at which Auxio starts scanning each file given from MediaStore and
|
||||||
// that will eventually be processed into completed raw songs.
|
// transforming it into a music library. MediaStore normally
|
||||||
logD("Starting song discovery")
|
logD("Starting discovery")
|
||||||
val completeSongs = Channel<RawSong>(Channel.UNLIMITED)
|
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED) // Not fully populated w/metadata
|
||||||
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED)
|
val completeSongs = Channel<RawSong>(Channel.UNLIMITED) // Populated with quality metadata
|
||||||
val processedSongs = Channel<RawSong>(Channel.UNLIMITED)
|
val processedSongs = Channel<RawSong>(Channel.UNLIMITED) // Transformed into SongImpl
|
||||||
logD("Started MediaStore discovery")
|
|
||||||
|
// MediaStoreExtractor discovers all music on the device, and forwards them to either
|
||||||
|
// DeviceLibrary if cached metadata exists for it, or TagExtractor if cached metadata
|
||||||
|
// does not exist. In the latter situation, it also applies it's own (inferior) metadata.
|
||||||
|
logD("Starting MediaStore discovery")
|
||||||
val mediaStoreJob =
|
val mediaStoreJob =
|
||||||
worker.scope.tryAsync {
|
worker.scope.async {
|
||||||
|
try {
|
||||||
mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs)
|
mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// To prevent a deadlock, we want to close the channel with an exception
|
||||||
|
// to cascade to and cancel all other routines before finally bubbling up
|
||||||
|
// to the main extractor loop.
|
||||||
|
incompleteSongs.close(e)
|
||||||
|
return@async
|
||||||
|
}
|
||||||
incompleteSongs.close()
|
incompleteSongs.close()
|
||||||
}
|
}
|
||||||
logD("Started ExoPlayer discovery")
|
|
||||||
val metadataJob =
|
// TagExtractor takes the incomplete songs from MediaStoreExtractor, parses up-to-date
|
||||||
worker.scope.tryAsync {
|
// metadata for them, and then forwards it to DeviceLibrary.
|
||||||
|
logD("Starting tag extraction")
|
||||||
|
val tagJob =
|
||||||
|
worker.scope.async {
|
||||||
|
try {
|
||||||
tagExtractor.consume(incompleteSongs, completeSongs)
|
tagExtractor.consume(incompleteSongs, completeSongs)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
completeSongs.close(e)
|
||||||
|
return@async
|
||||||
|
}
|
||||||
completeSongs.close()
|
completeSongs.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeviceLibrary constructs music parent instances as song information is provided,
|
||||||
|
// and then forwards them to the primary loading loop.
|
||||||
logD("Starting DeviceLibrary creation")
|
logD("Starting DeviceLibrary creation")
|
||||||
val deviceLibraryJob =
|
val deviceLibraryJob =
|
||||||
worker.scope.tryAsync(Dispatchers.Default) {
|
worker.scope.async(Dispatchers.Default) {
|
||||||
deviceLibraryFactory.create(completeSongs, processedSongs).also {
|
val deviceLibrary =
|
||||||
processedSongs.close()
|
try {
|
||||||
|
deviceLibraryFactory.create(completeSongs, processedSongs)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
processedSongs.close(e)
|
||||||
|
return@async Result.failure(e)
|
||||||
}
|
}
|
||||||
|
processedSongs.close()
|
||||||
|
Result.success(deviceLibrary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Await completed raw songs as they are processed.
|
// We could keep track of a total here, but we also need to collate this RawSong information
|
||||||
|
// for when we write the cache later on in the finalization step.
|
||||||
val rawSongs = LinkedList<RawSong>()
|
val rawSongs = LinkedList<RawSong>()
|
||||||
for (rawSong in processedSongs) {
|
for (rawSong in processedSongs) {
|
||||||
rawSongs.add(rawSong)
|
rawSongs.add(rawSong)
|
||||||
|
// Since discovery takes up the bulk of the music loading process, we switch to
|
||||||
|
// indicating a defined amount of loaded songs in comparison to the projected amount
|
||||||
|
// of songs that were queried.
|
||||||
emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
|
emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
|
||||||
}
|
}
|
||||||
logD("Awaiting discovery completion")
|
|
||||||
// These should be no-ops, but we need the error state to see if we should keep going.
|
|
||||||
mediaStoreJob.await().getOrThrow()
|
|
||||||
metadataJob.await().getOrThrow()
|
|
||||||
|
|
||||||
|
// This shouldn't occur, but keep them around just in case there's a regression.
|
||||||
|
// Note that DeviceLibrary might still actually be doing work (specifically parent
|
||||||
|
// processing), so we don't check if it's deadlocked.
|
||||||
|
check(!mediaStoreJob.isActive) { "MediaStore discovery is deadlocked" }
|
||||||
|
check(!tagJob.isActive) { "Tag extraction is deadlocked" }
|
||||||
|
|
||||||
|
// Deliberately done after the involved initialization step to make it less likely
|
||||||
|
// that the short-circuit occurs so quickly as to break the UI.
|
||||||
|
// TODO: Do not error, instead just wipe the entire library.
|
||||||
if (rawSongs.isEmpty()) {
|
if (rawSongs.isEmpty()) {
|
||||||
logE("Music library was empty")
|
logE("Music library was empty")
|
||||||
throw NoMusicException()
|
throw NoMusicException()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successfully loaded the library, now save the cache and read playlist information
|
// Now that the library is effectively loaded, we can start the finalization step, which
|
||||||
// in parallel.
|
// involves writing new cache information and creating more music data that is derived
|
||||||
|
// from the library (e.g playlists)
|
||||||
logD("Discovered ${rawSongs.size} songs, starting finalization")
|
logD("Discovered ${rawSongs.size} songs, starting finalization")
|
||||||
|
|
||||||
|
// We have no idea how long the cache will take, and the playlist construction
|
||||||
|
// will be too fast to indicate, so switch back to an indeterminate state.
|
||||||
emitIndexingProgress(IndexingProgress.Indeterminate)
|
emitIndexingProgress(IndexingProgress.Indeterminate)
|
||||||
|
|
||||||
|
// The UserLibrary job is split into a query and construction step, a la MediaStore.
|
||||||
|
// This way, we can start working on playlists even as DeviceLibrary might still be
|
||||||
|
// working on parent information.
|
||||||
logD("Starting UserLibrary query")
|
logD("Starting UserLibrary query")
|
||||||
val userLibraryQueryJob = worker.scope.tryAsync { userLibraryFactory.query() }
|
val userLibraryQueryJob =
|
||||||
|
worker.scope.async {
|
||||||
|
val rawPlaylists =
|
||||||
|
try {
|
||||||
|
userLibraryFactory.query()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return@async Result.failure(e)
|
||||||
|
}
|
||||||
|
Result.success(rawPlaylists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cache might not exist, or we might have encountered a song not present in it.
|
||||||
|
// Both situations require us to rewrite the cache in bulk. This is also done parallel
|
||||||
|
// since the playlist read will probably take some time.
|
||||||
|
// TODO: Read/write from the cache incrementally instead of in bulk?
|
||||||
if (cache == null || cache.invalidated) {
|
if (cache == null || cache.invalidated) {
|
||||||
logD("Writing cache [why=${cache?.invalidated}]")
|
logD("Writing cache [why=${cache?.invalidated}]")
|
||||||
cacheRepository.writeCache(rawSongs)
|
cacheRepository.writeCache(rawSongs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create UserLibrary once we finally get the required components for it.
|
||||||
logD("Awaiting UserLibrary query")
|
logD("Awaiting UserLibrary query")
|
||||||
val rawPlaylists = userLibraryQueryJob.await().getOrThrow()
|
val rawPlaylists = userLibraryQueryJob.await().getOrThrow()
|
||||||
logD("Awaiting DeviceLibrary creation")
|
logD("Awaiting DeviceLibrary creation")
|
||||||
|
@ -443,14 +519,20 @@ constructor(
|
||||||
logD("Starting UserLibrary creation")
|
logD("Starting UserLibrary creation")
|
||||||
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary)
|
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary)
|
||||||
|
|
||||||
logD("Successfully indexed music library [device=$deviceLibrary user=$userLibrary]")
|
// Loading process is functionally done, indicate such
|
||||||
|
logD(
|
||||||
|
"Successfully indexed music library [device=$deviceLibrary " +
|
||||||
|
"user=$userLibrary time=${System.currentTimeMillis() - start}]")
|
||||||
emitIndexingCompletion(null)
|
emitIndexingCompletion(null)
|
||||||
|
|
||||||
// Comparing the library instances is obscenely expensive, do it within the library
|
|
||||||
|
|
||||||
val deviceLibraryChanged: Boolean
|
val deviceLibraryChanged: Boolean
|
||||||
val userLibraryChanged: Boolean
|
val userLibraryChanged: Boolean
|
||||||
|
// We want to make sure that all reads and writes are synchronized due to the sheer
|
||||||
|
// amount of consumers of MusicRepository.
|
||||||
|
// TODO: Would Atomics not be a better fit here?
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
|
// It's possible that this reload might have changed nothing, so make sure that
|
||||||
|
// hasn't happened before dispatching a change to all consumers.
|
||||||
deviceLibraryChanged = this.deviceLibrary != deviceLibrary
|
deviceLibraryChanged = this.deviceLibrary != deviceLibrary
|
||||||
userLibraryChanged = this.userLibrary != userLibrary
|
userLibraryChanged = this.userLibrary != userLibrary
|
||||||
if (!deviceLibraryChanged && !userLibraryChanged) {
|
if (!deviceLibraryChanged && !userLibraryChanged) {
|
||||||
|
@ -462,27 +544,13 @@ constructor(
|
||||||
this.userLibrary = userLibrary
|
this.userLibrary = userLibrary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consumers expect their updates to be on the main thread (notably PlaybackService),
|
||||||
|
// so switch to it.
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
dispatchLibraryChange(deviceLibraryChanged, userLibraryChanged)
|
dispatchLibraryChange(deviceLibraryChanged, userLibraryChanged)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* An extension of [async] that forces the outcome to a [Result] to allow exceptions to bubble
|
|
||||||
* upwards instead of crashing the entire app.
|
|
||||||
*/
|
|
||||||
private inline fun <R> CoroutineScope.tryAsync(
|
|
||||||
context: CoroutineContext = EmptyCoroutineContext,
|
|
||||||
crossinline block: suspend () -> R
|
|
||||||
) =
|
|
||||||
async(context) {
|
|
||||||
try {
|
|
||||||
Result.success(block())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.failure(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun emitIndexingProgress(progress: IndexingProgress) {
|
private suspend fun emitIndexingProgress(progress: IndexingProgress) {
|
||||||
yield()
|
yield()
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
|
|
|
@ -151,6 +151,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
||||||
rawArtists =
|
rawArtists =
|
||||||
rawAlbumArtists
|
rawAlbumArtists
|
||||||
.ifEmpty { rawIndividualArtists }
|
.ifEmpty { rawIndividualArtists }
|
||||||
|
.distinctBy { it.key }
|
||||||
.ifEmpty { listOf(RawArtist(null, null)) })
|
.ifEmpty { listOf(RawArtist(null, null)) })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -159,7 +160,10 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
||||||
* [RawArtist]. This can be used to group up [Song]s into an [Artist].
|
* [RawArtist]. This can be used to group up [Song]s into an [Artist].
|
||||||
*/
|
*/
|
||||||
val rawArtists =
|
val rawArtists =
|
||||||
rawIndividualArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(RawArtist()) }
|
rawIndividualArtists
|
||||||
|
.ifEmpty { rawAlbumArtists }
|
||||||
|
.distinctBy { it.key }
|
||||||
|
.ifEmpty { listOf(RawArtist()) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [RawGenre] instances collated by the [Song]. This can be used to group up [Song]s into a
|
* The [RawGenre] instances collated by the [Song]. This can be used to group up [Song]s into a
|
||||||
|
@ -169,6 +173,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
||||||
rawSong.genreNames
|
rawSong.genreNames
|
||||||
.parseId3GenreNames(musicSettings)
|
.parseId3GenreNames(musicSettings)
|
||||||
.map { RawGenre(it) }
|
.map { RawGenre(it) }
|
||||||
|
.distinctBy { it.key }
|
||||||
.ifEmpty { listOf(RawGenre()) }
|
.ifEmpty { listOf(RawGenre()) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -207,6 +212,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
||||||
checkNotNull(_album) { "Malformed song: No album" }
|
checkNotNull(_album) { "Malformed song: No album" }
|
||||||
|
|
||||||
check(_artists.isNotEmpty()) { "Malformed song: No artists" }
|
check(_artists.isNotEmpty()) { "Malformed song: No artists" }
|
||||||
|
check(_artists.size == rawArtists.size) { "Malformed song: Artist grouping mismatch" }
|
||||||
for (i in _artists.indices) {
|
for (i in _artists.indices) {
|
||||||
// Non-destructively reorder the linked artists so that they align with
|
// Non-destructively reorder the linked artists so that they align with
|
||||||
// the artist ordering within the song metadata.
|
// the artist ordering within the song metadata.
|
||||||
|
@ -217,6 +223,7 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
||||||
}
|
}
|
||||||
|
|
||||||
check(_genres.isNotEmpty()) { "Malformed song: No genres" }
|
check(_genres.isNotEmpty()) { "Malformed song: No genres" }
|
||||||
|
check(_genres.size == rawGenres.size) { "Malformed song: Genre grouping mismatch" }
|
||||||
for (i in _genres.indices) {
|
for (i in _genres.indices) {
|
||||||
// Non-destructively reorder the linked genres so that they align with
|
// Non-destructively reorder the linked genres so that they align with
|
||||||
// the genre ordering within the song metadata.
|
// the genre ordering within the song metadata.
|
||||||
|
@ -334,6 +341,7 @@ class AlbumImpl(
|
||||||
fun finalize(): Album {
|
fun finalize(): Album {
|
||||||
check(songs.isNotEmpty()) { "Malformed album: Empty" }
|
check(songs.isNotEmpty()) { "Malformed album: Empty" }
|
||||||
check(_artists.isNotEmpty()) { "Malformed album: No artists" }
|
check(_artists.isNotEmpty()) { "Malformed album: No artists" }
|
||||||
|
check(_artists.size == rawArtists.size) { "Malformed album: Artist grouping mismatch" }
|
||||||
for (i in _artists.indices) {
|
for (i in _artists.indices) {
|
||||||
// Non-destructively reorder the linked artists so that they align with
|
// Non-destructively reorder the linked artists so that they align with
|
||||||
// the artist ordering within the song metadata.
|
// the artist ordering within the song metadata.
|
||||||
|
|
|
@ -364,7 +364,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
|
||||||
arrayOf(
|
arrayOf(
|
||||||
MediaStore.Audio.AudioColumns.TRACK,
|
MediaStore.Audio.AudioColumns.TRACK,
|
||||||
// Below API 29, we are restricted to the absolute path (Called DATA by
|
// Below API 29, we are restricted to the absolute path (Called DATA by
|
||||||
// MedaStore) when working with audio files.
|
// MediaStore) when working with audio files.
|
||||||
MediaStore.Audio.AudioColumns.DATA)
|
MediaStore.Audio.AudioColumns.DATA)
|
||||||
|
|
||||||
// The selector should be configured to convert the given directories instances to their
|
// The selector should be configured to convert the given directories instances to their
|
||||||
|
|
|
@ -149,18 +149,22 @@ private class TagWorkerImpl(
|
||||||
// Artist
|
// Artist
|
||||||
textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it }
|
textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it }
|
||||||
(textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it }
|
(textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it }
|
||||||
(textFrames["TXXX:artistssort"] ?: textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])
|
(textFrames["TXXX:artistssort"]
|
||||||
|
?: textFrames["TXXX:artists_sort"] ?: textFrames["TXXX:artists sort"]
|
||||||
|
?: textFrames["TSOP"])
|
||||||
?.let { rawSong.artistSortNames = it }
|
?.let { rawSong.artistSortNames = it }
|
||||||
|
|
||||||
// Album artist
|
// Album artist
|
||||||
textFrames["TXXX:musicbrainz album artist id"]?.let {
|
textFrames["TXXX:musicbrainz album artist id"]?.let {
|
||||||
rawSong.albumArtistMusicBrainzIds = it
|
rawSong.albumArtistMusicBrainzIds = it
|
||||||
}
|
}
|
||||||
(textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let {
|
(textFrames["TXXX:albumartists"]
|
||||||
rawSong.albumArtistNames = it
|
?: textFrames["TXXX:album_artists"] ?: textFrames["TXXX:album artists"]
|
||||||
}
|
?: textFrames["TPE2"])
|
||||||
|
?.let { rawSong.albumArtistNames = it }
|
||||||
(textFrames["TXXX:albumartistssort"]
|
(textFrames["TXXX:albumartistssort"]
|
||||||
?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartistsort"]
|
?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartists sort"]
|
||||||
|
?: textFrames["TXXX:albumartistsort"]
|
||||||
// This is a non-standard iTunes extension
|
// This is a non-standard iTunes extension
|
||||||
?: textFrames["TSO2"])
|
?: textFrames["TSO2"])
|
||||||
?.let { rawSong.albumArtistSortNames = it }
|
?.let { rawSong.albumArtistSortNames = it }
|
||||||
|
@ -261,17 +265,19 @@ private class TagWorkerImpl(
|
||||||
// Artist
|
// Artist
|
||||||
comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it }
|
comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it }
|
||||||
(comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it }
|
(comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it }
|
||||||
(comments["artistssort"] ?: comments["artists_sort"] ?: comments["artistsort"])?.let {
|
(comments["artistssort"]
|
||||||
rawSong.artistSortNames = it
|
?: comments["artists_sort"] ?: comments["artists sort"] ?: comments["artistsort"])
|
||||||
}
|
?.let { rawSong.artistSortNames = it }
|
||||||
|
|
||||||
// Album artist
|
// Album artist
|
||||||
comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it }
|
comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it }
|
||||||
(comments["albumartists"] ?: comments["album_artists"] ?: comments["albumartist"])?.let {
|
(comments["albumartists"]
|
||||||
rawSong.albumArtistNames = it
|
?: comments["album_artists"] ?: comments["album artists"]
|
||||||
}
|
?: comments["albumartist"])
|
||||||
|
?.let { rawSong.albumArtistNames = it }
|
||||||
(comments["albumartistssort"]
|
(comments["albumartistssort"]
|
||||||
?: comments["albumartists_sort"] ?: comments["albumartistsort"])
|
?: comments["albumartists_sort"] ?: comments["albumartists sort"]
|
||||||
|
?: comments["albumartistsort"])
|
||||||
?.let { rawSong.albumArtistSortNames = it }
|
?.let { rawSong.albumArtistSortNames = it }
|
||||||
|
|
||||||
// Genre
|
// Genre
|
||||||
|
|
|
@ -39,6 +39,7 @@ import org.oxycblt.auxio.util.logE
|
||||||
* @author Alexander Capehart
|
* @author Alexander Capehart
|
||||||
*
|
*
|
||||||
* TODO: Communicate errors
|
* TODO: Communicate errors
|
||||||
|
* TODO: How to handle empty playlists that appear because all of their songs have disappeared?
|
||||||
*/
|
*/
|
||||||
interface UserLibrary {
|
interface UserLibrary {
|
||||||
/** The current user-defined playlists. */
|
/** The current user-defined playlists. */
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.audiofx.AudioEffect
|
import android.media.audiofx.AudioEffect
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.media3.common.AudioAttributes
|
import androidx.media3.common.AudioAttributes
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
|
@ -150,8 +151,8 @@ class PlaybackService :
|
||||||
playbackManager.registerInternalPlayer(this)
|
playbackManager.registerInternalPlayer(this)
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
mediaSessionComponent.registerListener(this)
|
mediaSessionComponent.registerListener(this)
|
||||||
registerReceiver(
|
|
||||||
systemReceiver,
|
val intentFilter =
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
|
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
|
||||||
addAction(AudioManager.ACTION_HEADSET_PLUG)
|
addAction(AudioManager.ACTION_HEADSET_PLUG)
|
||||||
|
@ -162,7 +163,20 @@ class PlaybackService :
|
||||||
addAction(ACTION_SKIP_NEXT)
|
addAction(ACTION_SKIP_NEXT)
|
||||||
addAction(ACTION_EXIT)
|
addAction(ACTION_EXIT)
|
||||||
addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
|
addAction(WidgetProvider.ACTION_WIDGET_UPDATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
registerReceiver(
|
||||||
|
systemReceiver,
|
||||||
|
intentFilter,
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
RECEIVER_NOT_EXPORTED
|
||||||
|
} else {
|
||||||
|
0
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
registerReceiver(systemReceiver, intentFilter)
|
||||||
|
}
|
||||||
|
|
||||||
logD("Service created")
|
logD("Service created")
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,11 +94,11 @@ interface Settings<L> {
|
||||||
|
|
||||||
final override fun onSharedPreferenceChanged(
|
final override fun onSharedPreferenceChanged(
|
||||||
sharedPreferences: SharedPreferences,
|
sharedPreferences: SharedPreferences,
|
||||||
key: String
|
key: String?
|
||||||
) {
|
) {
|
||||||
// FIXME: Settings initialization firing the listener.
|
// FIXME: Settings initialization firing the listener.
|
||||||
logD("Dispatching settings change $key")
|
logD("Dispatching settings change $key")
|
||||||
onSettingChanged(key, unlikelyToBeNull(listener))
|
onSettingChanged(unlikelyToBeNull(key), unlikelyToBeNull(listener))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -47,7 +47,8 @@ class MusicPreferenceFragment : BasePreferenceFragment(R.xml.preferences_music)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSetupPreference(preference: Preference) {
|
override fun onSetupPreference(preference: Preference) {
|
||||||
if (preference.key == getString(R.string.set_key_cover_mode)) {
|
if (preference.key == getString(R.string.set_key_cover_mode) ||
|
||||||
|
preference.key == getString(R.string.set_key_square_covers)) {
|
||||||
logD("Configuring cover mode setting")
|
logD("Configuring cover mode setting")
|
||||||
preference.onPreferenceChangeListener =
|
preference.onPreferenceChangeListener =
|
||||||
Preference.OnPreferenceChangeListener { _, _ ->
|
Preference.OnPreferenceChangeListener { _, _ ->
|
||||||
|
|
|
@ -26,11 +26,11 @@ import androidx.preference.PreferenceGroupAdapter
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.R
|
import com.google.android.material.R
|
||||||
import com.google.android.material.divider.BackportMaterialDividerItemDecoration
|
import com.google.android.material.divider.MaterialDividerItemDecoration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly
|
* A [MaterialDividerItemDecoration] that sets up the divider configuration to correctly separate
|
||||||
* separate preference categories.
|
* preference categories.
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -41,7 +41,7 @@ constructor(
|
||||||
attributeSet: AttributeSet? = null,
|
attributeSet: AttributeSet? = null,
|
||||||
defStyleAttr: Int = R.attr.materialDividerStyle,
|
defStyleAttr: Int = R.attr.materialDividerStyle,
|
||||||
orientation: Int = LinearLayoutManager.VERTICAL
|
orientation: Int = LinearLayoutManager.VERTICAL
|
||||||
) : BackportMaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) {
|
) : MaterialDividerItemDecoration(context, attributeSet, defStyleAttr, orientation) {
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?) =
|
override fun shouldDrawDivider(position: Int, adapter: RecyclerView.Adapter<*>?) =
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -27,7 +27,8 @@ import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.image.BitmapProvider
|
import org.oxycblt.auxio.image.BitmapProvider
|
||||||
import org.oxycblt.auxio.image.ImageSettings
|
import org.oxycblt.auxio.image.ImageSettings
|
||||||
import org.oxycblt.auxio.image.RoundedCornersTransformation
|
import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
|
||||||
|
import org.oxycblt.auxio.image.extractor.SquareCropTransformation
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.queue.Queue
|
import org.oxycblt.auxio.playback.queue.Queue
|
||||||
|
@ -98,10 +99,19 @@ constructor(
|
||||||
return if (cornerRadius > 0) {
|
return if (cornerRadius > 0) {
|
||||||
// If rounded, reduce the bitmap size further to obtain more pronounced
|
// If rounded, reduce the bitmap size further to obtain more pronounced
|
||||||
// rounded corners.
|
// rounded corners.
|
||||||
builder
|
builder.size(getSafeRemoteViewsImageSize(context, 10f))
|
||||||
.size(getSafeRemoteViewsImageSize(context, 10f))
|
val cornersTransformation =
|
||||||
.transformations(RoundedCornersTransformation(cornerRadius.toFloat()))
|
RoundedRectTransformation(cornerRadius.toFloat())
|
||||||
|
if (imageSettings.forceSquareCovers) {
|
||||||
|
builder.transformations(
|
||||||
|
SquareCropTransformation.INSTANCE, cornersTransformation)
|
||||||
} else {
|
} else {
|
||||||
|
builder.transformations(cornersTransformation)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (imageSettings.forceSquareCovers) {
|
||||||
|
builder.transformations(SquareCropTransformation.INSTANCE)
|
||||||
|
}
|
||||||
builder.size(getSafeRemoteViewsImageSize(context))
|
builder.size(getSafeRemoteViewsImageSize(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:tint="?attr/colorControlNormal"
|
android:viewportWidth="960"
|
||||||
android:viewportWidth="24"
|
android:viewportHeight="960"
|
||||||
android:viewportHeight="24">
|
android:tint="?attr/colorControlNormal">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M9.175,10.575 L4,5.4 5.4,4 10.575,9.175ZM14,20V18H16.6L13.425,14.825L14.85,13.4L18,16.55V14H20V20ZM5.4,20 L4,18.6 16.6,6H14V4H20V10H18V7.4Z" />
|
android:pathData="M560,800L560,720L664,720L537,593L594,536L720,662L720,560L800,560L800,800L560,800ZM216,800L160,744L664,240L560,240L560,160L800,160L800,400L720,400L720,296L216,800ZM367,423L160,216L216,160L423,367L367,423Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:tint="?attr/colorPrimary"
|
android:viewportWidth="960"
|
||||||
android:viewportWidth="24"
|
android:viewportHeight="960"
|
||||||
android:viewportHeight="24">
|
android:tint="?attr/colorPrimary">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M9.05,10.775 L3.675,5.4 5.45,3.625 10.825,9ZM13.75,20.275V17.725H16L13.275,14.975L15.025,13.2L17.75,15.925V13.725H20.325V20.275ZM5.45,20.375 L3.675,18.6 16,6.275H13.75V3.725H20.325V10.275H17.75V8.05Z" />
|
android:pathData="M546,817L546,703L627,703L528,603L607,524L706,623L706,543L820,543L820,817L546,817ZM219,823L140,744L627,257L546,257L546,143L820,143L820,417L706,417L706,336L219,823ZM359,435L140,216L219,137L438,356L359,435Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -293,4 +293,6 @@
|
||||||
<string name="def_disc">Няма дыска</string>
|
<string name="def_disc">Няма дыска</string>
|
||||||
<string name="lbl_appears_on">З\'яўляецца на</string>
|
<string name="lbl_appears_on">З\'яўляецца на</string>
|
||||||
<string name="fmt_editing">Рэдагаванне %s</string>
|
<string name="fmt_editing">Рэдагаванне %s</string>
|
||||||
|
<string name="set_square_covers">Выкарыстоўваць квадратныя вокладкі альбомаў</string>
|
||||||
|
<string name="set_square_covers_desc">Абрэзаць усе вокладкі альбомаў да суадносін бакоў 1:1</string>
|
||||||
</resources>
|
</resources>
|
|
@ -55,9 +55,9 @@
|
||||||
<string name="set_root_title">Nastavení</string>
|
<string name="set_root_title">Nastavení</string>
|
||||||
<string name="set_ui">Vzhled a chování</string>
|
<string name="set_ui">Vzhled a chování</string>
|
||||||
<string name="set_theme">Motiv</string>
|
<string name="set_theme">Motiv</string>
|
||||||
<string name="set_theme_auto">Automatické</string>
|
<string name="set_theme_auto">Automaticky</string>
|
||||||
<string name="set_theme_day">Světlé</string>
|
<string name="set_theme_day">Světlý</string>
|
||||||
<string name="set_theme_night">Tmavé</string>
|
<string name="set_theme_night">Tmavý</string>
|
||||||
<string name="set_accent">Barevné schéma</string>
|
<string name="set_accent">Barevné schéma</string>
|
||||||
<string name="set_black_mode">Černý motiv</string>
|
<string name="set_black_mode">Černý motiv</string>
|
||||||
<string name="set_black_mode_desc">Použít kompletně černý tmavý motiv</string>
|
<string name="set_black_mode_desc">Použít kompletně černý tmavý motiv</string>
|
||||||
|
@ -304,4 +304,6 @@
|
||||||
<string name="def_disc">Žádný disk</string>
|
<string name="def_disc">Žádný disk</string>
|
||||||
<string name="fmt_editing">Úprava seznamu %s</string>
|
<string name="fmt_editing">Úprava seznamu %s</string>
|
||||||
<string name="lbl_share">Sdílet</string>
|
<string name="lbl_share">Sdílet</string>
|
||||||
|
<string name="set_square_covers">Vynutit čtvercové obaly alb</string>
|
||||||
|
<string name="set_square_covers_desc">Oříznout všechny covery alb na poměr stran 1:1</string>
|
||||||
</resources>
|
</resources>
|
|
@ -295,4 +295,6 @@
|
||||||
<string name="def_disc">Keine Disc</string>
|
<string name="def_disc">Keine Disc</string>
|
||||||
<string name="lbl_appears_on">Erscheint in</string>
|
<string name="lbl_appears_on">Erscheint in</string>
|
||||||
<string name="fmt_editing">%s bearbeiten</string>
|
<string name="fmt_editing">%s bearbeiten</string>
|
||||||
|
<string name="set_square_covers">Quadratische Album-Cover erzwingen</string>
|
||||||
|
<string name="set_square_covers_desc">Alle Album-Cover auf ein Seitenverhältnis von 1:1 zuschneiden</string>
|
||||||
</resources>
|
</resources>
|
|
@ -299,4 +299,6 @@
|
||||||
<string name="lbl_appears_on">Aparece en</string>
|
<string name="lbl_appears_on">Aparece en</string>
|
||||||
<string name="lbl_share">Compartir</string>
|
<string name="lbl_share">Compartir</string>
|
||||||
<string name="def_disc">Sin disco</string>
|
<string name="def_disc">Sin disco</string>
|
||||||
|
<string name="set_square_covers">Carátula del álbum Force Square</string>
|
||||||
|
<string name="set_square_covers_desc">Recorta todas las portadas de los álbumes a una relación de aspecto 1:1</string>
|
||||||
</resources>
|
</resources>
|
|
@ -297,4 +297,6 @@
|
||||||
<string name="cdc_aac">Codage audio avancé (AAC)</string>
|
<string name="cdc_aac">Codage audio avancé (AAC)</string>
|
||||||
<string name="def_disc">Aucun disque</string>
|
<string name="def_disc">Aucun disque</string>
|
||||||
<string name="fmt_list">%1$s, %2$s</string>
|
<string name="fmt_list">%1$s, %2$s</string>
|
||||||
|
<string name="set_square_covers">Forcer les pochettes d\'album carrées</string>
|
||||||
|
<string name="set_square_covers_desc">Recadrer toutes les pochettes d\'album au format 1:1</string>
|
||||||
</resources>
|
</resources>
|
|
@ -290,4 +290,6 @@
|
||||||
<string name="lbl_appears_on">Sudjelovanja:</string>
|
<string name="lbl_appears_on">Sudjelovanja:</string>
|
||||||
<string name="lbl_share">Dijeli</string>
|
<string name="lbl_share">Dijeli</string>
|
||||||
<string name="def_disc">Nema diska</string>
|
<string name="def_disc">Nema diska</string>
|
||||||
|
<string name="set_square_covers">Prisili kvadratične omote albuma</string>
|
||||||
|
<string name="set_square_covers_desc">Odreži sve omote albuma na omjer 1:1</string>
|
||||||
</resources>
|
</resources>
|
|
@ -180,7 +180,7 @@
|
||||||
<string name="set_separators_comma">ਕੌਮਾ (,)</string>
|
<string name="set_separators_comma">ਕੌਮਾ (,)</string>
|
||||||
<string name="set_separators_semicolon">ਸੈਮੀਕੋਲਨ (;)</string>
|
<string name="set_separators_semicolon">ਸੈਮੀਕੋਲਨ (;)</string>
|
||||||
<string name="set_separators_slash">ਸਲੈਸ਼ (/)</string>
|
<string name="set_separators_slash">ਸਲੈਸ਼ (/)</string>
|
||||||
<string name="set_separators_and">Ampersand (&)</string>
|
<string name="set_separators_and">ਐਂਪਰਸੈਂਡ (&)</string>
|
||||||
<string name="set_hide_collaborators">ਸਹਿਯੋਗੀਆਂ ਨੂੰ ਲੁਕਾਓ</string>
|
<string name="set_hide_collaborators">ਸਹਿਯੋਗੀਆਂ ਨੂੰ ਲੁਕਾਓ</string>
|
||||||
<string name="set_audio_desc">ਆਵਾਜ਼ ਅਤੇ ਪਲੇਬੈਕ ਵਿਵਹਾਰ ਦੀ ਸੰਰਚਨਾ ਕਰੋ</string>
|
<string name="set_audio_desc">ਆਵਾਜ਼ ਅਤੇ ਪਲੇਬੈਕ ਵਿਵਹਾਰ ਦੀ ਸੰਰਚਨਾ ਕਰੋ</string>
|
||||||
<string name="set_playback">ਪਲੇਅਬੈਕ</string>
|
<string name="set_playback">ਪਲੇਅਬੈਕ</string>
|
||||||
|
@ -287,4 +287,6 @@
|
||||||
<string name="clr_dynamic">ਡਾਇਨੈਮਿਕ</string>
|
<string name="clr_dynamic">ਡਾਇਨੈਮਿਕ</string>
|
||||||
<string name="fmt_editing">%s ਸੋਧ ਰਿਹਾ</string>
|
<string name="fmt_editing">%s ਸੋਧ ਰਿਹਾ</string>
|
||||||
<string name="fmt_indexing">ਤੁਹਾਡੀ ਸੰਗੀਤ ਲਾਇਬਰੇਰੀ ਲੋਡ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ… (%1$d/%2$d)</string>
|
<string name="fmt_indexing">ਤੁਹਾਡੀ ਸੰਗੀਤ ਲਾਇਬਰੇਰੀ ਲੋਡ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ… (%1$d/%2$d)</string>
|
||||||
|
<string name="set_square_covers">ਵਰਗੀਕ੍ਰਿਤ ਐਲਬਮ ਕਵਰ ਫੋਰਸ ਕਰੋ</string>
|
||||||
|
<string name="set_square_covers_desc">ਸਾਰੇ ਐਲਬਮ ਕਵਰਾਂ ਨੂੰ 1:1 ਦੇ ਆਕਾਰ ਅਨੁਪਾਤ ਤੱਕ ਕਾਂਟ-ਛਾਂਟ ਕਰੋ</string>
|
||||||
</resources>
|
</resources>
|
|
@ -40,7 +40,7 @@
|
||||||
<string name="err_no_music">Nie znaleziono utworów</string>
|
<string name="err_no_music">Nie znaleziono utworów</string>
|
||||||
<!-- Description Namespace | Accessibility Strings -->
|
<!-- Description Namespace | Accessibility Strings -->
|
||||||
<string name="desc_track_number">Utwór %d</string>
|
<string name="desc_track_number">Utwór %d</string>
|
||||||
<string name="desc_play_pause">Odtwórz bądź zapauzuj</string>
|
<string name="desc_play_pause">Odtwórz albo zapauzuj</string>
|
||||||
<!-- Hint Namespace | EditText Hints -->
|
<!-- Hint Namespace | EditText Hints -->
|
||||||
<string name="lng_search_library">Szukaj w bibliotece…</string>
|
<string name="lng_search_library">Szukaj w bibliotece…</string>
|
||||||
<!-- Color Label namespace | Accent names -->
|
<!-- Color Label namespace | Accent names -->
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
<string name="lbl_ep_live">Minialbum koncertowy</string>
|
<string name="lbl_ep_live">Minialbum koncertowy</string>
|
||||||
<string name="lbl_ep_remix">Minialbum z remiksami</string>
|
<string name="lbl_ep_remix">Minialbum z remiksami</string>
|
||||||
<string name="lbl_single_live">Koncertowy singiel</string>
|
<string name="lbl_single_live">Koncertowy singiel</string>
|
||||||
<string name="lbl_single_remix">Remiks</string>
|
<string name="lbl_single_remix">Remix</string>
|
||||||
<string name="lbl_compilations">Kompilacje</string>
|
<string name="lbl_compilations">Kompilacje</string>
|
||||||
<string name="lbl_compilation">Kompilacja</string>
|
<string name="lbl_compilation">Kompilacja</string>
|
||||||
<string name="lbl_soundtracks">Ścieżki dźwiękowe</string>
|
<string name="lbl_soundtracks">Ścieżki dźwiękowe</string>
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
<string name="set_reindex_desc">Odśwież bibliotekę muzyczną używając tagów z pamięci cache, jeśli są dostępne</string>
|
<string name="set_reindex_desc">Odśwież bibliotekę muzyczną używając tagów z pamięci cache, jeśli są dostępne</string>
|
||||||
<string name="desc_remove_song">Usuń utwór z kolejki</string>
|
<string name="desc_remove_song">Usuń utwór z kolejki</string>
|
||||||
<string name="set_replay_gain_mode_album">Preferuj album</string>
|
<string name="set_replay_gain_mode_album">Preferuj album</string>
|
||||||
<string name="set_observing">Automatycznie odśwież</string>
|
<string name="set_observing">Automatyczne odświeżanie</string>
|
||||||
<string name="cdc_flac">FLAC</string>
|
<string name="cdc_flac">FLAC</string>
|
||||||
<string name="set_separators_and">Et (&)</string>
|
<string name="set_separators_and">Et (&)</string>
|
||||||
<string name="err_index_failed">Nie udało się zaimportować utworów</string>
|
<string name="err_index_failed">Nie udało się zaimportować utworów</string>
|
||||||
|
@ -243,7 +243,7 @@
|
||||||
<string name="set_cover_mode_off">Wyłączone</string>
|
<string name="set_cover_mode_off">Wyłączone</string>
|
||||||
<string name="set_cover_mode_media_store">Niska jakość</string>
|
<string name="set_cover_mode_media_store">Niska jakość</string>
|
||||||
<string name="set_cover_mode_quality">Wysoka jakość</string>
|
<string name="set_cover_mode_quality">Wysoka jakość</string>
|
||||||
<string name="set_separators_warning">Uwaga: To ustawienie może powodować nieprawidłowe przetwarzenie tagów - tak, jakby posiadały wiele wartości. Problem ten należy rozwiązać stawiając ukośnik wsteczny (\\) przed niepożądanymi znakami traktowanymi jako oddzielające.</string>
|
<string name="set_separators_warning">Uwaga: To ustawienie może powodować nieprawidłowe przetwarzenie tagów (tak, jakby posiadały wiele wartości). Problem ten należy rozwiązać stawiając ukośnik wsteczny (\\) przed niepożądanymi znakami oddzielającymi.</string>
|
||||||
<string name="set_ui_desc">Dostosuj motyw i kolory aplikacji</string>
|
<string name="set_ui_desc">Dostosuj motyw i kolory aplikacji</string>
|
||||||
<string name="set_hide_collaborators">Ukryj wykonawców uczestniczących</string>
|
<string name="set_hide_collaborators">Ukryj wykonawców uczestniczących</string>
|
||||||
<string name="set_content_desc">Zarządzaj importowaniem muzyki i obrazów</string>
|
<string name="set_content_desc">Zarządzaj importowaniem muzyki i obrazów</string>
|
||||||
|
@ -254,7 +254,7 @@
|
||||||
<string name="set_dirs_list">Foldery</string>
|
<string name="set_dirs_list">Foldery</string>
|
||||||
<string name="set_state">Stan odtwarzania</string>
|
<string name="set_state">Stan odtwarzania</string>
|
||||||
<string name="set_images">Obrazy</string>
|
<string name="set_images">Obrazy</string>
|
||||||
<string name="set_audio_desc">Zarządzanie dźwiękiem i odtwarzaniem muzyki</string>
|
<string name="set_audio_desc">Zarządzaj dźwiękiem i odtwarzaniem muzyki</string>
|
||||||
<string name="lbl_play_selected">Odtwórz wybrane</string>
|
<string name="lbl_play_selected">Odtwórz wybrane</string>
|
||||||
<string name="lbl_shuffle_selected">Wybrane losowo</string>
|
<string name="lbl_shuffle_selected">Wybrane losowo</string>
|
||||||
<string name="fmt_selected">Wybrano %d</string>
|
<string name="fmt_selected">Wybrano %d</string>
|
||||||
|
@ -288,16 +288,18 @@
|
||||||
<string name="def_song_count">Brak utworów</string>
|
<string name="def_song_count">Brak utworów</string>
|
||||||
<string name="lng_playlist_added">Dodano do playlisty</string>
|
<string name="lng_playlist_added">Dodano do playlisty</string>
|
||||||
<string name="fmt_def_playlist">Playlista %d</string>
|
<string name="fmt_def_playlist">Playlista %d</string>
|
||||||
<string name="lbl_delete">Usuwać</string>
|
<string name="lbl_delete">Usuń</string>
|
||||||
<string name="fmt_deletion_info">Usunąć %s\? Tego nie da się cofnąć.</string>
|
<string name="fmt_deletion_info">Usunąć %s\? Tego nie da się cofnąć.</string>
|
||||||
<string name="lbl_rename">Przemianować</string>
|
<string name="lbl_rename">Zmień nazwę</string>
|
||||||
<string name="lbl_rename_playlist">Przemianować playlistę</string>
|
<string name="lbl_rename_playlist">Zmień nazwę playlisty</string>
|
||||||
<string name="lbl_confirm_delete_playlist">Usunąć playlistę\?</string>
|
<string name="lbl_confirm_delete_playlist">Usunąć playlistę\?</string>
|
||||||
<string name="lbl_edit">Edytować</string>
|
<string name="lbl_edit">Edytuj</string>
|
||||||
<string name="lbl_appears_on">Pojawia się</string>
|
<string name="lbl_appears_on">Pojawia się na</string>
|
||||||
<string name="lbl_share">Udział</string>
|
<string name="lbl_share">Udostępnij</string>
|
||||||
<string name="lng_playlist_renamed">Zmieniono nazwę playlisty</string>
|
<string name="lng_playlist_renamed">Zmieniono nazwę playlisty</string>
|
||||||
<string name="lng_playlist_deleted">Playlista usunięta</string>
|
<string name="lng_playlist_deleted">Usunięto playlistę</string>
|
||||||
<string name="def_disc">Brak dysku</string>
|
<string name="def_disc">Brak nr. płyty</string>
|
||||||
<string name="fmt_editing">Edytowanie %s</string>
|
<string name="fmt_editing">Edytowanie %s</string>
|
||||||
|
<string name="set_square_covers_desc">Przytnij okładki do formatu 1:1</string>
|
||||||
|
<string name="set_square_covers">Wymuś kwadratowe okładki</string>
|
||||||
</resources>
|
</resources>
|
|
@ -302,4 +302,6 @@
|
||||||
<string name="fmt_editing">Редактирование %s</string>
|
<string name="fmt_editing">Редактирование %s</string>
|
||||||
<string name="def_disc">Нет диска</string>
|
<string name="def_disc">Нет диска</string>
|
||||||
<string name="lbl_share">Поделиться</string>
|
<string name="lbl_share">Поделиться</string>
|
||||||
|
<string name="set_square_covers">Использовать квадратные обложки альбомов</string>
|
||||||
|
<string name="set_square_covers_desc">Обрезать все обложки альбомов до соотношения сторон 1:1</string>
|
||||||
</resources>
|
</resources>
|
156
app/src/main/res/values-sv/strings.xml
Normal file
156
app/src/main/res/values-sv/strings.xml
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="lbl_retry">Försök igen</string>
|
||||||
|
<string name="lbl_indexer">Musik laddar</string>
|
||||||
|
<string name="lbl_indexing">Laddar musik</string>
|
||||||
|
<string name="lbl_all_songs">Alla låtar</string>
|
||||||
|
<string name="lbl_albums">Album</string>
|
||||||
|
<string name="lbl_album">Albumet</string>
|
||||||
|
<string name="lbl_album_remix">Remix-album</string>
|
||||||
|
<string name="lbl_eps">EP</string>
|
||||||
|
<string name="lbl_ep">EP</string>
|
||||||
|
<string name="lbl_ep_live">Live-EP</string>
|
||||||
|
<string name="lbl_ep_remix">Remix-EP</string>
|
||||||
|
<string name="lbl_singles">Singlar</string>
|
||||||
|
<string name="lbl_single_remix">Remix-singel</string>
|
||||||
|
<string name="lbl_compilation">Sammanställning</string>
|
||||||
|
<string name="lbl_compilation_remix">Remix-sammanställning</string>
|
||||||
|
<string name="lbl_soundtracks">Ljudspår</string>
|
||||||
|
<string name="lbl_soundtrack">Ljudspår</string>
|
||||||
|
<string name="lbl_mixtapes">Blandband</string>
|
||||||
|
<string name="lbl_mixes">DJ-mixar</string>
|
||||||
|
<string name="lbl_live_group">Live</string>
|
||||||
|
<string name="lbl_remix_group">Remixar</string>
|
||||||
|
<string name="lbl_appears_on">Framträder på</string>
|
||||||
|
<string name="lbl_artist">Konstnär</string>
|
||||||
|
<string name="lbl_artists">Konstnär</string>
|
||||||
|
<string name="lbl_genres">Genrer</string>
|
||||||
|
<string name="lbl_playlist">Spellista</string>
|
||||||
|
<string name="lbl_playlists">Spellistor</string>
|
||||||
|
<string name="lbl_new_playlist">Ny spellista</string>
|
||||||
|
<string name="lbl_rename_playlist">Byt namn på spellista</string>
|
||||||
|
<string name="lbl_confirm_delete_playlist">Ta bort spellista\?</string>
|
||||||
|
<string name="lbl_search">Sök</string>
|
||||||
|
<string name="lbl_filter">Filtrera</string>
|
||||||
|
<string name="lbl_name">Namn</string>
|
||||||
|
<string name="lbl_date">Datum</string>
|
||||||
|
<string name="lbl_duration">Längd</string>
|
||||||
|
<string name="lbl_song_count">Antal låtar</string>
|
||||||
|
<string name="lbl_track">Spår</string>
|
||||||
|
<string name="lbl_date_added">Datum tillagt</string>
|
||||||
|
<string name="lbl_sort_asc">Stigande</string>
|
||||||
|
<string name="lbl_sort_dec">Fallande</string>
|
||||||
|
<string name="lbl_playback">Nu spelar</string>
|
||||||
|
<string name="lbl_equalizer">Utjämnare</string>
|
||||||
|
<string name="lbl_play">Spela</string>
|
||||||
|
<string name="lbl_play_selected">Spela utvalda</string>
|
||||||
|
<string name="lbl_shuffle">Blanda</string>
|
||||||
|
<string name="lbl_queue">Kö</string>
|
||||||
|
<string name="lbl_play_next">Spela nästa</string>
|
||||||
|
<string name="lbl_playlist_add">Lägg till spellista</string>
|
||||||
|
<string name="lbl_go_artist">Gå till konstnär</string>
|
||||||
|
<string name="lbl_go_album">Gå till album</string>
|
||||||
|
<string name="lbl_song_detail">Visa egenskaper</string>
|
||||||
|
<string name="lbl_share">Dela</string>
|
||||||
|
<string name="lbl_props">Egenskaper för låt</string>
|
||||||
|
<string name="lbl_relative_path">Överordnad mapp</string>
|
||||||
|
<string name="lbl_format">Format</string>
|
||||||
|
<string name="lbl_size">Storlek</string>
|
||||||
|
<string name="lbl_sample_rate">Samplingsfrekvens</string>
|
||||||
|
<string name="lbl_shuffle_shortcut_short">Blanda</string>
|
||||||
|
<string name="lbl_shuffle_shortcut_long">Blanda alla</string>
|
||||||
|
<string name="lbl_ok">Okej</string>
|
||||||
|
<string name="lbl_cancel">Avbryt</string>
|
||||||
|
<string name="lbl_save">Spara</string>
|
||||||
|
<string name="lbl_state_restored">Tillstånd återstallde</string>
|
||||||
|
<string name="lbl_about">Om</string>
|
||||||
|
<string name="lbl_code">Källkod</string>
|
||||||
|
<string name="lbl_wiki">Wiki</string>
|
||||||
|
<string name="lbl_licenses">Licenser</string>
|
||||||
|
<string name="lng_widget">Visa och kontrollera musikuppspelning</string>
|
||||||
|
<string name="lng_indexing">Laddar ditt musikbibliotek…</string>
|
||||||
|
<string name="lng_observing">Övervakning ditt musikbibliotek för ändringar…</string>
|
||||||
|
<string name="lng_queue_added">Tillagd till kö</string>
|
||||||
|
<string name="lng_playlist_created">Spellista skapade</string>
|
||||||
|
<string name="lng_playlist_added">Tillagd till spellista</string>
|
||||||
|
<string name="lng_search_library">Sök ditt musikbibliotek…</string>
|
||||||
|
<string name="set_root_title">Inställningar</string>
|
||||||
|
<string name="set_ui">Utseende</string>
|
||||||
|
<string name="set_ui_desc">Ändra tema och färger på appen</string>
|
||||||
|
<string name="set_theme_auto">Automatisk</string>
|
||||||
|
<string name="set_theme_day">Ljust</string>
|
||||||
|
<string name="set_black_mode">Svart tema</string>
|
||||||
|
<string name="set_round_mode">Rundläge</string>
|
||||||
|
<string name="lbl_grant">Bevilja</string>
|
||||||
|
<string name="info_app_desc">En enkel, rationell musikspelare för Android.</string>
|
||||||
|
<string name="lbl_observing">Övervakar musikbiblioteket</string>
|
||||||
|
<string name="lbl_songs">Låtar</string>
|
||||||
|
<string name="lbl_album_live">Live-album</string>
|
||||||
|
<string name="lbl_delete">Ta bort</string>
|
||||||
|
<string name="lbl_compilation_live">Live-sammanställning</string>
|
||||||
|
<string name="lbl_single">Singel</string>
|
||||||
|
<string name="lbl_single_live">Live-singel</string>
|
||||||
|
<string name="lbl_compilations">Sammanställningar</string>
|
||||||
|
<string name="lbl_mixtape">Blandband</string>
|
||||||
|
<string name="lbl_mix">DJ-mix</string>
|
||||||
|
<string name="lbl_genre">Genre</string>
|
||||||
|
<string name="lbl_rename">Byt namn</string>
|
||||||
|
<string name="lbl_edit">Redigera</string>
|
||||||
|
<string name="lbl_filter_all">Alla</string>
|
||||||
|
<string name="lbl_disc">Disk</string>
|
||||||
|
<string name="lbl_sort">Sortera</string>
|
||||||
|
<string name="lbl_shuffle_selected">Blanda utvalda</string>
|
||||||
|
<string name="lbl_queue_add">Lägg till kö</string>
|
||||||
|
<string name="lbl_file_name">Filnamn</string>
|
||||||
|
<string name="lbl_add">Lägg till</string>
|
||||||
|
<string name="lbl_state_wiped">Tillstånd tog bort</string>
|
||||||
|
<string name="lbl_bitrate">Bithastighet</string>
|
||||||
|
<string name="lbl_reset">Återställ</string>
|
||||||
|
<string name="lbl_state_saved">Tillstånd sparat</string>
|
||||||
|
<string name="lbl_version">Version</string>
|
||||||
|
<string name="lbl_library_counts">Statistik över beroende</string>
|
||||||
|
<string name="lng_playlist_renamed">Bytt namn av spellista</string>
|
||||||
|
<string name="lng_playlist_deleted">Spellista tog bort</string>
|
||||||
|
<string name="lng_author">Utvecklad av Alexander Capeheart</string>
|
||||||
|
<string name="set_theme">Tema</string>
|
||||||
|
<string name="set_theme_night">Mörkt</string>
|
||||||
|
<string name="set_accent">Färgschema</string>
|
||||||
|
<string name="set_black_mode_desc">Använda rent svart för det mörka temat</string>
|
||||||
|
<string name="set_round_mode_desc">Aktivera rundade hörn på ytterligare element i användargränssnittet (kräver att albumomslag är rundade)</string>
|
||||||
|
<string name="set_personalize">Anpassa</string>
|
||||||
|
<string name="set_lib_tabs_desc">Ändra synlighet och ordningsföljd av bibliotekflikar</string>
|
||||||
|
<string name="set_bar_action">Anpassad åtgärd för uppspelningsfält</string>
|
||||||
|
<string name="set_notif_action">Anpassad aviseringsåtgärd</string>
|
||||||
|
<string name="set_action_mode_next">Hoppa till nästa</string>
|
||||||
|
<string name="set_action_mode_repeat">Upprepningsmodus</string>
|
||||||
|
<string name="set_behavior">Beteende</string>
|
||||||
|
<string name="set_detail_song_playback_mode">När spelar från artikeluppgifter</string>
|
||||||
|
<string name="set_playback_mode_genre">Spela från genre</string>
|
||||||
|
<string name="set_keep_shuffle">Komma ihåg blandningsstatus</string>
|
||||||
|
<string name="set_keep_shuffle_desc">Behåll blandning på när spelar en ny låt</string>
|
||||||
|
<string name="set_content">Kontent</string>
|
||||||
|
<string name="set_content_desc">Kontrollera hur musik och bilar laddas</string>
|
||||||
|
<string name="set_music">Musik</string>
|
||||||
|
<string name="set_observing">Automatisk omladdning</string>
|
||||||
|
<string name="set_exclude_non_music">Inkludera bara musik</string>
|
||||||
|
<string name="set_exclude_non_music_desc">Ignorera ljudfiler som inte är musik, t.ex. podkaster</string>
|
||||||
|
<string name="set_separators">Värdeavskiljare</string>
|
||||||
|
<string name="set_separators_plus">Plus (+)</string>
|
||||||
|
<string name="set_intelligent_sorting">Intelligent sortering</string>
|
||||||
|
<string name="set_intelligent_sorting_desc">Sorterar namn som börjar med siffror eller ord som \"the\" korrekt (fungerar bäst med engelskspråkig music)</string>
|
||||||
|
<string name="set_hide_collaborators">Dölj medarbetare</string>
|
||||||
|
<string name="set_display">Skärm</string>
|
||||||
|
<string name="set_lib_tabs">Bibliotekflikar</string>
|
||||||
|
<string name="set_library_song_playback_mode">När spelar från biblioteket</string>
|
||||||
|
<string name="set_playback_mode_none">Spela från visad artikel</string>
|
||||||
|
<string name="set_playback_mode_songs">Spela från alla låtar</string>
|
||||||
|
<string name="set_playback_mode_artist">Spela från konstnär</string>
|
||||||
|
<string name="set_playback_mode_album">Spela från album</string>
|
||||||
|
<string name="set_separators_semicolon">Semikolon (;)</string>
|
||||||
|
<string name="set_observing_desc">Ladda om musikbiblioteket när det ändras (kräver permanent meddelande)</string>
|
||||||
|
<string name="set_separators_comma">Komma (,)</string>
|
||||||
|
<string name="set_separators_slash">Snedstreck (/)</string>
|
||||||
|
<string name="set_separators_desc">Konfigurera tecken som separerar flera värden i taggar</string>
|
||||||
|
<string name="set_separators_warning">Advarsel: Denna inställning kan leda till att vissa taggar separeras felaktigt. För att åtgärda detta, prefixa oönskade separatortecken med ett backslash (\\).</string>
|
||||||
|
<string name="set_personalize_desc">Anpassa UI-kontroller och beteende</string>
|
||||||
|
</resources>
|
|
@ -299,4 +299,6 @@
|
||||||
<string name="fmt_editing">Редагування %s</string>
|
<string name="fmt_editing">Редагування %s</string>
|
||||||
<string name="def_disc">Немає диску</string>
|
<string name="def_disc">Немає диску</string>
|
||||||
<string name="lbl_appears_on">З\'являється на</string>
|
<string name="lbl_appears_on">З\'являється на</string>
|
||||||
|
<string name="set_square_covers_desc">Обрізання обкладинки альбомів до співвідношення сторін 1:1</string>
|
||||||
|
<string name="set_square_covers">Примусові квадратні обкладинки</string>
|
||||||
</resources>
|
</resources>
|
|
@ -293,4 +293,6 @@
|
||||||
<string name="lbl_share">分享</string>
|
<string name="lbl_share">分享</string>
|
||||||
<string name="def_disc">无唱片</string>
|
<string name="def_disc">无唱片</string>
|
||||||
<string name="fmt_editing">正在编辑 %s</string>
|
<string name="fmt_editing">正在编辑 %s</string>
|
||||||
|
<string name="set_square_covers">强制使用方形专辑封面</string>
|
||||||
|
<string name="set_square_covers_desc">将所有专辑封面裁剪至 1:1 宽高比</string>
|
||||||
</resources>
|
</resources>
|
|
@ -20,7 +20,6 @@
|
||||||
<dimen name="size_corners_medium">16dp</dimen>
|
<dimen name="size_corners_medium">16dp</dimen>
|
||||||
<dimen name="size_corners_mid_large">24dp</dimen>
|
<dimen name="size_corners_mid_large">24dp</dimen>
|
||||||
|
|
||||||
|
|
||||||
<dimen name="size_btn">48dp</dimen>
|
<dimen name="size_btn">48dp</dimen>
|
||||||
<dimen name="size_accent_item">56dp</dimen>
|
<dimen name="size_accent_item">56dp</dimen>
|
||||||
<dimen name="size_bottom_sheet_bar">64dp</dimen>
|
<dimen name="size_bottom_sheet_bar">64dp</dimen>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<string name="set_key_observing" translatable="false">auxio_observing</string>
|
<string name="set_key_observing" translatable="false">auxio_observing</string>
|
||||||
<string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string>
|
<string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string>
|
||||||
<string name="set_key_cover_mode" translatable="false">auxio_cover_mode</string>
|
<string name="set_key_cover_mode" translatable="false">auxio_cover_mode</string>
|
||||||
|
<string name="set_key_square_covers" translatable="false">auxio_square_covers</string>
|
||||||
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
|
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
|
||||||
<string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string>
|
<string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string>
|
||||||
<string name="set_key_separators" translatable="false">auxio_separators</string>
|
<string name="set_key_separators" translatable="false">auxio_separators</string>
|
||||||
|
|
|
@ -239,6 +239,8 @@
|
||||||
<string name="set_cover_mode_off">Off</string>
|
<string name="set_cover_mode_off">Off</string>
|
||||||
<string name="set_cover_mode_media_store">Fast</string>
|
<string name="set_cover_mode_media_store">Fast</string>
|
||||||
<string name="set_cover_mode_quality">High quality</string>
|
<string name="set_cover_mode_quality">High quality</string>
|
||||||
|
<string name="set_square_covers">Force square album covers</string>
|
||||||
|
<string name="set_square_covers_desc">Crop all album covers to a 1:1 aspect ratio</string>
|
||||||
|
|
||||||
<string name="set_audio">Audio</string>
|
<string name="set_audio">Audio</string>
|
||||||
<string name="set_audio_desc">Configure sound and playback behavior</string>
|
<string name="set_audio_desc">Configure sound and playback behavior</string>
|
||||||
|
|
|
@ -216,7 +216,7 @@
|
||||||
<item name="android:paddingEnd">@dimen/spacing_small</item>
|
<item name="android:paddingEnd">@dimen/spacing_small</item>
|
||||||
<item name="android:paddingTop">@dimen/spacing_small</item>
|
<item name="android:paddingTop">@dimen/spacing_small</item>
|
||||||
<item name="android:paddingBottom">@dimen/spacing_small</item>
|
<item name="android:paddingBottom">@dimen/spacing_small</item>
|
||||||
<!-- Intentional to prevent button from spamming the log console over -->
|
<!-- Intentional to prevent button from spamming the log console -->
|
||||||
<item name="iconTint">@color/m3_text_button_foreground_color_selector</item>
|
<item name="iconTint">@color/m3_text_button_foreground_color_selector</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
@ -43,5 +43,11 @@
|
||||||
app:key="@string/set_key_cover_mode"
|
app:key="@string/set_key_cover_mode"
|
||||||
app:title="@string/set_cover_mode" />
|
app:title="@string/set_cover_mode" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:defaultValue="false"
|
||||||
|
app:key="@string/set_key_square_covers"
|
||||||
|
app:summary="@string/set_square_covers_desc"
|
||||||
|
app:title="@string/set_square_covers" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
|
@ -1,7 +1,7 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
kotlin_version = '1.8.21'
|
kotlin_version = '1.8.22'
|
||||||
navigation_version = "2.5.3"
|
navigation_version = "2.6.0"
|
||||||
hilt_version = '2.46.1'
|
hilt_version = '2.46.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
4
fastlane/metadata/android/en-US/changelogs/33.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/33.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Auxio 3.1.0 introduces playlisting functionality, with more features coming soon.
|
||||||
|
This release fixes an issue where some users would experience an infinite loading
|
||||||
|
screen, along other quality-of-life improvements.
|
||||||
|
For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v3.1.3.
|
1
fastlane/metadata/android/sv/short_description.txt
Normal file
1
fastlane/metadata/android/sv/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
En enkel, rationell musikspelare
|
Loading…
Reference in a new issue