From 35cfea78dff7f34db023888f69391f01f4f62cef Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Mon, 1 Aug 2022 10:36:53 -0600 Subject: [PATCH] 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. --- CHANGELOG.md | 2 +- .../java/org/oxycblt/auxio/MainFragment.kt | 3 +- .../auxio/detail/AlbumDetailFragment.kt | 1 - .../auxio/detail/ArtistDetailFragment.kt | 1 - .../oxycblt/auxio/detail/DetailViewModel.kt | 71 +++++++--------- .../auxio/detail/GenreDetailFragment.kt | 1 - .../detail/recycler/AlbumDetailAdapter.kt | 6 +- .../auxio/detail/recycler/DetailAdapter.kt | 4 +- .../auxio/playback/PlaybackSheetBehavior.kt | 25 +----- .../auxio/playback/queue/QueueDragCallback.kt | 7 +- .../auxio/playback/queue/QueueFragment.kt | 3 +- .../playback/queue/QueueSheetBehavior.kt | 32 ++++--- .../auxio/playback/queue/QueueViewModel.kt | 4 + .../playback/state/PlaybackStateManager.kt | 10 ++- .../oxycblt/auxio/search/SearchViewModel.kt | 8 +- .../oxycblt/auxio/ui/AuxioSheetBehavior.kt | 49 ++++++++--- .../auxio/ui/BottomSheetContentBehavior.kt | 83 ++++++++++--------- .../auxio/ui/recycler/RecyclerFramework.kt | 6 +- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 39 ++++----- app/src/main/res/layout/fragment_main.xml | 4 +- app/src/main/res/values/dimens.xml | 2 +- 21 files changed, 177 insertions(+), 184 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e533c92f1..add5edd0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ at the cost of longer loading times #### What's Improved - Migrated to better-looking motion transitions -- App now exposes an (immutable) queue. +- App now exposes an (immutable) queue to the MediaSession #### What's Fixed - Fixed default material theme being used before app shows up diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index d22bcbe11..376826c65 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -142,7 +142,8 @@ class MainFragment : isInvisible = alpha == 0f } - binding.playbackSheet.translationZ = 3f * outPlaybackRatio + binding.playbackSheet.translationZ = + requireContext().getDimenSafe(R.dimen.elevation_normal) * outPlaybackRatio playbackSheetBehavior.sheetBackgroundDrawable.alpha = (outPlaybackRatio * 255).toInt() binding.playbackBarFragment.apply { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 044beb778..5e4a50650 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -32,7 +32,6 @@ import com.google.android.material.transition.MaterialSharedAxis import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding 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.Artist import org.oxycblt.auxio.music.Music diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 5d987e82d..f39244d3a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -30,7 +30,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter 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.Artist import org.oxycblt.auxio.music.Music diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 7d77468d6..9393b185a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.detail import android.app.Application import android.media.MediaExtractor import android.media.MediaFormat +import androidx.annotation.StringRes import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers @@ -27,8 +28,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch 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.settings.Settings import org.oxycblt.auxio.ui.Sort @@ -214,7 +213,7 @@ class DetailViewModel(application: Application) : private fun refreshAlbumData(album: Album) { logD("Refreshing album data") val data = mutableListOf(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 // 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) { val disc = entry.key val discSongs = entry.value - data.add(DiscHeader(id = -2L - disc, disc)) // Ensure ID uniqueness + data.add(DiscHeader(disc)) // Ensure ID uniqueness data.addAll(discSongs) } } else { @@ -240,33 +239,33 @@ class DetailViewModel(application: Application) : val data = mutableListOf(artist) val albums = Sort(Sort.Mode.ByYear, false).albums(artist.albums) - val byGroup = + val byReleaseGroup = albums.groupBy { if (it.releaseType == null) { - return@groupBy ArtistAlbumGrouping.ALBUMS + return@groupBy R.string.lbl_albums } when (it.releaseType.refinement) { null -> when (it.releaseType) { - is ReleaseType.Album -> ArtistAlbumGrouping.ALBUMS - is ReleaseType.EP -> ArtistAlbumGrouping.EPS - is ReleaseType.Single -> ArtistAlbumGrouping.SINGLES - is ReleaseType.Compilation -> ArtistAlbumGrouping.COMPILATIONS - is ReleaseType.Soundtrack -> ArtistAlbumGrouping.SOUNDTRACKS - is ReleaseType.Mixtape -> ArtistAlbumGrouping.MIXTAPES + is ReleaseType.Album -> R.string.lbl_albums + is ReleaseType.EP -> R.string.lbl_eps + is ReleaseType.Single -> R.string.lbl_singles + is ReleaseType.Compilation -> R.string.lbl_compilations + is ReleaseType.Soundtrack -> R.string.lbl_soundtracks + is ReleaseType.Mixtape -> R.string.lbl_mixtapes } - ReleaseType.Refinement.LIVE -> ArtistAlbumGrouping.LIVE - ReleaseType.Refinement.REMIX -> ArtistAlbumGrouping.REMIXES + ReleaseType.Refinement.LIVE -> R.string.lbl_live_group + ReleaseType.Refinement.REMIX -> R.string.lbl_remix_group } } - for (entry in byGroup.entries.sortedBy { it.key }.withIndex()) { - data.add(Header(-2L - entry.index, entry.value.key.stringRes)) - data.addAll(entry.value.value) + for (entry in byReleaseGroup.entries.sortedBy { it.key }) { + data.add(Header(entry.key)) + 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)) _artistData.value = data.toList() } @@ -274,7 +273,7 @@ class DetailViewModel(application: Application) : private fun refreshGenreData(genre: Genre) { logD("Refreshing genre data") val data = mutableListOf(genre) - data.add(SortHeader(-2, R.string.lbl_songs)) + data.add(SortHeader(R.string.lbl_songs)) data.addAll(genreSort.songs(genre.songs)) _genreData.value = data } @@ -326,28 +325,14 @@ class DetailViewModel(application: Application) : override fun onCleared() { musicStore.removeCallback(this) } - - private enum class ArtistAlbumGrouping : Comparable { - ALBUMS, - EPS, - SINGLES, - COMPILATIONS, - SOUNDTRACKS, - MIXTAPES, - REMIXES, - LIVE; - - val stringRes: Int - get() = - when (this) { - 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 - } - } +} + +data class SortHeader(@StringRes val string: Int) : Item() { + override val id: Long + get() = string.toLong() +} + +data class DiscHeader(val disc: Int) : Item() { + override val id: Long + get() = disc.toLong() } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 230a15c6f..ac699c62a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -30,7 +30,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.DetailAdapter 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.Artist import org.oxycblt.auxio.music.Genre diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index 4fb21312f..d36bfa00f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -25,6 +25,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemAlbumSongBinding import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding +import org.oxycblt.auxio.detail.DiscHeader import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Song 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 duration = item.durationSecs.formatDuration(false) + val duration = "" text = 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) : BindingViewHolder(binding.root) { + override fun bind(item: DiscHeader, listener: Unit) { binding.discNo.textSafe = binding.context.getString(R.string.fmt_disc_no, item.disc) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index 49f1d7dcf..1013398e4 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -19,12 +19,12 @@ package org.oxycblt.auxio.detail.recycler import android.content.Context import android.view.View -import androidx.annotation.StringRes import androidx.appcompat.widget.TooltipCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable 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.BindingViewHolder import org.oxycblt.auxio.ui.recycler.Header @@ -127,8 +127,6 @@ abstract class DetailAdapter( } } -data class SortHeader(override val id: Long, @StringRes val string: Int) : Item() - class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : BindingViewHolder(binding.root) { override fun bind(item: SortHeader, listener: DetailAdapter.Listener) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSheetBehavior.kt index 387707d35..5a4140625 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSheetBehavior.kt @@ -21,13 +21,8 @@ import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import android.view.View -import android.view.ViewGroup -import android.view.WindowInsets import androidx.coordinatorlayout.widget.CoordinatorLayout -import kotlin.math.max 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 @@ -36,8 +31,6 @@ import org.oxycblt.auxio.util.systemGestureInsetsCompat */ class PlaybackSheetBehavior(context: Context, attributeSet: AttributeSet?) : AuxioSheetBehavior(context, attributeSet) { - private var lastInsets: WindowInsets? = null - init { isHideable = true } @@ -50,25 +43,10 @@ class PlaybackSheetBehavior(context: Context, attributeSet: AttributeS event: MotionEvent ): 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 override fun enableHidingGestures() = false + /** Hide this sheet in a safe manner. */ fun hideSafe() { if (state != STATE_HIDDEN) { isDraggable = false @@ -76,6 +54,7 @@ class PlaybackSheetBehavior(context: Context, attributeSet: AttributeS } } + /** Unhide this sheet in a safe manner. */ fun unhideSafe() { if (state == STATE_HIDDEN) { state = STATE_COLLAPSED diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt index 360a2a72f..4b09b91ff 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt @@ -138,10 +138,5 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe playbackModel.removeQueueDataItem(viewHolder.bindingAdapterPosition) } - override fun isLongPressDragEnabled(): Boolean = false - - companion object { - const val MINIMUM_INITIAL_DRAG_VELOCITY = 10 - const val MAXIMUM_INITIAL_DRAG_VELOCITY = 25 - } + override fun isLongPressDragEnabled() = false } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 8c8bb65e4..0f44fe934 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -34,8 +34,6 @@ import org.oxycblt.auxio.util.logD /** * A [Fragment] that shows the queue and enables editing as well. * - * TODO: Improve index updates - * * TODO: Test older versions * * TODO: Test restoration and song loss @@ -105,6 +103,7 @@ class QueueFragment : ViewBindingFragment(), QueueItemList if (start != RecyclerView.NO_POSITION && end != RecyclerView.NO_POSITION && scrollTo !in start..end) { + logD("Scrolling to new position") binding.queueRecycler.scrollToPosition(scrollTo) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueSheetBehavior.kt index 89d323f99..4fc9e1001 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueSheetBehavior.kt @@ -20,13 +20,16 @@ package org.oxycblt.auxio.playback.queue import android.content.Context import android.util.AttributeSet import android.view.View -import android.view.ViewGroup +import android.view.WindowInsets import androidx.coordinatorlayout.widget.CoordinatorLayout -import kotlin.math.max import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.AuxioSheetBehavior import org.oxycblt.auxio.util.* +/** + * The bottom sheet behavior designed for the queue in particular. + * @author OxygenCobalt + */ class QueueSheetBehavior(context: Context, attributeSet: AttributeSet?) : AuxioSheetBehavior(context, attributeSet) { private var barHeight = 0 @@ -45,25 +48,18 @@ class QueueSheetBehavior(context: Context, attributeSet: AttributeSet? child: V, dependency: View ): Boolean { - val ok = super.onDependentViewChanged(parent, child, dependency) barHeight = dependency.height - return ok + return false // No change, just grabbed the height } - override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean { - val success = super.onLayoutChild(parent, child, layoutDirection) + override fun applyWindowInsets(child: View, insets: WindowInsets): WindowInsets { + super.applyWindowInsets(child, insets) - child.setOnApplyWindowInsetsListener { _, insets -> - val bars = insets.systemBarInsetsCompat - val gestures = insets.systemGestureInsetsCompat - - expandedOffset = bars.top + barHeight + barSpacing - peekHeight = - (child as ViewGroup).getChildAt(0).height + max(gestures.bottom, bars.bottom) - insets.replaceSystemBarInsetsCompat( - bars.left, bars.top, bars.right, expandedOffset + bars.bottom) - } - - return success + // 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 + expandedOffset = bars.top + barHeight + barSpacing + return insets.replaceSystemBarInsetsCompat( + bars.left, bars.top, bars.right, expandedOffset + bars.bottom) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index c36f9f13a..386bb63b4 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -25,6 +25,10 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager +/** + * Class enabling more advanced queue list functionality and queue editing. + * @author OxygenCobalt + */ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { private val playbackManager = PlaybackStateManager.getInstance() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 5dabd54f2..017dda70c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.playback.state import kotlin.math.max import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.Album @@ -399,7 +400,14 @@ class PlaybackStateManager private constructor() { suspend fun wipeState(database: PlaybackStateDatabase) { 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]. */ diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 835125104..198662228 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -88,28 +88,28 @@ class SearchViewModel(application: Application) : if (filterMode == null || filterMode == DisplayMode.SHOW_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)) } } if (filterMode == null || filterMode == DisplayMode.SHOW_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)) } } if (filterMode == null || filterMode == DisplayMode.SHOW_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)) } } if (filterMode == null || filterMode == DisplayMode.SHOW_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)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/AuxioSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/AuxioSheetBehavior.kt index a24023413..32f0efd4e 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/AuxioSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/AuxioSheetBehavior.kt @@ -23,42 +23,69 @@ import android.graphics.drawable.LayerDrawable import android.util.AttributeSet import android.view.View import android.view.ViewGroup +import android.view.WindowInsets import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.bottomsheet.NeoBottomSheetBehavior import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R import org.oxycblt.auxio.util.* +/** + * Implements the fundamental bottom sheet attributes used across the entire app. + * @author OxygenCobalt + */ abstract class AuxioSheetBehavior(context: Context, attributeSet: AttributeSet?) : NeoBottomSheetBehavior(context, attributeSet) { - private var elevationNormal = context.getDimenSafe(R.dimen.elevation_normal) + private var setup = false val sheetBackgroundDrawable = MaterialShapeDrawable.createWithElevationOverlay(context).apply { fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList - elevation = elevationNormal + elevation = context.getDimenSafe(R.dimen.elevation_normal) } 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 } + /** 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 shouldExpandOnUpwardDrag(dragDurationMillis: Long, yPositionPercentage: Float) = true 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 { - background = - LayerDrawable( - arrayOf( - ColorDrawable(context.getAttrColorSafe(R.attr.colorSurface)), - sheetBackgroundDrawable)) + if (!setup) { + child.apply { + // Sometimes the sheet background will fade out, so guard it with another + // colorSurface drawable to prevent content overlap. + background = + LayerDrawable( + arrayOf( + ColorDrawable(context.getAttrColorSafe(R.attr.colorSurface)), + sheetBackgroundDrawable)) - disableDropShadowCompat() + // Try to disable drop shadows if possible. + disableDropShadowCompat() + + setOnApplyWindowInsetsListener(::applyWindowInsets) + } + + setup = true } - return success + return layout } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt index fa64cdb82..a2a118584 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/BottomSheetContentBehavior.kt @@ -28,12 +28,54 @@ import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat 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(context: Context, attributeSet: AttributeSet?) : CoordinatorLayout.Behavior(context, attributeSet) { - private var lastInsets: WindowInsets? = null private var dep: View? = null - private var setup: Boolean = false + private var lastInsets: WindowInsets? = 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( parent: CoordinatorLayout, @@ -107,41 +149,4 @@ class BottomSheetContentBehavior(context: Context, attributeSet: Attri (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 - } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt b/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt index 058737fdd..c82fd5984 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt @@ -166,10 +166,12 @@ abstract class Item { /** A data object used solely for the "Header" UI element. */ data class Header( - override val id: Long, /** The string resource used for the header. */ @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 diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 900a5e1fd..e34943b23 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -23,7 +23,6 @@ import android.content.res.ColorStateList import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.graphics.Insets -import android.graphics.Rect import android.graphics.drawable.Drawable import android.os.Build import android.view.View @@ -241,48 +240,46 @@ val AndroidViewModel.application: Application fun SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) = 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 -// work for Auxio's use-case. Use our own methods instead. +// Note: WindowInsetsCompat and it's related methods are an over-engineered mess that does not +// 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 * that properly follows all the frustrating changes that were made between Android 8-11. */ -val WindowInsets.systemBarInsetsCompat: Rect +val WindowInsets.systemBarInsetsCompat: Insets get() = when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { - getInsets(WindowInsets.Type.systemBars()).run { Rect(left, top, right, bottom) } + getInsets(WindowInsets.Type.systemBars()) } else -> { - @Suppress("DEPRECATION") - Rect( - systemWindowInsetLeft, - systemWindowInsetTop, - systemWindowInsetRight, - systemWindowInsetBottom) + @Suppress("DEPRECATION") systemWindowInsets } } /** * 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() = + // 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 { 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 -> { - @Suppress("DEPRECATION") val gestureInsets = systemGestureInsets - Rect( - gestureInsets.left, - gestureInsets.top, - gestureInsets.right, - gestureInsets.bottom) + @Suppress("DEPRECATION") Insets.max(systemGestureInsets, systemWindowInsets) } - else -> Rect(0, 0, 0, 0) + else -> Insets.of(0, 0, 0, 0) } /** diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 1e9c460ae..47cd3dfca 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -46,12 +46,12 @@ + android:layout_height="@dimen/size_bottom_sheet_bar"> 24dp 48dp + 64dp 72dp 24dp @@ -38,7 +39,6 @@ 2sp - 2dp 3dp 78dp