diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a0328f5d..4625c7228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ #### What's Improved - Shuffle and Repeat mode buttons now have more contrast when they are turned on +#### What's Changed +- All cover art is now cropped to a 1:1 aspect ratio + #### Dev/Meta - Enabled elevation drop shadows below Android P for consistency - Reworked dynamic color usage diff --git a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt index 6a9ba4f5c..85464857d 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt @@ -68,11 +68,11 @@ fun ImageView.load(music: T?, @DrawableRes error: Int) { // We don't round album covers by default as it desecrates album artwork, but we do provide // an option if one wants it. - // As for why we use clipToOutline instead of coil's RoundedCornersTransformation, the transform - // uses the dimensions of the image to create the corners, which results in inconsistent corners - // across loaded cover art. + // As for why we use clipToOutline instead of coils RoundedCornersTransformation, the radii + // of an image's corners is dependent on the actual dimensions of the image, which would force + // us to resize all images to a fixed size. clipToOutline is pretty much always cheaper as long + // as we have a perfectly-square image. val settingsManager = SettingsManager.getInstance() - if (settingsManager.roundCovers && background == null) { setBackgroundResource(R.drawable.ui_rounded_cutout) clipToOutline = true @@ -83,6 +83,7 @@ fun ImageView.load(music: T?, @DrawableRes error: Int) { load(music) { error(error) + transformations(SquareFrameTransform()) } } @@ -102,6 +103,7 @@ fun loadBitmap( ImageRequest.Builder(context) .data(song.album) .size(Size.ORIGINAL) + .transformations(SquareFrameTransform()) .target( onError = { onDone(null) }, onSuccess = { onDone(it.toBitmap()) } diff --git a/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt b/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt new file mode 100644 index 000000000..34875c602 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt @@ -0,0 +1,40 @@ +package org.oxycblt.auxio.coil + +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, however unlike + * the actual ScaleType, this isn't affected by any hacks we do with ImageView itself. + * @author OxygenCobalt + */ +class SquareFrameTransform : Transformation { + override val cacheKey: String + get() = "SquareFrameTransform" + + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + val dstSize = min(input.width, input.height) + val x = (input.width - dstSize) / 2 + val y = (input.height - dstSize) / 2 + + val wantedWidth = size.width.pxOrElse { dstSize } + val wantedHeight = size.height.pxOrElse { dstSize } + + val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize) + + if (dstSize != wantedWidth || dstSize != wantedHeight) { + // Desired size differs from the cropped size, resize the bitmap. + return Bitmap.createScaledBitmap( + dst, + wantedWidth, + wantedHeight, + true + ) + } + + return dst + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackButton.kt index a79ec014b..523f2a2b6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackButton.kt @@ -4,7 +4,6 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Matrix import android.graphics.RectF -import android.graphics.drawable.Drawable import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageButton import org.oxycblt.auxio.R @@ -32,7 +31,13 @@ class PlaybackButton @JvmOverloads constructor( private val centerMatrix = Matrix() private val matrixSrc = RectF() private val matrixDst = RectF() - private val indicatorDrawable: Drawable? + + private val indicatorDrawable = context.getDrawableSafe(R.drawable.ui_indicator) + var hasIndicator = false + set(value) { + field = value + invalidate() + } init { val size = context.getDimenSizeSafe(R.dimen.size_btn_small) @@ -42,14 +47,7 @@ class PlaybackButton @JvmOverloads constructor( setBackgroundResource(R.drawable.ui_large_unbounded_ripple) val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.PlaybackButton) - - val hasIndicator = styledAttrs.getBoolean(R.styleable.PlaybackButton_hasIndicator, false) - indicatorDrawable = if (hasIndicator) { - context.getDrawableSafe(R.drawable.ui_indicator) - } else { - null - } - + hasIndicator = styledAttrs.getBoolean(R.styleable.PlaybackButton_hasIndicator, false) styledAttrs.recycle() } @@ -74,20 +72,18 @@ class PlaybackButton @JvmOverloads constructor( } } - indicatorDrawable?.let { indicator -> - val x = (measuredWidth - indicator.intrinsicWidth) / 2 - val y = ((measuredHeight - iconSize) / 2) + iconSize + val x = (measuredWidth - indicatorDrawable.intrinsicWidth) / 2 + val y = ((measuredHeight - iconSize) / 2) + iconSize - indicator.bounds.set( - x, y, x + indicator.intrinsicWidth, y + indicator.intrinsicHeight - ) - } + indicatorDrawable.bounds.set( + x, y, x + indicatorDrawable.intrinsicWidth, y + indicatorDrawable.intrinsicHeight + ) } override fun onDrawForeground(canvas: Canvas) { super.onDrawForeground(canvas) - if (indicatorDrawable != null && isActivated) { + if (hasIndicator && isActivated) { indicatorDrawable.draw(canvas) } } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt index 50e607e9e..feb146d7a 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt @@ -179,15 +179,8 @@ private fun RemoteViews.applyFullControls(context: Context, state: WidgetState): ) ) - // RemoteView is so restrictive that emulating auxio's playback icons in a sensible way is - // more or less impossible, including: - // 1. Setting foreground drawables - // 2. Applying custom image matrices - // 3. Tinting icons at all - // - // So, we have to do the dumbest possible method of duplicating each drawable and hard-coding - // indicators, tints, and icon sizes. And then google wonders why nobody uses widgets on - // android. + // Like notifications, use the remote variants of icons since we really don't want to hack + // indicators. val shuffleRes = when { state.isShuffled -> R.drawable.ic_remote_shuffle_on else -> R.drawable.ic_remote_shuffle_off diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 6014f7072..dad9770de 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -34,6 +34,7 @@ import coil.imageLoader import coil.request.ImageRequest import coil.transform.RoundedCornersTransformation import org.oxycblt.auxio.BuildConfig +import org.oxycblt.auxio.coil.SquareFrameTransform import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.util.getDimenSizeSafe @@ -87,31 +88,34 @@ class WidgetProvider : AppWidgetProvider() { } private fun loadWidgetBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) { - // Load our image so that it takes up the phone screen. This allows - // us to get stable rounded corners for every single widget image. This probably - // sacrifices quality in some way, but it's really the only good option. - val metrics = context.resources.displayMetrics - val imageSize = min(metrics.widthPixels, metrics.heightPixels) - val coverRequest = ImageRequest.Builder(context) .data(song.album) - .size(imageSize) .target( onError = { onDone(null) }, onSuccess = { onDone(it.toBitmap()) } ) - // If we are on Android 12 or higher, round out the album cover. - // This is simply to maintain stylistic cohesion with other widgets. - // Here, we actually have to use RoundedCornersTransformation since the way - // we get a 1:1 aspect ratio image results in clipToOutline not working well. + // The widget has two distinct styles that we must transform the album art to accommodate: + // - Before Android 12, the widget has hard edges, so we don't need to round out the album + // art. + // - After Android 12, the widget has round edges, so we need to round out the album art. + // I dislike this, but it's mainly for stylistic cohesion. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Use RoundedCornersTransformation. This is because our hack to get a 1:1 aspect + // ratio on widget ImageViews doesn't actually result in a square ImageView, so + // clipToOutline won't work. val transform = RoundedCornersTransformation( context.getDimenSizeSafe(android.R.dimen.system_app_widget_inner_radius) .toFloat() ) - coverRequest.transformations(transform) + // The output of RoundedCornersTransformation is dimension-dependent, so scale up the + // image to the screen size to ensure consistent radii. + val metrics = context.resources.displayMetrics + coverRequest.transformations(SquareFrameTransform(), transform) + .size(min(metrics.widthPixels, metrics.heightPixels)) + } else { + coverRequest.transformations(SquareFrameTransform()) } context.imageLoader.enqueue(coverRequest.build()) @@ -214,7 +218,6 @@ class WidgetProvider : AppWidgetProvider() { // Find the layout with the greatest area that fits entirely within // the widget. This is what we will use. - val candidates = mutableListOf() for (size in views.keys) { diff --git a/app/src/main/res/drawable/ic_remote_loop_off.xml b/app/src/main/res/drawable/ic_remote_loop_off.xml index 2e5c0ed3c..433c53d66 100644 --- a/app/src/main/res/drawable/ic_remote_loop_off.xml +++ b/app/src/main/res/drawable/ic_remote_loop_off.xml @@ -6,6 +6,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_remote_shuffle_off.xml b/app/src/main/res/drawable/ic_remote_shuffle_off.xml index b8cc6e16e..c51c1c77c 100644 --- a/app/src/main/res/drawable/ic_remote_shuffle_off.xml +++ b/app/src/main/res/drawable/ic_remote_shuffle_off.xml @@ -6,6 +6,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index 180b7b0d3..9f41449fb 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -164,18 +164,16 @@ - - \ No newline at end of file