image: fix seam appearing on some images

Fix an issue where a seam might appear on some covers when rounded
covers was enabled.

This was caused by a poor usage of clipToOutline. Replace with simply
stacking existing image instances on top of eachother.
This commit is contained in:
OxygenCobalt 2022-06-17 10:24:16 -06:00
parent 3d19794d63
commit 09442c475f
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 78 additions and 193 deletions

View file

@ -199,8 +199,6 @@ class DetailViewModel(application: Application) :
null
}
// Ensure that we don't include the functionally useless
// "audio/raw" mime type
MimeType(song.mimeType.fromExtension, formatMimeType)
}

View file

@ -1,133 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
*
* 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
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.graphics.drawable.DrawableCompat
import coil.dispose
import coil.load
import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.getDrawableSafe
/**
* The base class for Auxio's images. Do not use this class outside of this module.
*
* Default behavior includes the addition of a tonal background and automatic icon sizing. Other
* behavior is implemented by [StyledImageView] and [ImageGroup].
*
* @author OxygenCobalt
*/
open class BaseStyledImageView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
AppCompatImageView(context, attrs, defStyleAttr) {
private var staticIcon = 0
init {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
staticIcon = styledAttrs.getResourceId(R.styleable.StyledImageView_staticIcon, -1)
styledAttrs.recycle()
if (staticIcon > -1) {
@Suppress("LeakingThis")
setImageDrawable(StyledDrawable(context, context.getDrawableSafe(staticIcon)))
}
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorStateListSafe(R.color.sel_cover_bg)
}
}
/** Bind the album cover for a [song]. */
open fun bind(song: Song) = loadImpl(song, R.drawable.ic_song)
/** Bind the album cover for an [album]. */
open fun bind(album: Album) = loadImpl(album, R.drawable.ic_album)
/** Bind the image for an [artist] */
open fun bind(artist: Artist) = loadImpl(artist, R.drawable.ic_artist)
/** Bind the image for a [genre] */
open fun bind(genre: Genre) = loadImpl(genre, R.drawable.ic_genre)
private fun <T : Music> loadImpl(music: T, @DrawableRes error: Int) {
if (staticIcon > -1) {
throw IllegalStateException("Static StyledImageViews cannot bind new images")
}
dispose()
load(music) {
error(StyledDrawable(context, error))
transformations(SquareFrameTransform.INSTANCE)
}
}
/**
* A companion drawable that can be used with the style that [StyledImageView] provides.
* @author OxygenCobalt
*/
class StyledDrawable(context: Context, private val src: Drawable) : Drawable() {
constructor(
context: Context,
@DrawableRes res: Int
) : this(context, context.getDrawableSafe(res))
init {
// Re-tint the drawable to something that will play along with the background
DrawableCompat.setTintList(src, context.getColorStateListSafe(R.color.sel_on_cover_bg))
}
override fun draw(canvas: Canvas) {
src.bounds.set(canvas.clipBounds)
val adjustWidth = src.bounds.width() / 4
val adjustHeight = src.bounds.height() / 4
src.bounds.set(
adjustWidth,
adjustHeight,
src.bounds.width() - adjustWidth,
src.bounds.height() - adjustHeight)
src.draw(canvas)
}
override fun setAlpha(alpha: Int) {
src.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
src.colorFilter = colorFilter
}
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}
}

View file

