recycler: remove useless header ids

Remove useless id fields from Headers, replacing them with vlaues
related to their string resource.

String resources and disc numbers are more or less garunteed to be
unique in Auxio's context.
This commit is contained in:
OxygenCobalt 2022-08-01 10:36:53 -06:00
parent 257516643f
commit 35cfea78df
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
21 changed files with 177 additions and 184 deletions

View file

@ -21,7 +21,7 @@ at the cost of longer loading times
#### What's Improved #### What's Improved
- Migrated to better-looking motion transitions - Migrated to better-looking motion transitions
- App now exposes an (immutable) queue. - App now exposes an (immutable) queue to the MediaSession
#### What's Fixed #### What's Fixed
- Fixed default material theme being used before app shows up - Fixed default material theme being used before app shows up

View file

@ -142,7 +142,8 @@ class MainFragment :
isInvisible = alpha == 0f isInvisible = alpha == 0f
} }
binding.playbackSheet.translationZ = 3f * outPlaybackRatio binding.playbackSheet.translationZ =
requireContext().getDimenSafe(R.dimen.elevation_normal) * outPlaybackRatio
playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt() playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt()
binding.playbackBarFragment.apply { binding.playbackBarFragment.apply {

View file

@ -32,7 +32,6 @@ import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music

View file

@ -30,7 +30,6 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.detail
import android.app.Application import android.app.Application
import android.media.MediaExtractor import android.media.MediaExtractor
import android.media.MediaFormat import android.media.MediaFormat
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -27,8 +28,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.recycler.DiscHeader
import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
@ -214,7 +213,7 @@ class DetailViewModel(application: Application) :
private fun refreshAlbumData(album: Album) { private fun refreshAlbumData(album: Album) {
logD("Refreshing album data") logD("Refreshing album data")
val data = mutableListOf<Item>(album) val data = mutableListOf<Item>(album)
data.add(SortHeader(id = -2, R.string.lbl_songs)) data.add(SortHeader(R.string.lbl_songs))
// To create a good user experience regarding disc numbers, we intersperse // To create a good user experience regarding disc numbers, we intersperse
// items that show the disc number throughout the album's songs. In the case // items that show the disc number throughout the album's songs. In the case
@ -225,7 +224,7 @@ class DetailViewModel(application: Application) :
for (entry in byDisc.entries) { for (entry in byDisc.entries) {
val disc = entry.key val disc = entry.key
val discSongs = entry.value val discSongs = entry.value
data.add(DiscHeader(id = -2L - disc, disc)) // Ensure ID uniqueness data.add(DiscHeader(disc)) // Ensure ID uniqueness
data.addAll(discSongs) data.addAll(discSongs)
} }
} else { } else {
@ -240,33 +239,33 @@ class DetailViewModel(application: Application) :
val data = mutableListOf<Item>(artist) val data = mutableListOf<Item>(artist)
val albums = Sort(Sort.Mode.ByYear, false).albums(artist.albums) val albums = Sort(Sort.Mode.ByYear, false).albums(artist.albums)
val byGroup = val byReleaseGroup =
albums.groupBy { albums.groupBy {
if (it.releaseType == null) { if (it.releaseType == null) {
return@groupBy ArtistAlbumGrouping.ALBUMS return@groupBy R.string.lbl_albums
} }
when (it.releaseType.refinement) { when (it.releaseType.refinement) {
null -> null ->
when (it.releaseType) { when (it.releaseType) {
is ReleaseType.Album -> ArtistAlbumGrouping.ALBUMS is ReleaseType.Album -> R.string.lbl_albums
is ReleaseType.EP -> ArtistAlbumGrouping.EPS is ReleaseType.EP -> R.string.lbl_eps
is ReleaseType.Single -> ArtistAlbumGrouping.SINGLES is ReleaseType.Single -> R.string.lbl_singles
is ReleaseType.Compilation -> ArtistAlbumGrouping.COMPILATIONS is ReleaseType.Compilation -> R.string.lbl_compilations
is ReleaseType.Soundtrack -> ArtistAlbumGrouping.SOUNDTRACKS is ReleaseType.Soundtrack -> R.string.lbl_soundtracks
is ReleaseType.Mixtape -> ArtistAlbumGrouping.MIXTAPES is ReleaseType.Mixtape -> R.string.lbl_mixtapes
} }
ReleaseType.Refinement.LIVE -> ArtistAlbumGrouping.LIVE ReleaseType.Refinement.LIVE -> R.string.lbl_live_group
ReleaseType.Refinement.REMIX -> ArtistAlbumGrouping.REMIXES ReleaseType.Refinement.REMIX -> R.string.lbl_remix_group
} }
} }
for (entry in byGroup.entries.sortedBy { it.key }.withIndex()) { for (entry in byReleaseGroup.entries.sortedBy { it.key }) {
data.add(Header(-2L - entry.index, entry.value.key.stringRes)) data.add(Header(entry.key))
data.addAll(entry.value.value) data.addAll(entry.value)
} }
data.add(SortHeader(-2L - byGroup.entries.size, R.string.lbl_songs)) data.add(SortHeader(R.string.lbl_songs))
data.addAll(artistSort.songs(artist.songs)) data.addAll(artistSort.songs(artist.songs))
_artistData.value = data.toList() _artistData.value = data.toList()
} }
@ -274,7 +273,7 @@ class DetailViewModel(application: Application) :
private fun refreshGenreData(genre: Genre) { private fun refreshGenreData(genre: Genre) {
logD("Refreshing genre data") logD("Refreshing genre data")
val data = mutableListOf<Item>(genre) val data = mutableListOf<Item>(genre)
data.add(SortHeader(-2, R.string.lbl_songs)) data.add(SortHeader(R.string.lbl_songs))
data.addAll(genreSort.songs(genre.songs)) data.addAll(genreSort.songs(genre.songs))
_genreData.value = data _genreData.value = data
} }
@ -326,28 +325,14 @@ class DetailViewModel(application: Application) :
override fun onCleared() { override fun onCleared() {
musicStore.removeCallback(this) musicStore.removeCallback(this)
} }
}
private enum class ArtistAlbumGrouping : Comparable<ArtistAlbumGrouping> { data class SortHeader(@StringRes val string: Int) : Item() {
ALBUMS, override val id: Long
EPS, get() = string.toLong()
SINGLES, }
COMPILATIONS,
SOUNDTRACKS,
MIXTAPES,
REMIXES,
LIVE;
val stringRes: Int data class DiscHeader(val disc: Int) : Item() {
get() = override val id: Long
when (this) { get() = disc.toLong()
ALBUMS -> R.string.lbl_albums
EPS -> R.string.lbl_eps
SINGLES -> R.string.lbl_singles
COMPILATIONS -> R.string.lbl_compilations
SOUNDTRACKS -> R.string.lbl_soundtracks
MIXTAPES -> R.string.lbl_mixtapes
REMIXES -> R.string.lbl_remix_group
LIVE -> R.string.lbl_live_group
}
}
} }

