From e1dbe6c40cf827b71691d00edf8836b8874ca6a5 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Mon, 21 Feb 2022 19:34:25 -0700 Subject: [PATCH] coil: force all images to a 1:1 aspect ratio Crop all images to a 1:1 aspect ratio. Sometimes cover art will not be a square. I never realized this because all covers in my music library are square. This led to many strange bugs that I would rather avoid, so create a new transformation that crops the image to a 1:1 aspect ratio. The reason why we use a transformation is that it won't be dependent on the actual state of the ImageView, which is what would happen if we used ScaleType.CENTER_CROP. In the case that we use a very weird hacky ImageView like in the widgets, doing such would result in any cropping not actually working correctly. It's better to do it in the Transformation stage simply to ensure consistency. --- CHANGELOG.md | 3 ++ .../java/org/oxycblt/auxio/coil/CoilUtils.kt | 10 +++-- .../auxio/coil/SquareFrameTransform.kt | 40 +++++++++++++++++++ .../oxycblt/auxio/playback/PlaybackButton.kt | 32 +++++++-------- .../java/org/oxycblt/auxio/widgets/Forms.kt | 11 +---- .../oxycblt/auxio/widgets/WidgetProvider.kt | 29 ++++++++------ .../main/res/drawable/ic_remote_loop_off.xml | 2 +- .../res/drawable/ic_remote_shuffle_off.xml | 2 +- app/src/main/res/values/styles_ui.xml | 14 +++---- 9 files changed, 89 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt 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