@ -29,7 +29,6 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.getDrawableSafe
@ -52,13 +51,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
FrameLayout(context, attrs, defStyleAttr) {
private val cornerRadius: Float
private val inner = BaseStyledImageView(context, attrs)
private val inner: StyledImageView
private var customView: View? = null
private val indicator =
BaseStyledImageView(context).apply {
setImageDrawable(
StyledDrawable(context, context.getDrawableSafe(R.drawable.ic_equalizer)))
}
private val indicator: StyledImageView
init {
// Android wants you to make separate attributes for each view type, but will
@ -68,23 +63,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
cornerRadius = styledAttrs.getDimension(R.styleable.StyledImageView_cornerRadius, 0f)
styledAttrs.recycle()
addView(inner)
// Use clipToOutline and a background drawable to crop images. While Coil's transformation
// could theoretically be used to round corners, the corner radius is dependent on the
// dimensions of the image, which will result in inconsistent corners across different
// album covers unless we resize all covers to be the same size. clipToOutline is both
// cheaper and more elegant. As a side-note, this also allows us to re-use the same
// background for both the tonal background color and the corner rounding.
background = MaterialShapeDrawable()
clipToOutline = true
if (!isInEditMode) {
val settingsManager = SettingsManager.getInstance()
if (settingsManager.roundCovers) {
(background as MaterialShapeDrawable).setCornerSize(cornerRadius)
inner = StyledImageView(context, attrs)
indicator =
StyledImageView(context).apply {
cornerRadius = this@ImageGroup.cornerRadius
staticIcon = context.getDrawableSafe(R.drawable.ic_equalizer)
}
}
addView(inner)
}
override fun onFinishInflate() {
@ -99,6 +85,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorStateListSafe(R.color.sel_cover_bg)
setCornerSize(cornerRadius)
}
}

View file

@ -18,16 +18,25 @@
package org.oxycblt.auxio.image
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.res.ResourcesCompat
import coil.dispose
import coil.load
import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.getDrawableSafe
/**
* An [AppCompatImageView] that applies many of the stylistic choices that Auxio uses regarding
@ -41,14 +50,27 @@ import org.oxycblt.auxio.settings.SettingsManager
class StyledImageView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
BaseStyledImageView(context, attrs, defStyleAttr) {
private var cornerRadius = 0f
AppCompatImageView(context, attrs, defStyleAttr) {
var cornerRadius = 0f
set(value) {
field = value
(background as? MaterialShapeDrawable)?.let { bg ->
if (!isInEditMode && SettingsManager.getInstance().roundCovers) {
bg.setCornerSize(value)
} else {
bg.setCornerSize(0f)
}
}
}
var staticIcon: Drawable? = null
set(value) {
val wrapped = value?.let { StyledDrawable(context, it) }
field = wrapped
setImageDrawable(field)
}
init {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
cornerRadius = styledAttrs.getDimension(R.styleable.StyledImageView_cornerRadius, 0f)
styledAttrs.recycle()
// Use clipToOutline and a background drawable to crop images. While Coil's transformation
// could theoretically be used to round corners, the corner radius is dependent on the
// dimensions of the image, which will result in inconsistent corners across different
@ -56,36 +78,47 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
// cheaper and more elegant. As a side-note, this also allows us to re-use the same
// background for both the tonal background color and the corner rounding.
clipToOutline = true
if (!isInEditMode) {
val settingsManager = SettingsManager.getInstance()
if (settingsManager.roundCovers) {
(background as MaterialShapeDrawable).setCornerSize(cornerRadius)
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorStateListSafe(R.color.sel_cover_bg)
setCornerSize(cornerRadius)
}
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
val staticIconRes =
styledAttrs.getResourceId(
R.styleable.StyledImageView_staticIcon, ResourcesCompat.ID_NULL)
if (staticIconRes != ResourcesCompat.ID_NULL) {
staticIcon = context.getDrawableSafe(staticIconRes)
}
cornerRadius = styledAttrs.getDimension(R.styleable.StyledImageView_cornerRadius, 0f)
styledAttrs.recycle()
}
/** Bind the album cover for a [song]. */
fun bind(song: Song) = loadImpl(song, R.drawable.ic_song, R.string.desc_album_cover)
/** Bind the album cover for an [album]. */
fun bind(album: Album) = loadImpl(album, R.drawable.ic_album, R.string.desc_album_cover)
/** Bind the image for an [artist] */
fun bind(artist: Artist) = loadImpl(artist, R.drawable.ic_artist, R.string.desc_artist_image)
/** Bind the image for a [genre] */
fun bind(genre: Genre) = loadImpl(genre, R.drawable.ic_genre, R.string.desc_genre_image)
private fun <T : Music> loadImpl(music: T, @DrawableRes error: Int, @StringRes desc: Int) {
if (staticIcon != null) {
error("Static StyledImageViews cannot bind new images")
}
contentDescription = context.getString(desc, music.resolveName(context))
dispose()
load(music) {
error(StyledDrawable(context, context.getDrawableSafe(error)))
transformations(SquareFrameTransform.INSTANCE)
}
}
override fun bind(song: Song) {
super.bind(song)
contentDescription =
context.getString(R.string.desc_album_cover, song.album.resolveName(context))
}
override fun bind(album: Album) {
super.bind(album)
contentDescription =
context.getString(R.string.desc_album_cover, album.resolveName(context))
}
override fun bind(artist: Artist) {
super.bind(artist)
contentDescription =
context.getString(R.string.desc_artist_image, artist.resolveName(context))
}
override fun bind(genre: Genre) {
super.bind(genre)
contentDescription =
context.getString(R.string.desc_genre_image, genre.resolveName(context))
}
}