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