View file

@ -30,7 +30,6 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre

View file

@ -25,6 +25,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemAlbumSongBinding import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
import org.oxycblt.auxio.detail.DiscHeader
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.recycler.BindingViewHolder import org.oxycblt.auxio.ui.recycler.BindingViewHolder
@ -129,7 +130,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
val songCount = context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size) val songCount = context.getPluralSafe(R.plurals.fmt_song_count, item.songs.size)
val duration = item.durationSecs.formatDuration(false) val duration = "<duration>"
text = text =
if (item.releaseType != null) { if (item.releaseType != null) {
@ -171,10 +172,9 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
} }
} }
data class DiscHeader(override val id: Long, val disc: Int) : Item()
class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
BindingViewHolder<DiscHeader, Unit>(binding.root) { BindingViewHolder<DiscHeader, Unit>(binding.root) {
override fun bind(item: DiscHeader, listener: Unit) { override fun bind(item: DiscHeader, listener: Unit) {
binding.discNo.textSafe = binding.context.getString(R.string.fmt_disc_no, item.disc) binding.discNo.textSafe = binding.context.getString(R.string.fmt_disc_no, item.disc)
} }

View file

@ -19,12 +19,12 @@ package org.oxycblt.auxio.detail.recycler
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import androidx.annotation.StringRes
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.ui.recycler.AsyncBackingData import org.oxycblt.auxio.ui.recycler.AsyncBackingData
import org.oxycblt.auxio.ui.recycler.BindingViewHolder import org.oxycblt.auxio.ui.recycler.BindingViewHolder
import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.Header
@ -127,8 +127,6 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
} }
} }
data class SortHeader(override val id: Long, @StringRes val string: Int) : Item()
class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
BindingViewHolder<SortHeader, DetailAdapter.Listener>(binding.root) { BindingViewHolder<SortHeader, DetailAdapter.Listener>(binding.root) {
override fun bind(item: SortHeader, listener: DetailAdapter.Listener) { override fun bind(item: SortHeader, listener: DetailAdapter.Listener) {

View file

@ -21,13 +21,8 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import kotlin.math.max
import org.oxycblt.auxio.ui.AuxioSheetBehavior import org.oxycblt.auxio.ui.AuxioSheetBehavior
import org.oxycblt.auxio.util.systemBarInsetsCompat
import org.oxycblt.auxio.util.systemGestureInsetsCompat
/** /**
* The coordinator layout behavior used for the playback sheet, hacking in the many fixes required * The coordinator layout behavior used for the playback sheet, hacking in the many fixes required
@ -36,8 +31,6 @@ import org.oxycblt.auxio.util.systemGestureInsetsCompat
*/ */
class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) : class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
AuxioSheetBehavior<V>(context, attributeSet) { AuxioSheetBehavior<V>(context, attributeSet) {
private var lastInsets: WindowInsets? = null
init { init {
isHideable = true isHideable = true
} }
@ -50,25 +43,10 @@ class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeS
event: MotionEvent event: MotionEvent
): Boolean = super.onInterceptTouchEvent(parent, child, event) && state != STATE_EXPANDED ): Boolean = super.onInterceptTouchEvent(parent, child, event) && state != STATE_EXPANDED
override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean {
val success = super.onLayoutChild(parent, child, layoutDirection)
(child as ViewGroup).apply {
setOnApplyWindowInsetsListener { _, insets ->
lastInsets = insets
val bars = insets.systemBarInsetsCompat
val gestures = insets.systemGestureInsetsCompat
peekHeight = getChildAt(0).measuredHeight + max(gestures.bottom, bars.bottom)
insets
}
}
return success
}
// Note: This is an extension to Auxio's vendored BottomSheetBehavior // Note: This is an extension to Auxio's vendored BottomSheetBehavior
override fun enableHidingGestures() = false override fun enableHidingGestures() = false
/** Hide this sheet in a safe manner. */
fun hideSafe() { fun hideSafe() {
if (state != STATE_HIDDEN) { if (state != STATE_HIDDEN) {
isDraggable = false isDraggable = false
@ -76,6 +54,7 @@ class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeS
} }
} }
/** Unhide this sheet in a safe manner. */
fun unhideSafe() { fun unhideSafe() {
if (state == STATE_HIDDEN) { if (state == STATE_HIDDEN) {
state = STATE_COLLAPSED state = STATE_COLLAPSED

View file

@ -138,10 +138,5 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe
playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition) playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition)
} }
override fun isLongPressDragEnabled(): Boolean = false override fun isLongPressDragEnabled() = false
companion object {
const val MINIMUM_INITIAL_DRAG_VELOCITY = 10
const val MAXIMUM_INITIAL_DRAG_VELOCITY = 25
}
} }

View file

@ -34,8 +34,6 @@ import org.oxycblt.auxio.util.logD
/** /**
* A [Fragment] that shows the queue and enables editing as well. * A [Fragment] that shows the queue and enables editing as well.
* *
* TODO: Improve index updates
*
* TODO: Test older versions * TODO: Test older versions
* *
* TODO: Test restoration and song loss * TODO: Test restoration and song loss
@ -105,6 +103,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
if (start != RecyclerView.NO_POSITION && if (start != RecyclerView.NO_POSITION &&
end != RecyclerView.NO_POSITION && end != RecyclerView.NO_POSITION &&
scrollTo !in start..end) { scrollTo !in start..end) {
logD("Scrolling to new position")
binding.queueRecycler.scrollToPosition(scrollTo) binding.queueRecycler.scrollToPosition(scrollTo)
} }
} }

View file

@ -20,13 +20,16 @@ package org.oxycblt.auxio.playback.queue
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.WindowInsets
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import kotlin.math.max
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.AuxioSheetBehavior import org.oxycblt.auxio.ui.AuxioSheetBehavior
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/**
* The bottom sheet behavior designed for the queue in particular.
* @author OxygenCobalt
*/
class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) : class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
AuxioSheetBehavior<V>(context, attributeSet) { AuxioSheetBehavior<V>(context, attributeSet) {
private var barHeight = 0 private var barHeight = 0
@ -45,25 +48,18 @@ class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?
child: V, child: V,
dependency: View dependency: View
): Boolean { ): Boolean {
val ok = super.onDependentViewChanged(parent, child, dependency)
barHeight = dependency.height barHeight = dependency.height
return ok return false // No change, just grabbed the height
} }
override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean { override fun applyWindowInsets(child: View, insets: WindowInsets): WindowInsets {
val success = super.onLayoutChild(parent, child, layoutDirection) super.applyWindowInsets(child, insets)
child.setOnApplyWindowInsetsListener { _, insets -> // Offset our expanded panel by the size of the playback bar, as that is shown when
// we slide up the panel.
val bars = insets.systemBarInsetsCompat val bars = insets.systemBarInsetsCompat
val gestures = insets.systemGestureInsetsCompat
expandedOffset = bars.top + barHeight + barSpacing expandedOffset = bars.top + barHeight + barSpacing
peekHeight = return insets.replaceSystemBarInsetsCompat(
(child as ViewGroup).getChildAt(0).height + max(gestures.bottom, bars.bottom)
insets.replaceSystemBarInsetsCompat(
bars.left, bars.top, bars.right, expandedOffset + bars.bottom) bars.left, bars.top, bars.right, expandedOffset + bars.bottom)
} }
return success
}
} }

