coil: make mosaics use squareframetransform

Change createMosaic to use SquareFrameTransform.

This should help fix a theoretical bug where non-1:1 album covers will
be oddly stretched in a generated mosaic, as all album covers will be
forcefully cropped to a 1:1 ratio.
This commit is contained in:
OxygenCobalt 2022-02-24 14:55:35 -07:00
parent 61dbfe3185
commit 9171f9a3b4
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
10 changed files with 36 additions and 18 deletions

View file

@ -40,6 +40,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
/** /**
* The single [AppCompatActivity] for Auxio. * The single [AppCompatActivity] for Auxio.
* TODO: Add a new view for crashes with a stack trace
* TODO: Custom language support
*/ */
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val playbackModel: PlaybackViewModel by viewModels() private val playbackModel: PlaybackViewModel by viewModels()
@ -104,10 +106,10 @@ class MainActivity : AppCompatActivity() {
// The black theme has a completely separate set of styles since style attributes cannot // The black theme has a completely separate set of styles since style attributes cannot
// be modified at runtime. // be modified at runtime.
if (isNight && settingsManager.useBlackTheme) { if (isNight && settingsManager.useBlackTheme) {
logD("Applying black theme [with accent $accent]") logD("Applying black theme [accent $accent]")
setTheme(accent.blackTheme) setTheme(accent.blackTheme)
} else { } else {
logD("Applying normal theme [with accent $accent]") logD("Applying normal theme [accent $accent]")
setTheme(accent.theme) setTheme(accent.theme)
} }
} }

View file

