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 null
} }
// Ensure that we don't include the functionally useless
// "audio/raw" mime type
MimeType(song.mimeType.fromExtension, formatMimeType) 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.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getColorStateListSafe
import org.oxycblt.auxio.util.getDrawableSafe import org.oxycblt.auxio.util.getDrawableSafe
@ -52,13 +51,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
FrameLayout(context, attrs, defStyleAttr) { FrameLayout(context, attrs, defStyleAttr) {
private val cornerRadius: Float private val cornerRadius: Float
private val inner = BaseStyledImageView(context, attrs) private val inner: StyledImageView
private var customView: View? = null private var customView: View? = null
private val indicator = private val indicator: StyledImageView
BaseStyledImageView(context).apply {
setImageDrawable(
StyledDrawable(context, context.getDrawableSafe(R.drawable.ic_equalizer)))
}
init { init {
// Android wants you to make separate attributes for each view type, but will // 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) cornerRadius = styledAttrs.getDimension(R.styleable.StyledImageView_cornerRadius, 0f)
styledAttrs.recycle() styledAttrs.recycle()
inner = StyledImageView(context, attrs)
indicator =
StyledImageView(context).apply {
cornerRadius = this@ImageGroup.cornerRadius
staticIcon = context.getDrawableSafe(R.drawable.ic_equalizer)
}
addView(inner) 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)
}
}
} }
override fun onFinishInflate() { override fun onFinishInflate() {
@ -99,6 +85,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
background = background =
MaterialShapeDrawable().apply { MaterialShapeDrawable().apply {
fillColor = context.getColorStateListSafe(R.color.sel_cover_bg) fillColor = context.getColorStateListSafe(R.color.sel_cover_bg)
setCornerSize(cornerRadius)
} }
} }

View file

@ -18,16 +18,25 @@
package org.oxycblt.auxio.image package org.oxycblt.auxio.image
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageView 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 com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
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
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager 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 * 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 class StyledImageView
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
BaseStyledImageView(context, attrs, defStyleAttr) { AppCompatImageView(context, attrs, defStyleAttr) {
private var cornerRadius = 0f 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 { 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 // 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 // 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 // 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 // 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 for both the tonal background color and the corner rounding.
clipToOutline = true clipToOutline = true
background =
if (!isInEditMode) { MaterialShapeDrawable().apply {
val settingsManager = SettingsManager.getInstance() fillColor = context.getColorStateListSafe(R.color.sel_cover_bg)
if (settingsManager.roundCovers) { setCornerSize(cornerRadius)
(background as MaterialShapeDrawable).setCornerSize(cornerRadius)
}
}
} }
override fun bind(song: Song) { val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
super.bind(song) val staticIconRes =
contentDescription = styledAttrs.getResourceId(
context.getString(R.string.desc_album_cover, song.album.resolveName(context)) R.styleable.StyledImageView_staticIcon, ResourcesCompat.ID_NULL)
if (staticIconRes != ResourcesCompat.ID_NULL) {
staticIcon = context.getDrawableSafe(staticIconRes)
} }
override fun bind(album: Album) { cornerRadius = styledAttrs.getDimension(R.styleable.StyledImageView_cornerRadius, 0f)
super.bind(album) styledAttrs.recycle()
contentDescription =
context.getString(R.string.desc_album_cover, album.resolveName(context))
} }
override fun bind(artist: Artist) { /** Bind the album cover for a [song]. */
super.bind(artist) fun bind(song: Song) = loadImpl(song, R.drawable.ic_song, R.string.desc_album_cover)
contentDescription =
context.getString(R.string.desc_artist_image, artist.resolveName(context)) /** 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")
} }
override fun bind(genre: Genre) { contentDescription = context.getString(desc, music.resolveName(context))
super.bind(genre)
contentDescription = dispose()
context.getString(R.string.desc_genre_image, genre.resolveName(context)) load(music) {
error(StyledDrawable(context, context.getDrawableSafe(error)))
transformations(SquareFrameTransform.INSTANCE)
}
} }
} }