View file

@ -25,6 +25,10 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
/**
* Class enabling more advanced queue list functionality and queue editing.
* @author OxygenCobalt
*/
class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.playback.state
import kotlin.math.max import kotlin.math.max
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -399,7 +400,14 @@ class PlaybackStateManager private constructor() {
suspend fun wipeState(database: PlaybackStateDatabase) { suspend fun wipeState(database: PlaybackStateDatabase) {
logD("Wiping state") logD("Wiping state")
withContext(Dispatchers.IO) { database.write(null) } withContext(Dispatchers.IO) {
delay(5000)
withContext(Dispatchers.Main) {
index = -1
notifyNewPlayback()
}
}
} }
/** Sanitize the state with [newLibrary]. */ /** Sanitize the state with [newLibrary]. */

View file

@ -88,28 +88,28 @@ class SearchViewModel(application: Application) :
if (filterMode == null || filterMode == DisplayMode.SHOW_ARTISTS) { if (filterMode == null || filterMode == DisplayMode.SHOW_ARTISTS) {
library.artists.filterArtistsBy(query)?.let { artists -> library.artists.filterArtistsBy(query)?.let { artists ->
results.add(Header(-1, R.string.lbl_artists)) results.add(Header(R.string.lbl_artists))
results.addAll(sort.artists(artists)) results.addAll(sort.artists(artists))
} }
} }
if (filterMode == null || filterMode == DisplayMode.SHOW_ALBUMS) { if (filterMode == null || filterMode == DisplayMode.SHOW_ALBUMS) {
library.albums.filterAlbumsBy(query)?.let { albums -> library.albums.filterAlbumsBy(query)?.let { albums ->
results.add(Header(-2, R.string.lbl_albums)) results.add(Header(R.string.lbl_albums))
results.addAll(sort.albums(albums)) results.addAll(sort.albums(albums))
} }
} }
if (filterMode == null || filterMode == DisplayMode.SHOW_GENRES) { if (filterMode == null || filterMode == DisplayMode.SHOW_GENRES) {
library.genres.filterGenresBy(query)?.let { genres -> library.genres.filterGenresBy(query)?.let { genres ->
results.add(Header(-3, R.string.lbl_genres)) results.add(Header(R.string.lbl_genres))
results.addAll(sort.genres(genres)) results.addAll(sort.genres(genres))
} }
} }
if (filterMode == null || filterMode == DisplayMode.SHOW_SONGS) { if (filterMode == null || filterMode == DisplayMode.SHOW_SONGS) {
library.songs.filterSongsBy(query)?.let { songs -> library.songs.filterSongsBy(query)?.let { songs ->
results.add(Header(-4, R.string.lbl_songs)) results.add(Header(R.string.lbl_songs))
results.addAll(sort.songs(songs)) results.addAll(sort.songs(songs))
} }
} }

View file

@ -23,42 +23,69 @@ import android.graphics.drawable.LayerDrawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowInsets
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/**
* Implements the fundamental bottom sheet attributes used across the entire app.
* @author OxygenCobalt
*/
abstract class AuxioSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) : abstract class AuxioSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
NeoBottomSheetBehavior<V>(context, attributeSet) { NeoBottomSheetBehavior<V>(context, attributeSet) {
private var elevationNormal = context.getDimenSafe(R.dimen.elevation_normal) private var setup = false
val sheetBackgroundDrawable = val sheetBackgroundDrawable =
MaterialShapeDrawable.createWithElevationOverlay(context).apply { MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList
elevation = elevationNormal elevation = context.getDimenSafe(R.dimen.elevation_normal)
} }
init { init {
// We need to disable isFitToContents for us to have our bottom sheet expand to the
// whole of the screen and not just whatever portion it takes up.
isFitToContents = false isFitToContents = false
} }
/** Called when the child the bottom sheet applies to receives window insets. */
open fun applyWindowInsets(child: View, insets: WindowInsets): WindowInsets {
// All sheet behaviors derive their peek height from the size of the "bar" (i.e the
// first child) plus the gesture insets.
val gestures = insets.systemGestureInsetsCompat
peekHeight = (child as ViewGroup).getChildAt(0).height + gestures.bottom
return insets
}
// Enable experimental settings to allow us to skip the half expanded state without
// dumb hacks.
override fun shouldSkipHalfExpandedStateWhenDragging() = true override fun shouldSkipHalfExpandedStateWhenDragging() = true
override fun shouldExpandOnUpwardDrag(dragDurationMillis: Long, yPositionPercentage: Float) = override fun shouldExpandOnUpwardDrag(dragDurationMillis: Long, yPositionPercentage: Float) =
true true
override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean { override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean {
val success = super.onLayoutChild(parent, child, layoutDirection) val layout = super.onLayoutChild(parent, child, layoutDirection)
(child as ViewGroup).apply { if (!setup) {
child.apply {
// Sometimes the sheet background will fade out, so guard it with another
// colorSurface drawable to prevent content overlap.
background = background =
LayerDrawable( LayerDrawable(
arrayOf( arrayOf(
ColorDrawable(context.getAttrColorSafe(R.attr.colorSurface)), ColorDrawable(context.getAttrColorSafe(R.attr.colorSurface)),
sheetBackgroundDrawable)) sheetBackgroundDrawable))
// Try to disable drop shadows if possible.
disableDropShadowCompat() disableDropShadowCompat()
setOnApplyWindowInsetsListener(::applyWindowInsets)
} }
return success setup = true
}
return layout
} }
} }