@ -42,6 +42,7 @@ import org.oxycblt.auxio.util.logW
* A wrapper around the home fragment that shows the playback fragment and controls * A wrapper around the home fragment that shows the playback fragment and controls
* the more high-level navigation features. * the more high-level navigation features.
* @author OxygenCobalt * @author OxygenCobalt
* TODO: Add a new view with a stack trace whenever the music loading process fails.
*/ */
class MainFragment : Fragment() { class MainFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()

View file

@ -35,6 +35,7 @@ import android.util.Size as AndroidSize
/** /**
* The base implementation for all image fetchers in Auxio. * The base implementation for all image fetchers in Auxio.
* @author OxygenCobalt * @author OxygenCobalt
* TODO: Artist images
*/ */
abstract class AuxioFetcher : Fetcher { abstract class AuxioFetcher : Fetcher {
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
@ -204,7 +205,7 @@ abstract class AuxioFetcher : Fetcher {
* Create a mosaic image from multiple streams of image data, Code adapted from Phonograph * Create a mosaic image from multiple streams of image data, Code adapted from Phonograph
* https://github.com/kabouzeid/Phonograph * https://github.com/kabouzeid/Phonograph
*/ */
protected fun createMosaic(context: Context, streams: List<InputStream>, size: Size): FetchResult? { protected suspend fun createMosaic(context: Context, streams: List<InputStream>, size: Size): FetchResult? {
if (streams.size < 4) { if (streams.size < 4) {
return streams.firstOrNull()?.let { stream -> return streams.firstOrNull()?.let { stream ->
return SourceResult( return SourceResult(
@ -219,7 +220,9 @@ abstract class AuxioFetcher : Fetcher {
// get a symmetrical mosaic [and to prevent bugs]. If there is no size, default to a // get a symmetrical mosaic [and to prevent bugs]. If there is no size, default to a
// 512x512 mosaic. // 512x512 mosaic.
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize()) val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
val increment = AndroidSize(mosaicSize.width / 2, mosaicSize.height / 2) val mosaicFrameSize = Size(
Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2)
)
val mosaicBitmap = Bitmap.createBitmap( val mosaicBitmap = Bitmap.createBitmap(
mosaicSize.width, mosaicSize.width,
@ -238,20 +241,20 @@ abstract class AuxioFetcher : Fetcher {
break break
} }
val bitmap = Bitmap.createScaledBitmap( // Run the bitmap through a transform to make sure it's square
BitmapFactory.decodeStream(stream), val bitmap = SquareFrameTransform.INSTANCE
increment.width, .transform(
increment.height, BitmapFactory.decodeStream(stream),
true mosaicFrameSize
) )
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null) canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
x += increment.width x += bitmap.width
if (x == mosaicSize.width) { if (x == mosaicSize.width) {
x = 0 x = 0
y += increment.height y += bitmap.height
} }
} }

View file

@ -64,10 +64,9 @@ fun ImageView.bindGenreImage(genre: Genre?) = load(genre, R.drawable.ic_genre)
fun <T : Music> ImageView.load(music: T?, @DrawableRes error: Int) { fun <T : Music> ImageView.load(music: T?, @DrawableRes error: Int) {
dispose() dispose()
load(music) { load(music) {
error(error) error(error)
transformations(SquareFrameTransform()) transformations(SquareFrameTransform.INSTANCE)
} }
} }

View file

@ -10,6 +10,11 @@ import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getColorSafe import org.oxycblt.auxio.util.getColorSafe
import org.oxycblt.auxio.util.stateList import org.oxycblt.auxio.util.stateList
/**
* An [AppCompatImageView] that applies the specified cornerRadius attribute if the user
* has enabled the "Round album covers" option. We don't round album covers by default as
* it desecrates album artwork, but if the user desires it we do have an option to enable it.
*/
class RoundableImageView @JvmOverloads constructor( class RoundableImageView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
@ -17,7 +22,7 @@ class RoundableImageView @JvmOverloads constructor(
) : AppCompatImageView(context, attrs, defStyleAttr) { ) : AppCompatImageView(context, attrs, defStyleAttr) {
init { init {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.RoundableImageView) val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.RoundableImageView)
val cornerRadius = styledAttrs.getDimension(R.styleable.RoundableImageView_cornerRadius, 0.0f) val cornerRadius = styledAttrs.getDimension(R.styleable.RoundableImageView_cornerRadius, 0f)
styledAttrs.recycle() styledAttrs.recycle()
background = MaterialShapeDrawable().apply { background = MaterialShapeDrawable().apply {
@ -29,8 +34,6 @@ class RoundableImageView @JvmOverloads constructor(
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
// 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 coils RoundedCornersTransformation, the radii // 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 // 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 // force us to resize all images to a fixed size. clipToOutline is pretty much always

View file

@ -37,4 +37,8 @@ class SquareFrameTransform : Transformation {
return dst return dst
} }
companion object {
val INSTANCE = SquareFrameTransform()
}
} }

View file

@ -55,6 +55,7 @@ import org.oxycblt.auxio.util.logTraceOrThrow
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail * The main "Launching Point" fragment of Auxio, allowing navigation to the detail
* views for each respective item. * views for each respective item.
* @author OxygenCobalt * @author OxygenCobalt
* TODO: Make tabs invisible when there is only one
*/ */
class HomeFragment : Fragment() { class HomeFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
@ -163,7 +164,7 @@ class HomeFragment : Fragment() {
playbackModel.shuffleAll() playbackModel.shuffleAll()
} }
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
// There is no way a fast scrolling event can continue across a re-create. Reset it. // There is no way a fast scrolling event can continue across a re-create. Reset it.
homeModel.updateFastScrolling(false) homeModel.updateFastScrolling(false)

View file

@ -36,6 +36,7 @@ import java.lang.Exception
* The main storage for music items. * The main storage for music items.
* Getting an instance of this object is more complicated as it loads asynchronously. * Getting an instance of this object is more complicated as it loads asynchronously.
* See the companion object for more. * See the companion object for more.
* TODO: Add automatic rescanning [major change]
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class MusicStore private constructor() { class MusicStore private constructor() {

View file

@ -36,6 +36,7 @@ import org.oxycblt.auxio.util.stateList
* A custom view that bundles together a seekbar with a current duration and a total duration. * A custom view that bundles together a seekbar with a current duration and a total duration.
* The sub-views are specifically laid out so that the seekbar has an adequate touch height while * The sub-views are specifically laid out so that the seekbar has an adequate touch height while
* still not having gobs of whitespace everywhere. * still not having gobs of whitespace everywhere.
* TODO: Add smooth seeking [i.e seeking in sub-second values]
* @author OxygenCobalt * @author OxygenCobalt
*/ */
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")

View file

@ -55,6 +55,9 @@ fun Fragment.newMenu(anchor: View, data: Item, flag: Int = ActionMenu.FLAG_NONE)
* @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM], [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details. * @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM], [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details.
* @throws IllegalStateException When there is no menu for this specific datatype/flag * @throws IllegalStateException When there is no menu for this specific datatype/flag
* @author OxygenCobalt * @author OxygenCobalt
* TODO: Stop scrolling when a menu is open
* TODO: Prevent duplicate menus from showing up
* TODO: Maybe replace this with a bottom sheet?
*/ */
class ActionMenu( class ActionMenu(
activity: AppCompatActivity, activity: AppCompatActivity,