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:
parent
10d05b1f26
commit
b7c15e0cc5
30 changed files with 517 additions and 611 deletions
|
@ -85,7 +85,16 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
|
||||||
editedPlaylist: List<Song>?,
|
editedPlaylist: List<Song>?,
|
||||||
listener: DetailHeaderAdapter.Listener
|
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.detailType.text = binding.context.getString(R.string.lbl_playlist)
|
||||||
binding.detailName.text = playlist.name.resolve(binding.context)
|
binding.detailName.text = playlist.name.resolve(binding.context)
|
||||||
// Nothing about a playlist is applicable to the sub-head text.
|
// Nothing about a playlist is applicable to the sub-head text.
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
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>) {
|
fun bind(song: Song, listener: SelectableListListener<Song>) {
|
||||||
listener.bind(song, this, menuButton = binding.songMenu)
|
listener.bind(song, this, menuButton = binding.songMenu)
|
||||||
|
|
||||||
binding.songTrack.apply {
|
val track = song.track
|
||||||
if (song.track != null) {
|
if (track != null) {
|
||||||
// Instead of an album cover, we show the track number, as the song list
|
binding.songTrackCover.contentDescription =
|
||||||
// within the album detail view would have homogeneous album covers otherwise.
|
binding.context.getString(R.string.desc_track_number, track)
|
||||||
|
binding.songTrackText.apply {
|
||||||
|
isVisible = true
|
||||||
text = context.getString(R.string.fmt_number, song.track)
|
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)
|
binding.songName.text = song.name.resolve(binding.context)
|
||||||
|
// Use duration instead of album or artist for each song to be more contextually relevant.
|
||||||
// Use duration instead of album or artist for each song, as this text would
|
|
||||||
// be homogenous otherwise.
|
|
||||||
binding.songDuration.text = song.durationMs.formatDurationMs(false)
|
binding.songDuration.text = song.durationMs.formatDurationMs(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
binding.root.isSelected = isActive
|
binding.root.isSelected = isActive
|
||||||
binding.songTrackBg.isPlaying = isPlaying
|
binding.songTrackCover.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
|
|
@ -110,7 +110,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
||||||
|
|
||||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
binding.root.isSelected = isActive
|
binding.root.isSelected = isActive
|
||||||
binding.parentImage.isPlaying = isPlaying
|
binding.parentImage.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
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) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
binding.root.isSelected = isActive
|
binding.root.isSelected = isActive
|
||||||
binding.songAlbumCover.isPlaying = isPlaying
|
binding.songAlbumCover.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
|
|
@ -256,7 +256,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
|
||||||
|
|
||||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
binding.interactBody.isSelected = isActive
|
binding.interactBody.isSelected = isActive
|
||||||
binding.songAlbumCover.isPlaying = isPlaying
|
binding.songAlbumCover.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateEditing(editing: Boolean) {
|
override fun updateEditing(editing: Boolean) {
|
||||||
|
|
|
@ -97,16 +97,14 @@ constructor(
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(listOf(song))
|
.data(listOf(song))
|
||||||
// Use ORIGINAL sizing, as we are not loading into any View-like component.
|
// Use ORIGINAL sizing, as we are not loading into any View-like component.
|
||||||
.size(Size.ORIGINAL)
|
.size(Size.ORIGINAL))
|
||||||
.transformations(SquareFrameTransform.INSTANCE))
|
|
||||||
// Override the target in order to deliver the bitmap to the given
|
// Override the target in order to deliver the bitmap to the given
|
||||||
// listener.
|
// listener.
|
||||||
|
.transformations(SquareFrameTransform.INSTANCE)
|
||||||
.target(
|
.target(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (currentHandle == handle) {
|
if (currentHandle == handle) {
|
||||||
// Has not been superseded by a new request, can deliver
|
|
||||||
// this result.
|
|
||||||
target.onCompleted(it.toBitmap())
|
target.onCompleted(it.toBitmap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,8 +112,6 @@ constructor(
|
||||||
onError = {
|
onError = {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (currentHandle == handle) {
|
if (currentHandle == handle) {
|
||||||
// Has not been superseded by a new request, can deliver
|
|
||||||
// this result.
|
|
||||||
target.onCompleted(null)
|
target.onCompleted(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
349
app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
Normal file
349
app/src/main/java/org/oxycblt/auxio/image/CoverView.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -26,12 +26,9 @@ import android.util.AttributeSet
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
import androidx.core.widget.ImageViewCompat
|
import androidx.core.widget.ImageViewCompat
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.ui.UISettings
|
|
||||||
import org.oxycblt.auxio.util.getColorCompat
|
import org.oxycblt.auxio.util.getColorCompat
|
||||||
import org.oxycblt.auxio.util.getDrawableCompat
|
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
|
* A view that displays an activation (i.e playback) indicator, with an accented styling and an
|
||||||
* animated equalizer icon.
|
* animated equalizer icon.
|
||||||
*
|
*
|
||||||
* This is only meant for use with [ImageGroup]. Due to limitations with [AnimationDrawable]
|
* This is only meant for use with [CoverView]. Due to limitations with [AnimationDrawable]
|
||||||
* instances within custom views, this cannot be merged with [ImageGroup].
|
* instances within custom views, this cannot be merged with [CoverView].
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -56,32 +53,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
private val indicatorMatrix = Matrix()
|
private val indicatorMatrix = Matrix()
|
||||||
private val indicatorMatrixSrc = RectF()
|
private val indicatorMatrixSrc = RectF()
|
||||||
private val indicatorMatrixDst = RectF()
|
private val indicatorMatrixDst = RectF()
|
||||||
@Inject lateinit var uiSettings: UISettings
|
|
||||||
|
|
||||||
/**
|
fun setPlaying(isPlaying: Boolean) {
|
||||||
* The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius
|
if (isPlaying) {
|
||||||
* 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()
|
playingIndicatorDrawable.start()
|
||||||
setImageDrawable(playingIndicatorDrawable)
|
setImageDrawable(playingIndicatorDrawable)
|
||||||
} else {
|
} else {
|
||||||
|
@ -96,19 +70,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
scaleType = ScaleType.MATRIX
|
scaleType = ScaleType.MATRIX
|
||||||
// Tint the playing/paused drawables so they are harmonious with the background.
|
// Tint the playing/paused drawables so they are harmonious with the background.
|
||||||
ImageViewCompat.setImageTintList(this, context.getColorCompat(R.color.sel_on_cover_bg))
|
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) {
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -43,6 +43,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.min
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.guava.asDeferred
|
import kotlinx.coroutines.guava.asDeferred
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -252,4 +253,21 @@ constructor(
|
||||||
val size = pxOrElse { 512 }
|
val size = pxOrElse { 512 }
|
||||||
return if (size.mod(2) > 0) size + 1 else size
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
|
|
||||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
binding.root.isSelected = isActive
|
binding.root.isSelected = isActive
|
||||||
binding.songAlbumCover.isPlaying = isPlaying
|
binding.songAlbumCover.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
@ -114,7 +114,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
|
|
||||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
binding.root.isSelected = isActive
|
binding.root.isSelected = isActive
|
||||||
binding.parentImage.isPlaying = isPlaying
|
binding.parentImage.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
@ -174,7 +174,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
||||||
|
|
||||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
binding.root.isSelected = isActive
|
binding.root.isSelected = isActive
|
||||||
binding.parentImage.isPlaying = isPlaying
|
binding.parentImage.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
@ -231,7 +231,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
|
||||||
|
|
||||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
binding.root.isSelected = isActive
|
binding.root.isSelected = isActive
|
||||||
binding.parentImage.isPlaying = isPlaying
|
binding.parentImage.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
@ -288,7 +288,7 @@ class PlaylistViewHolder private constructor(private val binding: ItemParentBind
|
||||||
|
|
||||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
binding.root.isSelected = isActive
|
binding.root.isSelected = isActive
|
||||||
binding.parentImage.isPlaying = isPlaying
|
binding.parentImage.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
|
|
@ -167,7 +167,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS
|
||||||
|
|
||||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
binding.interactBody.isSelected = isActive
|
binding.interactBody.isSelected = isActive
|
||||||
binding.songAlbumCover.isPlaying = isPlaying
|
binding.songAlbumCover.setPlaying(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -16,15 +16,16 @@
|
||||||
app:title="@string/lbl_playback"
|
app:title="@string/lbl_playback"
|
||||||
tools:subtitle="@string/lbl_all_songs" />
|
tools:subtitle="@string/lbl_all_songs" />
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/playback_cover"
|
android:id="@+id/playback_cover"
|
||||||
style="@style/Widget.Auxio.Image.Full"
|
style="@style/Widget.Auxio.Image.Full"
|
||||||
android:layout_margin="@dimen/spacing_medium"
|
android:layout_margin="@dimen/spacing_medium"
|
||||||
|
app:enablePlaybackIndicator="false"
|
||||||
|
app:enableSelectionBadge="false"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_song"
|
app:layout_constraintBottom_toTopOf="@+id/playback_song"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
|
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playback_song"
|
android:id="@+id/playback_song"
|
||||||
|
|
|
@ -8,14 +8,15 @@
|
||||||
android:paddingStart="@dimen/spacing_medium"
|
android:paddingStart="@dimen/spacing_medium"
|
||||||
android:paddingEnd="@dimen/spacing_medium"
|
android:paddingEnd="@dimen/spacing_medium"
|
||||||
android:paddingBottom="@dimen/spacing_mid_medium">
|
android:paddingBottom="@dimen/spacing_mid_medium">
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/detail_cover"
|
android:id="@+id/detail_cover"
|
||||||
style="@style/Widget.Auxio.Image.Huge"
|
style="@style/Widget.Auxio.Image.Huge"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:ignore="ContentDescription"
|
app:enablePlaybackIndicator="false"
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
app:enableSelectionBadge="false"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/detail_type"
|
android:id="@+id/detail_type"
|
||||||
|
|
|
@ -9,13 +9,14 @@
|
||||||
android:paddingEnd="@dimen/spacing_medium"
|
android:paddingEnd="@dimen/spacing_medium"
|
||||||
android:paddingBottom="@dimen/spacing_mid_medium">
|
android:paddingBottom="@dimen/spacing_mid_medium">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/detail_cover"
|
android:id="@+id/detail_cover"
|
||||||
style="@style/Widget.Auxio.Image.Large"
|
style="@style/Widget.Auxio.Image.Large"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:ignore="ContentDescription"
|
app:enablePlaybackIndicator="false"
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
app:enableSelectionBadge="false"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/detail_type"
|
android:id="@+id/detail_type"
|
||||||
|
|
|
@ -16,15 +16,16 @@
|
||||||
app:title="@string/lbl_playback"
|
app:title="@string/lbl_playback"
|
||||||
tools:subtitle="@string/lbl_all_songs" />
|
tools:subtitle="@string/lbl_all_songs" />
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/playback_cover"
|
android:id="@+id/playback_cover"
|
||||||
style="@style/Widget.Auxio.Image.Full"
|
style="@style/Widget.Auxio.Image.Full"
|
||||||
android:layout_margin="@dimen/spacing_medium"
|
android:layout_margin="@dimen/spacing_medium"
|
||||||
|
app:enablePlaybackIndicator="false"
|
||||||
|
app:enableSelectionBadge="false"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_song"
|
app:layout_constraintBottom_toTopOf="@+id/playback_song"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
|
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar" />
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playback_song"
|
android:id="@+id/playback_song"
|
||||||
|
|
|
@ -7,14 +7,15 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="@dimen/spacing_medium">
|
android:padding="@dimen/spacing_medium">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/detail_cover"
|
android:id="@+id/detail_cover"
|
||||||
style="@style/Widget.Auxio.Image.MidHuge"
|
style="@style/Widget.Auxio.Image.MidHuge"
|
||||||
app:layout_constraintDimensionRatio="1"
|
app:layout_constraintDimensionRatio="1"
|
||||||
|
app:enablePlaybackIndicator="false"
|
||||||
|
app:enableSelectionBadge="false"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:ignore="ContentDescription"
|
tools:ignore="ContentDescription" />
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/detail_type"
|
android:id="@+id/detail_type"
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="@dimen/spacing_medium">
|
android:padding="@dimen/spacing_medium">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/detail_cover"
|
android:id="@+id/detail_cover"
|
||||||
style="@style/Widget.Auxio.Image.Huge"
|
style="@style/Widget.Auxio.Image.Huge"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:ignore="ContentDescription"
|
app:enablePlaybackIndicator="false"
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
app:enableSelectionBadge="false"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/detail_type"
|
android:id="@+id/detail_type"
|
||||||
|
|
|
@ -6,14 +6,15 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/playback_cover"
|
android:id="@+id/playback_cover"
|
||||||
style="@style/Widget.Auxio.Image.Small"
|
style="@style/Widget.Auxio.Image.Small"
|
||||||
android:layout_margin="@dimen/spacing_small"
|
android:layout_margin="@dimen/spacing_small"
|
||||||
|
app:enablePlaybackIndicator="false"
|
||||||
|
app:enableSelectionBadge="false"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_progress_container"
|
app:layout_constraintBottom_toTopOf="@+id/playback_progress_container"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playback_song"
|
android:id="@+id/playback_song"
|
||||||
|
|
|
@ -16,16 +16,17 @@
|
||||||
app:title="@string/lbl_playback"
|
app:title="@string/lbl_playback"
|
||||||
tools:subtitle="@string/lbl_all_songs" />
|
tools:subtitle="@string/lbl_all_songs" />
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/playback_cover"
|
android:id="@+id/playback_cover"
|
||||||
style="@style/Widget.Auxio.Image.Full"
|
style="@style/Widget.Auxio.Image.Full"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
android:layout_marginTop="@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_constraintBottom_toTopOf="@+id/playback_seek_bar"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
|
app:layout_constraintTop_toBottomOf="@+id/playback_toolbar"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed" />
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Playback information is wrapped in a container so that marquee doesn't break -->
|
<!-- Playback information is wrapped in a container so that marquee doesn't break -->
|
||||||
|
|
|
@ -16,15 +16,25 @@
|
||||||
with us only overlaying the track number (and other elements) onto it.
|
with us only overlaying the track number (and other elements) onto it.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.ImageGroup
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/song_track_bg"
|
android:id="@+id/song_track_cover"
|
||||||
style="@style/Widget.Auxio.Image.Small"
|
style="@style/Widget.Auxio.Image.Small"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
app:staticIcon="@drawable/ic_song_24">
|
|
||||||
|
<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
|
<TextView
|
||||||
android:id="@+id/song_track"
|
android:id="@+id/song_track_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
|
@ -44,7 +54,7 @@
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="16" />
|
tools:text="16" />
|
||||||
|
|
||||||
</org.oxycblt.auxio.image.ImageGroup>
|
</org.oxycblt.auxio.image.CoverView>
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -56,8 +66,8 @@
|
||||||
android:textColor="@color/sel_selectable_text_primary"
|
android:textColor="@color/sel_selectable_text_primary"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/song_duration"
|
app:layout_constraintBottom_toTopOf="@+id/song_duration"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/song_menu"
|
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_toTopOf="@+id/song_track_bg"
|
app:layout_constraintTop_toTopOf="@+id/song_track_cover"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
tools:text="Song Name" />
|
tools:text="Song Name" />
|
||||||
|
|
||||||
|
@ -68,9 +78,9 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="@dimen/spacing_mid_medium"
|
android:layout_marginEnd="@dimen/spacing_mid_medium"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
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_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"
|
app:layout_constraintTop_toBottomOf="@+id/song_name"
|
||||||
tools:text="16:16" />
|
tools:text="16:16" />
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,15 @@
|
||||||
android:paddingEnd="@dimen/spacing_medium"
|
android:paddingEnd="@dimen/spacing_medium"
|
||||||
android:paddingBottom="@dimen/spacing_mid_medium">
|
android:paddingBottom="@dimen/spacing_mid_medium">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/detail_cover"
|
android:id="@+id/detail_cover"
|
||||||
|
app:enablePlaybackIndicator="false"
|
||||||
|
app:enableSelectionBadge="false"
|
||||||
style="@style/Widget.Auxio.Image.MidHuge"
|
style="@style/Widget.Auxio.Image.MidHuge"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:ignore="ContentDescription"
|
tools:ignore="ContentDescription" />
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/detail_type"
|
android:id="@+id/detail_type"
|
||||||
|
|
|
@ -10,16 +10,28 @@
|
||||||
android:paddingEnd="@dimen/spacing_medium"
|
android:paddingEnd="@dimen/spacing_medium"
|
||||||
android:paddingBottom="@dimen/spacing_mid_medium">
|
android:paddingBottom="@dimen/spacing_mid_medium">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.StyledImageView
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/disc_icon"
|
android:id="@+id/disc_cover"
|
||||||
style="@style/Widget.Auxio.Image.Small"
|
style="@style/Widget.Auxio.Image.Small"
|
||||||
android:scaleType="matrix"
|
android:scaleType="matrix"
|
||||||
|
app:enablePlaybackIndicator="false"
|
||||||
|
app:enableSelectionBadge="false"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:staticIcon="@drawable/ic_album_24"
|
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" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
</org.oxycblt.auxio.image.CoverView>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/disc_number"
|
android:id="@+id/disc_number"
|
||||||
style="@style/Widget.Auxio.TextView.Item.Primary"
|
style="@style/Widget.Auxio.TextView.Item.Primary"
|
||||||
|
@ -29,7 +41,7 @@
|
||||||
android:textColor="@color/sel_selectable_text_primary"
|
android:textColor="@color/sel_selectable_text_primary"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/disc_name"
|
app:layout_constraintBottom_toTopOf="@+id/disc_name"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
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_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
tools:text="Disc 1" />
|
tools:text="Disc 1" />
|
||||||
|
@ -43,7 +55,7 @@
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:visibility="gone"
|
tools:visibility="gone"
|
||||||
app:layout_constraintStart_toEndOf="@+id/disc_icon"
|
app:layout_constraintStart_toEndOf="@+id/disc_cover"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/disc_number"
|
app:layout_constraintTop_toBottomOf="@+id/disc_number"
|
||||||
tools:text="Part 1" />
|
tools:text="Part 1" />
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/ui_item_ripple">
|
android:background="@drawable/ui_item_ripple">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.ImageGroup
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/song_album_cover"
|
android:id="@+id/song_album_cover"
|
||||||
style="@style/Widget.Auxio.Image.Small"
|
style="@style/Widget.Auxio.Image.Small"
|
||||||
android:layout_marginStart="@dimen/spacing_medium"
|
android:layout_marginStart="@dimen/spacing_medium"
|
||||||
|
@ -43,8 +43,7 @@
|
||||||
android:layout_marginBottom="@dimen/spacing_mid_medium"
|
android:layout_marginBottom="@dimen/spacing_mid_medium"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/song_name"
|
android:id="@+id/song_name"
|
||||||
|
|
|
@ -3,19 +3,33 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:background="?attr/selectableItemBackground"
|
android:background="?attr/selectableItemBackground"
|
||||||
android:paddingStart="@dimen/spacing_large"
|
android:paddingStart="@dimen/spacing_large"
|
||||||
android:paddingTop="@dimen/spacing_mid_medium"
|
android:paddingTop="@dimen/spacing_mid_medium"
|
||||||
android:paddingEnd="@dimen/spacing_large"
|
android:paddingEnd="@dimen/spacing_large"
|
||||||
android:paddingBottom="@dimen/spacing_mid_medium">
|
android:paddingBottom="@dimen/spacing_mid_medium">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.ImageGroup
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/picker_image"
|
android:id="@+id/picker_image"
|
||||||
style="@style/Widget.Auxio.Image.Small"
|
style="@style/Widget.Auxio.Image.Small"
|
||||||
|
android:contentDescription="@string/lbl_new_playlist"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="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
|
<TextView
|
||||||
android:id="@+id/picker_name"
|
android:id="@+id/picker_name"
|
||||||
|
|
|
@ -10,13 +10,12 @@
|
||||||
android:paddingEnd="@dimen/spacing_mid_medium"
|
android:paddingEnd="@dimen/spacing_mid_medium"
|
||||||
android:paddingBottom="@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"
|
android:id="@+id/parent_image"
|
||||||
style="@style/Widget.Auxio.Image.Medium"
|
style="@style/Widget.Auxio.Image.Medium"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
tools:staticIcon="@drawable/ic_artist_24" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/parent_name"
|
android:id="@+id/parent_name"
|
||||||
|
|
|
@ -10,13 +10,12 @@
|
||||||
android:paddingEnd="@dimen/spacing_large"
|
android:paddingEnd="@dimen/spacing_large"
|
||||||
android:paddingBottom="@dimen/spacing_mid_medium">
|
android:paddingBottom="@dimen/spacing_mid_medium">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.ImageGroup
|
<org.oxycblt.auxio.image.CoverView
|
||||||
android:id="@+id/picker_image"
|
android:id="@+id/picker_image"
|
||||||
style="@style/Widget.Auxio.Image.Small"
|
style="@style/Widget.Auxio.Image.Small"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/picker_name"
|
android:id="@+id/picker_name"
|
||||||
|
|
|
@ -10,13 +10,12 @@
|
||||||
android:paddingEnd="@dimen/spacing_mid_medium"
|
android:paddingEnd="@dimen/spacing_mid_medium"
|
||||||
android:paddingBottom="@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"
|
android:id="@+id/song_album_cover"
|
||||||
style="@style/Widget.Auxio.Image.Small"
|
style="@style/Widget.Auxio.Image.Small"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
tools:staticIcon="@drawable/ic_song_24" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/song_name"
|
android:id="@+id/song_name"
|
||||||
|
|
|
@ -10,10 +10,10 @@
|
||||||
<integer name="anim_fade_enter_duration">200</integer>
|
<integer name="anim_fade_enter_duration">200</integer>
|
||||||
<integer name="anim_fade_exit_duration">100</integer>
|
<integer name="anim_fade_exit_duration">100</integer>
|
||||||
|
|
||||||
<declare-styleable name="StyledImageView">
|
<declare-styleable name="CoverView">
|
||||||
<attr name="cornerRadius" format="dimension" />
|
<attr name="cornerRadius" format="dimension" />
|
||||||
<attr name="staticIcon" format="reference" />
|
<attr name="enablePlaybackIndicator" format="boolean" />
|
||||||
<attr name="useLargeIcon" format="boolean" />
|
<attr name="enableSelectionBadge" format="boolean" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
<declare-styleable name="IntListPreference">
|
<declare-styleable name="IntListPreference">
|
||||||
|
|
|
@ -47,21 +47,18 @@
|
||||||
<item name="android:layout_width">@dimen/size_cover_large</item>
|
<item name="android:layout_width">@dimen/size_cover_large</item>
|
||||||
<item name="android:layout_height">@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="cornerRadius">@dimen/size_corners_medium</item>
|
||||||
<item name="useLargeIcon">true</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Auxio.Image.MidHuge" parent="">
|
<style name="Widget.Auxio.Image.MidHuge" parent="">
|
||||||
<item name="android:layout_width">@dimen/size_cover_mid_huge</item>
|
<item name="android:layout_width">@dimen/size_cover_mid_huge</item>
|
||||||
<item name="android:layout_height">@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="cornerRadius">@dimen/size_corners_medium</item>
|
||||||
<item name="useLargeIcon">true</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Auxio.Image.Huge" parent="">
|
<style name="Widget.Auxio.Image.Huge" parent="">
|
||||||
<item name="android:layout_width">@dimen/size_cover_huge</item>
|
<item name="android:layout_width">@dimen/size_cover_huge</item>
|
||||||
<item name="android:layout_height">@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="cornerRadius">@dimen/size_corners_medium</item>
|
||||||
<item name="useLargeIcon">true</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Auxio.Image.Full" parent="">
|
<style name="Widget.Auxio.Image.Full" parent="">
|
||||||
|
@ -69,7 +66,6 @@
|
||||||
<item name="android:layout_height">0dp</item>
|
<item name="android:layout_height">0dp</item>
|
||||||
<item name="layout_constraintDimensionRatio">1</item>
|
<item name="layout_constraintDimensionRatio">1</item>
|
||||||
<item name="cornerRadius">@dimen/size_corners_medium</item>
|
<item name="cornerRadius">@dimen/size_corners_medium</item>
|
||||||
<item name="useLargeIcon">true</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Auxio.RecyclerView.Linear" parent="">
|
<style name="Widget.Auxio.RecyclerView.Linear" parent="">
|
||||||
|
|
Loading…
Reference in a new issue