View file

@ -28,12 +28,54 @@ import org.oxycblt.auxio.util.coordinatorLayoutBehavior
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
import org.oxycblt.auxio.util.systemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat
/**
* A behavior that automatically re-layouts and re-insets content to align with the parent layout's
* bottom sheet.
* @author OxygenCobalt
*/
class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: AttributeSet?) : class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
CoordinatorLayout.Behavior<V>(context, attributeSet) { CoordinatorLayout.Behavior<V>(context, attributeSet) {
private var lastInsets: WindowInsets? = null
private var dep: View? = null private var dep: View? = null
private var setup: Boolean = false private var lastInsets: WindowInsets? = null
private var lastConsumed: Int? = null private var lastConsumed: Int? = null
private var setup: Boolean = false
override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean {
if (dependency.coordinatorLayoutBehavior is NeoBottomSheetBehavior) {
dep = dependency
return true
}
return false
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: V,
dependency: View
): Boolean {
val behavior = dependency.coordinatorLayoutBehavior as NeoBottomSheetBehavior
val consumed = behavior.calculateConsumedByBar()
if (consumed < Int.MIN_VALUE) {
return false
}
if (consumed != lastConsumed) {
lastConsumed = consumed
val insets = lastInsets
if (insets != null) {
child.dispatchApplyWindowInsets(insets)
}
lastInsets?.let(child::dispatchApplyWindowInsets)
measureContent(parent, child, consumed)
layoutContent(child)
return true
}
return false
}
override fun onMeasureChild( override fun onMeasureChild(
parent: CoordinatorLayout, parent: CoordinatorLayout,
@ -107,41 +149,4 @@ class BottomSheetContentBehavior<V : View>(context: Context, attributeSet: Attri
(peekHeight * (1 - abs(offset))).toInt() (peekHeight * (1 - abs(offset))).toInt()
} }
} }
override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean {
if (dependency.coordinatorLayoutBehavior is NeoBottomSheetBehavior) {
dep = dependency
return true
}
return false
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: V,
dependency: View
): Boolean {
val behavior = dependency.coordinatorLayoutBehavior as NeoBottomSheetBehavior
val consumed = behavior.calculateConsumedByBar()
if (consumed < Int.MIN_VALUE) {
return false
}
if (consumed != lastConsumed) {
lastConsumed = consumed
val insets = lastInsets
if (insets != null) {
child.dispatchApplyWindowInsets(insets)
}
lastInsets?.let(child::dispatchApplyWindowInsets)
measureContent(parent, child, consumed)
layoutContent(child)
return true
}
return false
}
} }

