image: refactor views

Refactor StyledImageView and ImageGroup into a new class called
CoverView.

This new view is more sensibly designed and should be capable of
handling non-square album covers when implemented.
This commit is contained in:
Alexander Capehart 2023-05-28 15:29:38 -06:00
parent 10d05b1f26
commit b7c15e0cc5
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
30 changed files with 517 additions and 611 deletions

View file

@ -85,7 +85,16 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
editedPlaylist: List<Song>?,
listener: DetailHeaderAdapter.Listener
) {
binding.detailCover.bind(playlist, editedPlaylist)
if (editedPlaylist != null) {
logD("Binding edited playlist image")
binding.detailCover.bind(
editedPlaylist,
binding.context.getString(R.string.desc_playlist_image, playlist.name),
R.drawable.ic_playlist_24)
} else {
binding.detailCover.bind(playlist)
}
binding.detailType.text = binding.context.getString(R.string.lbl_playlist)
binding.detailName.text = playlist.name.resolve(binding.context)
// Nothing about a playlist is applicable to the sub-head text.

View file

@ -22,6 +22,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
@ -162,31 +163,33 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
fun bind(song: Song, listener: SelectableListListener<Song>) {
listener.bind(song, this, menuButton = binding.songMenu)
binding.songTrack.apply {
if (song.track != null) {
// Instead of an album cover, we show the track number, as the song list
// within the album detail view would have homogeneous album covers otherwise.
val track = song.track
if (track != null) {
binding.songTrackCover.contentDescription =
binding.context.getString(R.string.desc_track_number, track)
binding.songTrackText.apply {
isVisible = true
text = context.getString(R.string.fmt_number, song.track)
isInvisible = false
contentDescription = context.getString(R.string.desc_track_number, song.track)
} else {
// No track, do not show a number, instead showing a generic icon.
text = ""
isInvisible = true
contentDescription = context.getString(R.string.def_track)
}
binding.songTrackPlaceholder.isInvisible = true
} else {
binding.songTrackCover.contentDescription =
binding.context.getString(R.string.def_track)
binding.songTrackText.apply {
isInvisible = true
text = null
}
binding.songTrackPlaceholder.isVisible = true
}
binding.songName.text = song.name.resolve(binding.context)
// Use duration instead of album or artist for each song, as this text would
// be homogenous otherwise.
// Use duration instead of album or artist for each song to be more contextually relevant.
binding.songDuration.text = song.durationMs.formatDurationMs(false)
}
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.songTrackBg.isPlaying = isPlaying
binding.songTrackCover.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {

View file

@ -110,7 +110,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying
binding.parentImage.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@ -162,7 +162,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.songAlbumCover.isPlaying = isPlaying
binding.songAlbumCover.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {

View file

@ -256,7 +256,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.interactBody.isSelected = isActive
binding.songAlbumCover.isPlaying = isPlaying
binding.songAlbumCover.setPlaying(isPlaying)
}
override fun updateEditing(editing: Boolean) {

View file

@ -97,16 +97,14 @@ constructor(
ImageRequest.Builder(context)
.data(listOf(song))
// Use ORIGINAL sizing, as we are not loading into any View-like component.
.size(Size.ORIGINAL)
.transformations(SquareFrameTransform.INSTANCE))
.size(Size.ORIGINAL))
// Override the target in order to deliver the bitmap to the given
// listener.
.transformations(SquareFrameTransform.INSTANCE)
.target(
onSuccess = {
synchronized(this) {
if (currentHandle == handle) {
// Has not been superseded by a new request, can deliver
// this result.
target.onCompleted(it.toBitmap())
}
}
@ -114,8 +112,6 @@ constructor(
onError = {
synchronized(this) {
if (currentHandle == handle) {
// Has not been superseded by a new request, can deliver
// this result.
target.onCompleted(null)
}
}

View file

@ -0,0 +1,349 @@
/*
* Copyright (c) 2023 Auxio Project
* CoverView.kt is part of Auxio.
*
* 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.animation.ValueAnimator
import android.annotation.SuppressLint
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 android.view.Gravity
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.children
import androidx.core.view.updateMarginsRelative
import coil.ImageLoader
import coil.request.ImageRequest
import coil.util.CoilUtils
import com.google.android.material.R as MR
import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.extractor.SquareFrameTransform
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.getDrawableCompat
import org.oxycblt.auxio.util.getInteger
/**
* Auxio's extension of [ImageView] that enables cover art loading and playing indicator and
* selection badge. In practice, it's three [ImageView]'s in a [FrameLayout] trenchcoat. By default,
* all of this functionality is enabled. The playback indicator and selection badge selectively
* disabled with the "playbackIndicatorEnabled" and "selectionBadgeEnabled" attributes, and image
* itself can be overridden if populated like a normal [FrameLayout].
*
* @author Alexander Capehart (OxygenCobalt)
*
* TODO: Enable non-square covers as soon as I can confirm that my workaround is okay
*/
@AndroidEntryPoint
class CoverView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) {
@Inject lateinit var imageLoader: ImageLoader
@Inject lateinit var uiSettings: UISettings
private val image: ImageView
private val playbackIndicator: PlaybackIndicatorView?
private val selectionBadge: ImageView?
private val cornerRadius: Float
private var fadeAnimator: ValueAnimator? = null
init {
// Obtain some StyledImageView attributes to use later when theming the custom view.
@SuppressLint("CustomViewStyleable")
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.CoverView)
// Keep track of our corner radius so that we can apply the same attributes to the custom
// view.
cornerRadius =
if (uiSettings.roundMode) {
styledAttrs.getDimension(R.styleable.CoverView_cornerRadius, 0f)
} else {
0f
}
val playbackIndicatorEnabled =
styledAttrs.getBoolean(R.styleable.CoverView_enablePlaybackIndicator, true)
val selectionBadgeEnabled =
styledAttrs.getBoolean(R.styleable.CoverView_enableSelectionBadge, true)
styledAttrs.recycle()
image = ImageView(context, attrs)
// Initialize the playback indicator if enabled.
playbackIndicator =
if (playbackIndicatorEnabled) {
PlaybackIndicatorView(context)
} else {
null
}
// Initialize the selection badge if enabled.
selectionBadge =
if (selectionBadgeEnabled) {
ImageView(context).apply {
imageTintList = context.getAttrColorCompat(MR.attr.colorOnPrimary)
setImageResource(R.drawable.ic_check_20)
setBackgroundResource(R.drawable.ui_selection_badge_bg)
}
} else {
null
}
}
override fun onFinishInflate() {
super.onFinishInflate()
// The image isn't added if other children have populated the body. This is by design.
if (childCount == 0) {
addView(image)
}
playbackIndicator?.let(::addView)
// Add backgrounds to each children. This creates visual consistency between each view,
// and also enables views to be hidden without clunky visibility changes.
for (child in children) {
child.apply {
// If there are rounded corners, we want to make sure view content will be cropped
// with it.
clipToOutline = true
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorCompat(R.color.sel_cover_bg)
setCornerSize(cornerRadius)
}
}
}
// The selection badge has it's own background we don't want overridden, add it after
// all other elements.
selectionBadge?.let {
addView(
it,
// Position the selection badge to the bottom right.
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
// Override the layout params of the indicator so that it's in the
// bottom left corner.
gravity = Gravity.BOTTOM or Gravity.END
val spacing = context.getDimenPixels(R.dimen.spacing_tiny)
updateMarginsRelative(bottom = spacing, end = spacing)
})
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
invalidateRootAlpha()
invalidatePlaybackIndicatorAlpha(playbackIndicator ?: return)
invalidateSelectionIndicatorAlpha(selectionBadge ?: return)
}
override fun setSelected(selected: Boolean) {
super.setSelected(selected)
invalidateRootAlpha()
invalidatePlaybackIndicatorAlpha(playbackIndicator ?: return)
}
override fun setActivated(activated: Boolean) {
super.setActivated(activated)
invalidateSelectionIndicatorAlpha(selectionBadge ?: return)
}
/**
* Set if the playback indicator should be indicated ongoing or paused playback.
*
* @param playing Whether playback is ongoing or paused.
*/
fun setPlaying(playing: Boolean) {
playbackIndicator?.setPlaying(playing)
}
private fun invalidateRootAlpha() {
alpha = if (isSelected || isEnabled) 1f else 0.5f
}
private fun invalidatePlaybackIndicatorAlpha(playbackIndicator: ImageView) {
playbackIndicator.alpha = if (isSelected) 1f else 0f
}
private fun invalidateSelectionIndicatorAlpha(selectionBadge: ImageView) {
// Set up a target transition for the selection indicator.
val targetAlpha: Float
val targetDuration: Long
if (isActivated) {
// View is "activated" (i.e marked as selected), so show the selection indicator.
targetAlpha = 1f
targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else {
// View is not "activated", hide the selection indicator.
targetAlpha = 0f
targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong()
}
if (selectionBadge.alpha == targetAlpha) {
// Nothing to do.
return
}
if (!isLaidOut) {
// Not laid out, initialize it without animation before drawing.
selectionBadge.alpha = targetAlpha
return
}
if (fadeAnimator != null) {
// Cancel any previous animation.
fadeAnimator?.cancel()
fadeAnimator = null
}
fadeAnimator =
ValueAnimator.ofFloat(selectionBadge.alpha, targetAlpha).apply {
duration = targetDuration
addUpdateListener { selectionBadge.alpha = it.animatedValue as Float }
start()
}
}
/**
* Bind a [Song]'s image to this view.
*
* @param song The [Song] to bind to the view.
*/
fun bind(song: Song) = bind(song.album)
/**
* Bind an [Album]'s image to this view.
*
* @param album The [Album] to bind to the view.
*/
fun bind(album: Album) =
bind(
album.songs,
context.getString(R.string.desc_album_cover, album.name),
R.drawable.ic_album_24)
/**
* Bind an [Artist]'s image to this view.
*
* @param artist The [Artist] to bind to the view.
*/
fun bind(artist: Artist) =
bind(
artist.songs,
context.getString(R.string.desc_artist_image, artist.name),
R.drawable.ic_artist_24)
/**
* Bind a [Genre]'s image to this view.
*
* @param genre The [Genre] to bind to the view.
*/
fun bind(genre: Genre) =
bind(
genre.songs,
context.getString(R.string.desc_genre_image, genre.name),
R.drawable.ic_genre_24)
/**
* Bind a [Playlist]'s image to this view.
*
* @param playlist the [Playlist] to bind.
*/
fun bind(playlist: Playlist) =
bind(
playlist.songs,
context.getString(R.string.desc_playlist_image, playlist.name),
R.drawable.ic_playlist_24)
/**
* Bind the covers of a generic list of [Song]s.
*
* @param songs The [Song]s to bind.
* @param desc The content description to describe the bound data.
* @param errorRes The resource of the error drawable to use if the cover cannot be loaded.
*/
fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) {
val request =
ImageRequest.Builder(context)
.data(songs)
.error(StyledDrawable(context, context.getDrawableCompat(errorRes)))
.transformations(SquareFrameTransform.INSTANCE)
.target(image)
.build()
// Dispose of any previous image request and load a new image.
CoilUtils.dispose(image)
imageLoader.enqueue(request)
contentDescription = desc
}
private class StyledDrawable(context: Context, private val inner: Drawable) : Drawable() {
init {
// Re-tint the drawable to use the analogous "on surface" color for
// StyledImageView.
DrawableCompat.setTintList(inner, context.getColorCompat(R.color.sel_on_cover_bg))
}
override fun draw(canvas: Canvas) {
// Resize the drawable such that it's always 1/4 the size of the image and
// centered in the middle of the canvas.
val adjustWidth = bounds.width() / 4
val adjustHeight = bounds.height() / 4
inner.bounds.set(
adjustWidth,
adjustHeight,
bounds.width() - adjustWidth,
bounds.height() - adjustHeight)
inner.draw(canvas)
}
// Required drawable overrides. Just forward to the wrapped drawable.
override fun setAlpha(alpha: Int) {
inner.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
inner.colorFilter = colorFilter
}
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}
}

View file

@ -1,266 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
* ImageGroup.kt is part of Auxio.
*
* 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.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.annotation.AttrRes
import androidx.core.view.updateMarginsRelative
import com.google.android.material.R as MR
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.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDimenPixels
import org.oxycblt.auxio.util.getInteger
/**
* A super-charged [StyledImageView]. This class enables the following features in addition to
* [StyledImageView]:
* - A selection indicator
* - An activation (playback) indicator
* - Support for ONE custom view
*
* This class is primarily intended for list items. For other uses, [StyledImageView] is more
* suitable.
*
* @author Alexander Capehart (OxygenCobalt)
*
* TODO: Rework content descriptions here
* TODO: Attempt unification with StyledImageView with some kind of dynamic configuration to avoid
* superfluous elements
* TODO: Handle non-square covers by gracefully placing them in the layout
*/
class ImageGroup
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) {
private val innerImageView: StyledImageView
private var customView: View? = null
private val playbackIndicatorView: PlaybackIndicatorView
private val selectionIndicatorView: ImageView
private var fadeAnimator: ValueAnimator? = null
private val cornerRadius: Float
init {
// Obtain some StyledImageView attributes to use later when theming the custom view.
@SuppressLint("CustomViewStyleable")
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
// Keep track of our corner radius so that we can apply the same attributes to the custom
// view.
cornerRadius = styledAttrs.getDimension(R.styleable.StyledImageView_cornerRadius, 0f)
styledAttrs.recycle()
// Initialize what views we can here.
innerImageView = StyledImageView(context, attrs)
playbackIndicatorView =
PlaybackIndicatorView(context).apply { cornerRadius = this@ImageGroup.cornerRadius }
selectionIndicatorView =
ImageView(context).apply {
imageTintList = context.getAttrColorCompat(MR.attr.colorOnPrimary)
setImageResource(R.drawable.ic_check_20)
setBackgroundResource(R.drawable.ui_selection_badge_bg)
}
// The inner StyledImageView should be at the bottom and hidden by any other elements
// if they become visible.
addView(innerImageView)
}
override fun onFinishInflate() {
super.onFinishInflate()
// Due to innerImageView, the max child count is actually 2 and not 1.
check(childCount < 3) { "Only one custom view is allowed" }
// Get the second inflated child, making sure we customize it to align with
// the rest of this view.
customView =
getChildAt(1)?.apply {
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorCompat(R.color.sel_cover_bg)
setCornerSize(cornerRadius)
}
}
// Playback indicator should sit above the inner StyledImageView and custom view/
addView(playbackIndicatorView)
// Selection indicator should never be obscured, so place it at the top.
addView(
selectionIndicatorView,
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
// Override the layout params of the indicator so that it's in the
// bottom left corner.
gravity = Gravity.BOTTOM or Gravity.END
val spacing = context.getDimenPixels(R.dimen.spacing_tiny)
updateMarginsRelative(bottom = spacing, end = spacing)
})
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
// Initialize each component before this view is drawn.
invalidateImageAlpha()
invalidatePlayingIndicator()
invalidateSelectionIndicator()
}
override fun setActivated(activated: Boolean) {
super.setActivated(activated)
invalidateSelectionIndicator()
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
invalidateImageAlpha()
invalidatePlayingIndicator()
}
override fun setSelected(selected: Boolean) {
super.setSelected(selected)
invalidateImageAlpha()
invalidatePlayingIndicator()
}
/**
* Bind a [Song] to the internal [StyledImageView].
*
* @param song The [Song] to bind to the view.
* @see StyledImageView.bind
*/
fun bind(song: Song) = innerImageView.bind(song)
/**
* Bind a [Album] to the internal [StyledImageView].
*
* @param album The [Album] to bind to the view.
* @see StyledImageView.bind
*/
fun bind(album: Album) = innerImageView.bind(album)
/**
* Bind a [Genre] to the internal [StyledImageView].
*
* @param artist The [Artist] to bind to the view.
* @see StyledImageView.bind
*/
fun bind(artist: Artist) = innerImageView.bind(artist)
/**
* Bind a [Genre] to the internal [StyledImageView].
*
* @param genre The [Genre] to bind to the view.
* @see StyledImageView.bind
*/
fun bind(genre: Genre) = innerImageView.bind(genre)
/**
* Bind a [Playlist]'s image to the internal [StyledImageView].
*
* @param playlist the [Playlist] to bind.
* @see StyledImageView.bind
*/
fun bind(playlist: Playlist) = innerImageView.bind(playlist)
/**
* Whether this view should be indicated to have ongoing playback or not. See
* PlaybackIndicatorView for more information on what occurs here. Note: It's expected for this
* view to already be marked as playing with setSelected (not the same thing) before this is set
* to true.
*/
var isPlaying: Boolean
get() = playbackIndicatorView.isPlaying
set(value) {
playbackIndicatorView.isPlaying = value
}
private fun invalidateImageAlpha() {
// If this view is disabled, show it at half-opacity, *unless* it is also marked
// as playing, in which we still want to show it at full-opacity.
alpha = if (isSelected || isEnabled) 1f else 0.5f
}
private fun invalidatePlayingIndicator() {
if (isSelected) {
// View is "selected" (actually marked as playing), so show the playing indicator
// and hide all other elements except for the selection indicator.
// TODO: Animate the other indicators?
customView?.alpha = 0f
innerImageView.alpha = 0f
playbackIndicatorView.alpha = 1f
} else {
// View is not "selected", hide the playing indicator.
customView?.alpha = 1f
innerImageView.alpha = 1f
playbackIndicatorView.alpha = 0f
}
}
private fun invalidateSelectionIndicator() {
// Set up a target transition for the selection indicator.
val targetAlpha: Float
val targetDuration: Long
if (isActivated) {
// View is "activated" (i.e marked as selected), so show the selection indicator.
targetAlpha = 1f
targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong()
} else {
// View is not "activated", hide the selection indicator.
targetAlpha = 0f
targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong()
}
if (selectionIndicatorView.alpha == targetAlpha) {
// Nothing to do.
return
}
if (!isLaidOut) {
// Not laid out, initialize it without animation before drawing.
selectionIndicatorView.alpha = targetAlpha
return
}
if (fadeAnimator != null) {
// Cancel any previous animation.
fadeAnimator?.cancel()
fadeAnimator = null
}
fadeAnimator =
ValueAnimator.ofFloat(selectionIndicatorView.alpha, targetAlpha).apply {
duration = targetDuration
addUpdateListener { selectionIndicatorView.alpha = it.animatedValue as Float }
start()
}
}
}

View file

@ -26,12 +26,9 @@ import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.widget.ImageViewCompat
import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.max
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat
@ -39,8 +36,8 @@ import org.oxycblt.auxio.util.getDrawableCompat
* A view that displays an activation (i.e playback) indicator, with an accented styling and an
* animated equalizer icon.
*
* This is only meant for use with [ImageGroup]. Due to limitations with [AnimationDrawable]
* instances within custom views, this cannot be merged with [ImageGroup].
* This is only meant for use with [CoverView]. Due to limitations with [AnimationDrawable]
* instances within custom views, this cannot be merged with [CoverView].
*
* @author Alexander Capehart (OxygenCobalt)
*/
@ -56,39 +53,16 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private val indicatorMatrix = Matrix()
private val indicatorMatrixSrc = RectF()
private val indicatorMatrixDst = RectF()
@Inject lateinit var uiSettings: UISettings
/**
* The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius
* to this view without any attribute hacks.
*/
var cornerRadius = 0f
set(value) {
field = value
(background as? MaterialShapeDrawable)?.let { bg ->
if (uiSettings.roundMode) {
bg.setCornerSize(value)
} else {
bg.setCornerSize(0f)
}
}
}
/**
* Whether this view should be indicated to have ongoing playback or not. If true, the animated
* playing icon will be shown. If false, the static paused icon will be shown.
*/
var isPlaying: Boolean
get() = drawable == playingIndicatorDrawable
set(value) {
if (value) {
playingIndicatorDrawable.start()
setImageDrawable(playingIndicatorDrawable)
} else {
playingIndicatorDrawable.stop()
setImageDrawable(pausedIndicatorDrawable)
}
fun setPlaying(isPlaying: Boolean) {
if (isPlaying) {
playingIndicatorDrawable.start()
setImageDrawable(playingIndicatorDrawable)
} else {
playingIndicatorDrawable.stop()
setImageDrawable(pausedIndicatorDrawable)
}
}
init {
// We will need to manually re-scale the playing/paused drawables to align with
@ -96,19 +70,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
scaleType = ScaleType.MATRIX
// Tint the playing/paused drawables so they are harmonious with the background.
ImageViewCompat.setImageTintList(this, context.getColorCompat(R.color.sel_on_cover_bg))
// 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.
clipToOutline = true
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorCompat(R.color.sel_cover_bg)
setCornerSize(cornerRadius)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

View file

@ -1,201 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
* StyledImageView.kt is part of Auxio.
*
* 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.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import coil.ImageLoader
import coil.request.ImageRequest
import coil.util.CoilUtils
import com.google.android.material.shape.MaterialShapeDrawable
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.extractor.SquareFrameTransform
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat
/**
* An [AppCompatImageView] with some additional styling, including:
* - Tonal background
* - Rounded corners based on user preferences
* - Built-in support for binding image data or using a static icon with the same styling as
* placeholder drawables.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
class StyledImageView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
AppCompatImageView(context, attrs, defStyleAttr) {
@Inject lateinit var imageLoader: ImageLoader
@Inject lateinit var uiSettings: UISettings
init {
// Load view attributes
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.StyledImageView)
val staticIcon =
styledAttrs.getResourceId(
R.styleable.StyledImageView_staticIcon, ResourcesCompat.ID_NULL)
val cornerRadius = styledAttrs.getDimension(R.styleable.StyledImageView_cornerRadius, 0f)
styledAttrs.recycle()
if (staticIcon != ResourcesCompat.ID_NULL) {
// Use the static icon if specified for this image.
setImageDrawable(StyledDrawable(context, context.getDrawableCompat(staticIcon)))
}
// 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.
clipToOutline = true
background =
MaterialShapeDrawable().apply {
fillColor = context.getColorCompat(R.color.sel_cover_bg)
if (uiSettings.roundMode) {
// Only use the specified corner radius when round mode is enabled.
setCornerSize(cornerRadius)
}
}
}
/**
* Bind a [Song]'s album cover to this view, also updating the content description.
*
* @param song The [Song] to bind.
*/
fun bind(song: Song) = bind(song.album)
/**
* Bind an [Album]'s cover to this view, also updating the content description.
*
* @param album the [Album] to bind.
*/
fun bind(album: Album) = bind(album, R.drawable.ic_album_24, R.string.desc_album_cover)
/**
* Bind an [Artist]'s image to this view, also updating the content description.
*
* @param artist the [Artist] to bind.
*/
fun bind(artist: Artist) = bind(artist, R.drawable.ic_artist_24, R.string.desc_artist_image)
/**
* Bind an [Genre]'s image to this view, also updating the content description.
*
* @param genre the [Genre] to bind.
*/
fun bind(genre: Genre) = bind(genre, R.drawable.ic_genre_24, R.string.desc_genre_image)
/**
* Bind a [Playlist]'s image to this view, also updating the content description.
*
* @param playlist The [Playlist] to bind.
* @param songs [Song]s that can override the playlist image if it needs to differ for any
* reason.
*/
fun bind(playlist: Playlist, songs: List<Song>? = null) =
if (songs != null) {
bind(
songs,
context.getString(R.string.desc_playlist_image, playlist.name.resolve(context)),
R.drawable.ic_playlist_24)
} else {
bind(playlist, R.drawable.ic_playlist_24, R.string.desc_playlist_image)
}
private fun bind(parent: MusicParent, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
bind(parent.songs, context.getString(descRes, parent.name.resolve(context)), errorRes)
}
private fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) {
val request =
ImageRequest.Builder(context)
.data(songs)
.error(StyledDrawable(context, context.getDrawableCompat(errorRes)))
.transformations(SquareFrameTransform.INSTANCE)
.target(this)
.build()
// Dispose of any previous image request and load a new image.
CoilUtils.dispose(this)
imageLoader.enqueue(request)
contentDescription = desc
}
/**
* A [Drawable] wrapper that re-styles the drawable to better align with the style of
* [StyledImageView].
*
* @param context [Context] required for initialization.
* @param inner The [Drawable] to wrap.
*/
private class StyledDrawable(context: Context, private val inner: Drawable) : Drawable() {
init {
// Re-tint the drawable to use the analogous "on surface" color for
// StyledImageView.
DrawableCompat.setTintList(inner, context.getColorCompat(R.color.sel_on_cover_bg))
}
override fun draw(canvas: Canvas) {
// Resize the drawable such that it's always 1/4 the size of the image and
// centered in the middle of the canvas.
val adjustWidth = bounds.width() / 4
val adjustHeight = bounds.height() / 4
inner.bounds.set(
adjustWidth,
adjustHeight,
bounds.width() - adjustWidth,
bounds.height() - adjustHeight)
inner.draw(canvas)
}
// Required drawable overrides. Just forward to the wrapped drawable.
override fun setAlpha(alpha: Int) {
inner.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
inner.colorFilter = colorFilter
}
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}
}

View file

@ -43,6 +43,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.ByteArrayInputStream
import java.io.InputStream
import javax.inject.Inject
import kotlin.math.min
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.guava.asDeferred
import kotlinx.coroutines.withContext
@ -252,4 +253,21 @@ constructor(
val size = pxOrElse { 512 }
return if (size.mod(2) > 0) size + 1 else size
}
private fun transform(input: Bitmap, size: Size): Bitmap {
// Find the smaller dimension and then take a center portion of the image that
// has that size.
val dstSize = min(input.width, input.height)
val x = (input.width - dstSize) / 2
val y = (input.height - dstSize) / 2
val dst = Bitmap.createBitmap(input, x, y, dstSize, dstSize)
val desiredWidth = size.width.pxOrElse { dstSize }
val desiredHeight = size.height.pxOrElse { dstSize }
if (dstSize != desiredWidth || dstSize != desiredHeight) {
// Image is not the desired size, upscale it.
return Bitmap.createScaledBitmap(dst, desiredWidth, desiredHeight, true)
}
return dst
}
}

View file

@ -64,7 +64,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.songAlbumCover.isPlaying = isPlaying
binding.songAlbumCover.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@ -114,7 +114,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying
binding.parentImage.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@ -174,7 +174,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying
binding.parentImage.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@ -231,7 +231,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying
binding.parentImage.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {
@ -288,7 +288,7 @@ class PlaylistViewHolder private constructor(private val binding: ItemParentBind
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying
binding.parentImage.setPlaying(isPlaying)
}
override fun updateSelectionIndicator(isSelected: Boolean) {

View file

@ -167,7 +167,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.interactBody.isSelected = isActive
binding.songAlbumCover.isPlaying = isPlaying
binding.songAlbumCover.setPlaying(isPlaying)
}
companion object {

View file

@ -16,15 +16,16 @@
app:title="@string/lbl_playback"
tools:subtitle="@string/lbl_all_songs" />
<org.oxycblt.auxio.image.StyledImageView
<org.oxycblt.auxio.image.CoverView
android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full"
android:layout_margin="@dimen/spacing_medium"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toTopOf="@+id/playback_song"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
tools:staticIcon="@drawable/ic_song_24" />
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
<TextView
android:id="@+id/playback_song"

View file

@ -8,14 +8,15 @@
android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_mid_medium">
<org.oxycblt.auxio.image.StyledImageView
<org.oxycblt.auxio.image.CoverView
android:id="@+id/detail_cover"
style="@style/Widget.Auxio.Image.Huge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:staticIcon="@drawable/ic_song_24" />
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/detail_type"

View file

@ -9,13 +9,14 @@
android:paddingEnd="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_mid_medium">
<org.oxycblt.auxio.image.StyledImageView
<org.oxycblt.auxio.image.CoverView
android:id="@+id/detail_cover"
style="@style/Widget.Auxio.Image.Large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:staticIcon="@drawable/ic_song_24" />
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/detail_type"

View file

@ -16,15 +16,16 @@
app:title="@string/lbl_playback"
tools:subtitle="@string/lbl_all_songs" />
<org.oxycblt.auxio.image.StyledImageView
<org.oxycblt.auxio.image.CoverView
android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full"
android:layout_margin="@dimen/spacing_medium"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toTopOf="@+id/playback_song"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
tools:staticIcon="@drawable/ic_song_24" />
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
<TextView
android:id="@+id/playback_song"

View file

@ -7,14 +7,15 @@
android:layout_height="match_parent"
android:padding="@dimen/spacing_medium">
<org.oxycblt.auxio.image.StyledImageView
<org.oxycblt.auxio.image.CoverView
android:id="@+id/detail_cover"
style="@style/Widget.Auxio.Image.MidHuge"
app:layout_constraintDimensionRatio="1"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:staticIcon="@drawable/ic_song_24" />
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/detail_type"

View file

@ -6,13 +6,14 @@
android:layout_height="match_parent"
android:padding="@dimen/spacing_medium">
<org.oxycblt.auxio.image.StyledImageView
<org.oxycblt.auxio.image.CoverView
android:id="@+id/detail_cover"
style="@style/Widget.Auxio.Image.Huge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:staticIcon="@drawable/ic_song_24" />
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/detail_type"

View file

@ -6,14 +6,15 @@
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<org.oxycblt.auxio.image.StyledImageView
<org.oxycblt.auxio.image.CoverView
android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Small"
android:layout_margin="@dimen/spacing_small"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toTopOf="@+id/playback_progress_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:staticIcon="@drawable/ic_song_24" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/playback_song"

View file

@ -16,16 +16,17 @@
app:title="@string/lbl_playback"
tools:subtitle="@string/lbl_all_songs" />
<org.oxycblt.auxio.image.StyledImageView
<org.oxycblt.auxio.image.CoverView
android:id="@+id/playback_cover"
style="@style/Widget.Auxio.Image.Full"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_medium"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
app:layout_constraintVertical_chainStyle="packed"
tools:staticIcon="@drawable/ic_song_24" />
app:layout_constraintVertical_chainStyle="packed" />
<!-- Playback information is wrapped in a container so that marquee doesn't break -->

View file

@ -16,15 +16,25 @@
with us only overlaying the track number (and other elements) onto it.
-->
<org.oxycblt.auxio.image.ImageGroup
android:id="@+id/song_track_bg"
<org.oxycblt.auxio.image.CoverView
android:id="@+id/song_track_cover"
style="@style/Widget.Auxio.Image.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:staticIcon="@drawable/ic_song_24">
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/song_track_placeholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/ic_song_24"
android:scaleType="center"
android:contentDescription="@string/def_track"
android:visibility="invisible"
app:tint="@color/sel_on_cover_bg"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/song_track"
android:id="@+id/song_track_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end"
@ -44,7 +54,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="16" />
</org.oxycblt.auxio.image.ImageGroup>
</org.oxycblt.auxio.image.CoverView>
<TextView
@ -56,8 +66,8 @@
android:textColor="@color/sel_selectable_text_primary"
app:layout_constraintBottom_toTopOf="@+id/song_duration"
app:layout_constraintEnd_toStartOf="@+id/song_menu"
app:layout_constraintStart_toEndOf="@+id/song_track_bg"
app:layout_constraintTop_toTopOf="@+id/song_track_bg"
app:layout_constraintStart_toEndOf="@+id/song_track_cover"
app:layout_constraintTop_toTopOf="@+id/song_track_cover"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Song Name" />
@ -68,9 +78,9 @@
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_mid_medium"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/song_track_bg"
app:layout_constraintBottom_toBottomOf="@+id/song_track_cover"
app:layout_constraintEnd_toStartOf="@+id/song_menu"
app:layout_constraintStart_toEndOf="@+id/song_track_bg"
app:layout_constraintStart_toEndOf="@+id/song_track_cover"
app:layout_constraintTop_toBottomOf="@+id/song_name"
tools:text="16:16" />

View file

@ -10,14 +10,15 @@
android:paddingEnd="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_mid_medium">
<org.oxycblt.auxio.image.StyledImageView
<org.oxycblt.auxio.image.CoverView
android:id="@+id/detail_cover"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
style="@style/Widget.Auxio.Image.MidHuge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:staticIcon="@drawable/ic_song_24" />
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/detail_type"

View file

@ -10,15 +10,27 @@
android:paddingEnd="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_mid_medium">
<org.oxycblt.auxio.image.StyledImageView
android:id="@+id/disc_icon"
<org.oxycblt.auxio.image.CoverView
android:id="@+id/disc_cover"
style="@style/Widget.Auxio.Image.Small"
android:scaleType="matrix"
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:staticIcon="@drawable/ic_album_24"
tools:ignore="ContentDescription" />
tools:ignore="ContentDescription">
<ImageView
android:id="@+id/disc_icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/ic_add_24"
android:scaleType="center"
app:tint="@color/sel_on_cover_bg"
tools:ignore="ContentDescription" />
</org.oxycblt.auxio.image.CoverView>
<TextView
android:id="@+id/disc_number"
@ -29,7 +41,7 @@
android:textColor="@color/sel_selectable_text_primary"
app:layout_constraintBottom_toTopOf="@+id/disc_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/disc_icon"
app:layout_constraintStart_toEndOf="@+id/disc_cover"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Disc 1" />
@ -43,7 +55,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/disc_icon"
app:layout_constraintStart_toEndOf="@+id/disc_cover"
app:layout_constraintTop_toBottomOf="@+id/disc_number"
tools:text="Part 1" />

View file

@ -34,7 +34,7 @@
android:layout_height="wrap_content"
android:background="@drawable/ui_item_ripple">
<org.oxycblt.auxio.image.ImageGroup
<org.oxycblt.auxio.image.CoverView
android:id="@+id/song_album_cover"
style="@style/Widget.Auxio.Image.Small"
android:layout_marginStart="@dimen/spacing_medium"
@ -43,8 +43,7 @@
android:layout_marginBottom="@dimen/spacing_mid_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:staticIcon="@drawable/ic_song_24" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/song_name"

View file

@ -3,19 +3,33 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:background="?attr/selectableItemBackground"
android:paddingStart="@dimen/spacing_large"
android:paddingTop="@dimen/spacing_mid_medium"
android:paddingEnd="@dimen/spacing_large"
android:paddingBottom="@dimen/spacing_mid_medium">
<org.oxycblt.auxio.image.ImageGroup
<org.oxycblt.auxio.image.CoverView
android:id="@+id/picker_image"
style="@style/Widget.Auxio.Image.Small"
android:contentDescription="@string/lbl_new_playlist"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:staticIcon="@drawable/ic_add_24" />
app:enablePlaybackIndicator="false"
app:enableSelectionBadge="false">
<ImageView
android:id="@+id/picker_icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/ic_add_24"
android:scaleType="center"
app:tint="@color/sel_on_cover_bg"
tools:ignore="ContentDescription" />
</org.oxycblt.auxio.image.CoverView>
<TextView
android:id="@+id/picker_name"

View file

@ -10,13 +10,12 @@
android:paddingEnd="@dimen/spacing_mid_medium"
android:paddingBottom="@dimen/spacing_mid_medium">
<org.oxycblt.auxio.image.ImageGroup
<org.oxycblt.auxio.image.CoverView
android:id="@+id/parent_image"
style="@style/Widget.Auxio.Image.Medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:staticIcon="@drawable/ic_artist_24" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/parent_name"

View file

@ -10,13 +10,12 @@
android:paddingEnd="@dimen/spacing_large"
android:paddingBottom="@dimen/spacing_mid_medium">
<org.oxycblt.auxio.image.ImageGroup
<org.oxycblt.auxio.image.CoverView
android:id="@+id/picker_image"
style="@style/Widget.Auxio.Image.Small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:staticIcon="@drawable/ic_song_24" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/picker_name"

View file

@ -10,13 +10,12 @@
android:paddingEnd="@dimen/spacing_mid_medium"
android:paddingBottom="@dimen/spacing_mid_medium">
<org.oxycblt.auxio.image.ImageGroup
<org.oxycblt.auxio.image.CoverView
android:id="@+id/song_album_cover"
style="@style/Widget.Auxio.Image.Small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:staticIcon="@drawable/ic_song_24" />
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/song_name"

View file

@ -10,10 +10,10 @@
<integer name="anim_fade_enter_duration">200</integer>
<integer name="anim_fade_exit_duration">100</integer>
<declare-styleable name="StyledImageView">
<declare-styleable name="CoverView">
<attr name="cornerRadius" format="dimension" />
<attr name="staticIcon" format="reference" />
<attr name="useLargeIcon" format="boolean" />
<attr name="enablePlaybackIndicator" format="boolean" />
<attr name="enableSelectionBadge" format="boolean" />
</declare-styleable>
<declare-styleable name="IntListPreference">

View file

@ -47,21 +47,18 @@
<item name="android:layout_width">@dimen/size_cover_large</item>
<item name="android:layout_height">@dimen/size_cover_large</item>
<item name="cornerRadius">@dimen/size_corners_medium</item>
<item name="useLargeIcon">true</item>
</style>
<style name="Widget.Auxio.Image.MidHuge" parent="">
<item name="android:layout_width">@dimen/size_cover_mid_huge</item>
<item name="android:layout_height">@dimen/size_cover_mid_huge</item>
<item name="cornerRadius">@dimen/size_corners_medium</item>
<item name="useLargeIcon">true</item>
</style>
<style name="Widget.Auxio.Image.Huge" parent="">
<item name="android:layout_width">@dimen/size_cover_huge</item>
<item name="android:layout_height">@dimen/size_cover_huge</item>
<item name="cornerRadius">@dimen/size_corners_medium</item>
<item name="useLargeIcon">true</item>
</style>
<style name="Widget.Auxio.Image.Full" parent="">
@ -69,7 +66,6 @@
<item name="android:layout_height">0dp</item>
<item name="layout_constraintDimensionRatio">1</item>
<item name="cornerRadius">@dimen/size_corners_medium</item>
<item name="useLargeIcon">true</item>
</style>
<style name="Widget.Auxio.RecyclerView.Linear" parent="">