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:
parent
257516643f
commit
35cfea78df
21 changed files with 177 additions and 184 deletions
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<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
|
||||
// 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<Item>(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<Item>(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<ArtistAlbumGrouping> {
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = "<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<DiscHeader, Unit>(binding.root) {
|
||||
|
||||
override fun bind(item: DiscHeader, listener: Unit) {
|
||||
binding.discNo.textSafe = binding.context.getString(R.string.fmt_disc_no, item.disc)
|
||||
}
|
||||
|
|
|
@ -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<L : DetailAdapter.Listener>(
|
|||
}
|
||||
}
|
||||
|
||||
data class SortHeader(override val id: Long, @StringRes val string: Int) : Item()
|
||||
|
||||
class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
|
||||
BindingViewHolder<SortHeader, DetailAdapter.Listener>(binding.root) {
|
||||
override fun bind(item: SortHeader, listener: DetailAdapter.Listener) {
|
||||
|
|
|
@ -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<V : View>(context: Context, attributeSet: AttributeSet?) :
|
||||
AuxioSheetBehavior<V>(context, attributeSet) {
|
||||
private var lastInsets: WindowInsets? = null
|
||||
|
||||
init {
|
||||
isHideable = true
|
||||
}
|
||||
|
@ -50,25 +43,10 @@ class PlaybackSheetBehavior<V : View>(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<V : View>(context: Context, attributeSet: AttributeS
|
|||
}
|
||||
}
|
||||
|
||||
/** Unhide this sheet in a safe manner. */
|
||||
fun unhideSafe() {
|
||||
if (state == STATE_HIDDEN) {
|
||||
state = STATE_COLLAPSED
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<FragmentQueueBinding>(), QueueItemList
|
|||
if (start != RecyclerView.NO_POSITION &&
|
||||
end != RecyclerView.NO_POSITION &&
|
||||
scrollTo !in start..end) {
|
||||
logD("Scrolling to new position")
|
||||
binding.queueRecycler.scrollToPosition(scrollTo)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<V : View>(context: Context, attributeSet: AttributeSet?) :
|
||||
AuxioSheetBehavior<V>(context, attributeSet) {
|
||||
private var barHeight = 0
|
||||
|
@ -45,25 +48,18 @@ class QueueSheetBehavior<V : View>(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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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]. */
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<V : View>(context: Context, attributeSet: AttributeSet?) :
|
||||
NeoBottomSheetBehavior<V>(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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<V : View>(context: Context, attributeSet: AttributeSet?) :
|
||||
CoordinatorLayout.Behavior<V>(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<V : View>(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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <R> 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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -46,12 +46,12 @@
|
|||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/handle_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp">
|
||||
android:layout_height="@dimen/size_bottom_sheet_bar">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/handle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_height="@dimen/size_btn"
|
||||
android:scaleType="center"
|
||||
android:paddingBottom="@dimen/spacing_small"
|
||||
android:src="@drawable/ic_down_24"
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<dimen name="size_corners_mid_large">24dp</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_icon_small">24dp</dimen>
|
||||
|
@ -38,7 +39,6 @@
|
|||
<dimen name="text_size_track_number_step">2sp</dimen>
|
||||
|
||||
<!-- Misc -->
|
||||
<dimen name="elevation_small">2dp</dimen>
|
||||
<dimen name="elevation_normal">3dp</dimen>
|
||||
|
||||
<dimen name="fast_scroll_popup_min_width">78dp</dimen>
|
||||
|
|
Loading…
Reference in a new issue