View file

@ -166,10 +166,12 @@ abstract class Item {
/** A data object used solely for the "Header" UI element. */ /** A data object used solely for the "Header" UI element. */
data class Header( data class Header(
override val id: Long,
/** The string resource used for the header. */ /** The string resource used for the header. */
@StringRes val string: Int @StringRes val string: Int
) : Item() ) : Item() {
override val id: Long
get() = string.toLong()
}
/** /**
* Represents data that backs a [MonoAdapter] or [MultiAdapter]. This can be implemented by any * Represents data that backs a [MonoAdapter] or [MultiAdapter]. This can be implemented by any

View file

@ -23,7 +23,6 @@ import android.content.res.ColorStateList
import android.database.Cursor import android.database.Cursor
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.graphics.Insets import android.graphics.Insets
import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.view.View import android.view.View
@ -241,48 +240,46 @@ val AndroidViewModel.application: Application
fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) = fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
query(tableName, null, null, null, null, null, null)?.use(block) query(tableName, null, null, null, null, null, null)?.use(block)
// Note: WindowInsetsCompat and it's related methods are a non-functional mess that does not // Note: WindowInsetsCompat and it's related methods are an over-engineered mess that does not
// work for Auxio's use-case. Use our own methods instead. // work for Auxio's use-case. Use our own compat methods instead.
/** /**
* Resolve system bar insets in a version-aware manner. This can be used to apply padding to a view * Resolve system bar insets in a version-aware manner. This can be used to apply padding to a view
* that properly follows all the frustrating changes that were made between Android 8-11. * that properly follows all the frustrating changes that were made between Android 8-11.
*/ */
val WindowInsets.systemBarInsetsCompat: Rect val WindowInsets.systemBarInsetsCompat: Insets
get() = get() =
when { when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
getInsets(WindowInsets.Type.systemBars()).run { Rect(left, top, right, bottom) } getInsets(WindowInsets.Type.systemBars())
} }
else -> { else -> {
@Suppress("DEPRECATION") @Suppress("DEPRECATION") systemWindowInsets
Rect(
systemWindowInsetLeft,
systemWindowInsetTop,
systemWindowInsetRight,
systemWindowInsetBottom)
} }
} }
/** /**
* Resolve gesture insets in a version-aware manner. This can be used to apply padding to a view * Resolve gesture insets in a version-aware manner. This can be used to apply padding to a view
* that properly follows all the frustrating changes that were made between Android 8-11. * that properly follows all the frustrating changes that were made between Android 8-11. Note that
* if the gesture insets are not present (i.e zeroed), the bar insets will be used instead.
*/ */
val WindowInsets.systemGestureInsetsCompat: Rect val WindowInsets.systemGestureInsetsCompat: Insets
get() = get() =
// The reasoning for why we take a larger inset is because gesture insets are seemingly
// not present on some android versions. To prevent the app from appearing below the
// system bars, we fall back to the bar insets. This is guaranteed not to fire in any
// context but the gesture insets being invalid, as gesture insets are intended to
// be larger than bar insets.
when { when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
getInsets(WindowInsets.Type.systemGestures()).run { Rect(left, top, right, bottom) } Insets.max(
getInsets(WindowInsets.Type.systemGestures()),
getInsets(WindowInsets.Type.systemBars()))
} }
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
@Suppress("DEPRECATION") val gestureInsets = systemGestureInsets @Suppress("DEPRECATION") Insets.max(systemGestureInsets, systemWindowInsets)
Rect(
gestureInsets.left,
gestureInsets.top,
gestureInsets.right,
gestureInsets.bottom)
} }
else -> Rect(0, 0, 0, 0) else -> Insets.of(0, 0, 0, 0)
} }
/** /**

View file

@ -46,12 +46,12 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/handle_wrapper" android:id="@+id/handle_wrapper"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="64dp"> android:layout_height="@dimen/size_bottom_sheet_bar">
<ImageView <ImageView
android:id="@+id/handle" android:id="@+id/handle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="@dimen/size_btn"
android:scaleType="center" android:scaleType="center"
android:paddingBottom="@dimen/spacing_small" android:paddingBottom="@dimen/spacing_small"
android:src="@drawable/ic_down_24" android:src="@drawable/ic_down_24"

View file

@ -26,6 +26,7 @@
<dimen name="size_corners_mid_large">24dp</dimen> <dimen name="size_corners_mid_large">24dp</dimen>
<dimen name="size_btn">48dp</dimen> <dimen name="size_btn">48dp</dimen>
<dimen name="size_bottom_sheet_bar">64dp</dimen>
<dimen name="size_play_pause_button">72dp</dimen> <dimen name="size_play_pause_button">72dp</dimen>
<dimen name="size_icon_small">24dp</dimen> <dimen name="size_icon_small">24dp</dimen>
@ -38,7 +39,6 @@
<dimen name="text_size_track_number_step">2sp</dimen> <dimen name="text_size_track_number_step">2sp</dimen>
<!-- Misc --> <!-- Misc -->
<dimen name="elevation_small">2dp</dimen>
<dimen name="elevation_normal">3dp</dimen> <dimen name="elevation_normal">3dp</dimen>
<dimen name="fast_scroll_popup_min_width">78dp</dimen> <dimen name="fast_scroll_popup_min_width">78dp</dimen>