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.
This commit is contained in:
parent
6fc3c9374c
commit
e1dbe6c40c
9 changed files with 89 additions and 54 deletions
|
@ -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
|
||||
|
|
|
@ -68,11 +68,11 @@ fun <T : Music> 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 <T : Music> 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()) }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<SizeF>()
|
||||
|
||||
for (size in views.keys) {
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A0ffffff"
|
||||
android:fillColor="#80ffffff"
|
||||
android:pathData="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z" />
|
||||
</vector>
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#A0FFFFFF"
|
||||
android:fillColor="#80FFFFFF"
|
||||
android:pathData="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm0.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z" />
|
||||
</vector>
|
||||
|
|
|
@ -164,18 +164,16 @@
|
|||
</style>
|
||||
|
||||
<style name="Widget.Auxio.FloatingActionButton.PlayPause" parent="Widget.Material3.FloatingActionButton.Secondary">
|
||||
<item name="maxImageSize">@dimen/size_playback_icon</item>
|
||||
<item name="fabCustomSize">@dimen/size_btn_large</item>
|
||||
|
||||
<!--
|
||||
Abuse this floating action button to act more like an old-school auxio button.
|
||||
This is only done because elevation acts weird with the panel layout.
|
||||
This is for two reasons:
|
||||
1. We upscale the play icon to 32dp, so the total FAB size also needs to increase to
|
||||
compensate.
|
||||
2. For some reason elevation behaves strangely in the playback panel, so we disable it.
|
||||
-->
|
||||
<item name="maxImageSize">@dimen/size_playback_icon</item>
|
||||
<item name="fabCustomSize">@dimen/size_btn_large</item>
|
||||
<item name="android:elevation">0dp</item>
|
||||
<item name="elevation">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearance.Auxio.FloatingActionButton.PlayPause" parent="">
|
||||
<item name="cornerSize">50%</item>
|
||||
</style>
|
||||
</resources>
|
Loading…
Reference in a new issue