@@ -25,30 +25,38 @@ import android.media.audiofx.AudioEffect
2525import android.os.Bundle
2626import android.view.LayoutInflater
2727import android.view.MenuItem
28- import android.view.ViewTreeObserver
28+ import android.view.View
2929import androidx.activity.result.ActivityResultLauncher
3030import androidx.activity.result.contract.ActivityResultContracts
3131import androidx.appcompat.widget.Toolbar
3232import androidx.core.view.updatePadding
3333import androidx.dynamicanimation.animation.SpringForce
3434import androidx.fragment.app.activityViewModels
35+ import androidx.fragment.app.viewModels
3536import dagger.hilt.android.AndroidEntryPoint
3637import org.oxycblt.auxio.R
3738import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
3839import org.oxycblt.auxio.detail.DetailViewModel
3940import org.oxycblt.auxio.list.ListViewModel
4041import org.oxycblt.auxio.music.resolve
4142import org.oxycblt.auxio.music.resolveNames
43+ import org.oxycblt.auxio.playback.queue.QueueViewModel
4244import org.oxycblt.auxio.playback.state.RepeatMode
4345import org.oxycblt.auxio.playback.ui.StyledSeekBar
46+ import org.oxycblt.auxio.playback.ui.swiper.CarouselTransformer
47+ import org.oxycblt.auxio.playback.ui.swiper.CoverPagerAdapter
48+ import org.oxycblt.auxio.playback.ui.swiper.UserAwarePagerCallback
4449import org.oxycblt.auxio.playback.ui.stepper.Direction
4550import org.oxycblt.auxio.playback.ui.stepper.PlayerFastSeekOverlay
4651import org.oxycblt.auxio.ui.ViewBindingFragment
4752import org.oxycblt.auxio.util.collectImmediately
53+ import org.oxycblt.auxio.util.dampen
54+ import org.oxycblt.auxio.util.recycler
4855import org.oxycblt.auxio.util.showToast
4956import org.oxycblt.auxio.util.systemBarInsetsCompat
5057import org.oxycblt.musikr.MusicParent
5158import org.oxycblt.musikr.Song
59+ import kotlin.math.abs
5260import timber.log.Timber as L
5361
5462/* *
@@ -64,13 +72,15 @@ class PlaybackPanelFragment :
6472 ViewBindingFragment <FragmentPlaybackPanelBinding >(),
6573 Toolbar .OnMenuItemClickListener ,
6674 StyledSeekBar .Listener ,
67- ViewTreeObserver .OnGlobalLayoutListener ,
6875 PlayerFastSeekOverlay .PerformListener {
76+ private val coverPagerAdapter = CoverPagerAdapter (this )
6977 private val playbackModel: PlaybackViewModel by activityViewModels()
7078 private val detailModel: DetailViewModel by activityViewModels()
7179 private val listModel: ListViewModel by activityViewModels()
80+ private val queueModel: QueueViewModel by viewModels()
7281 private var equalizerLauncher: ActivityResultLauncher <Intent >? = null
73- private var lastCoverWidth = 0
82+ private var userAwarePagerCallback: UserAwarePagerCallback ? = null
83+ private var currentPagerPosition = 0
7484
7585 override fun onCreateBinding (inflater : LayoutInflater ) =
7686 FragmentPlaybackPanelBinding .inflate(inflater)
@@ -100,11 +110,27 @@ class PlaybackPanelFragment :
100110 setOnMenuItemClickListener(this @PlaybackPanelFragment)
101111 }
102112
103- // Disable swipe gestures on cover for now
104- binding.playbackCover.onSwipeListener = null
113+ binding.playbackPager?.apply {
114+ adapter = coverPagerAdapter
115+ userAwarePagerCallback = UserAwarePagerCallback (this ) {
116+ // Posting the queue goto command prevents the seekbar pos from desyncing
117+ // from the song's duration, which creates a visual flicker in the seekbar.
118+ post { queueModel.goto(it) }
119+ }.also { it.attach() }
120+ setPageTransformer(CarouselTransformer ())
121+ recycler().apply {
122+ // Make it possible to collapse the bottom sheet from the ViewPager's touch area.
123+ isNestedScrollingEnabled = false
124+ // Visual effect consistency
125+ // TODO: Custom overscroll?
126+ overScrollMode = View .OVER_SCROLL_NEVER
127+ }
128+ // Make it easier to collapse the bottom sheet
129+ dampen()
130+ offscreenPageLimit = 1
131+ }
105132
106133 // Set up fast seek overlay
107- binding.playbackFastSeekOverlay?.performListener = this
108134 binding.playbackSong.apply {
109135 isSelected = true
110136 setOnClickListener { navigateToCurrentSong() }
@@ -149,23 +175,25 @@ class PlaybackPanelFragment :
149175 collectImmediately(playbackModel.repeatMode, ::updateRepeat)
150176 collectImmediately(playbackModel.isPlaying, ::updatePlaying)
151177 collectImmediately(playbackModel.isShuffled, ::updateShuffled)
178+ collectImmediately(playbackModel.pagerQueue, ::updatePager)
152179 }
153180
154- override fun onStart () {
155- super .onStart()
156- playbackModel.song.value?.let { requireBinding().playbackCover.bind(it) }
157- requireBinding().root.viewTreeObserver.addOnGlobalLayoutListener(this )
158- }
181+ // FIXME: Old code!! Maybe not necessary anymore?
182+ // override fun onStart() {
183+ // super.onStart()
184+ // playbackModel.song.value?.let { requireBinding().playbackCover.bind(it) }
185+ // requireBinding().root.viewTreeObserver.addOnGlobalLayoutListener(this)
186+ // }
159187
160- override fun onStop () {
161- super .onStop()
162- requireBinding().root.viewTreeObserver.removeOnGlobalLayoutListener(this )
163- }
188+ // override fun onStop() {
189+ // super.onStop()
190+ // requireBinding().root.viewTreeObserver.removeOnGlobalLayoutListener(this)
191+ // }
164192
165- override fun onGlobalLayout () {
166- if (binding == null || lastCoverWidth < 0 ) {
167- return
168- }
193+ // override fun onGlobalLayout() {
194+ // if (binding == null || lastCoverWidth < 0) {
195+ // return
196+ // }
169197 // Hacky workaround for cover radius not being preserved in between sizing changes
170198 // (i.e split screen or landscape mode)
171199 // For some reason ConstraintLayout does several passes on 1:1 elements that causes their
@@ -175,15 +203,15 @@ class PlaybackPanelFragment :
175203 // covers) we can force it to reload.
176204 // If this breaks, it's fine since we also started a load as we normally did w/state
177205 // updates, so the cover will not break.
178- val binding = requireBinding()
179- val coverWidth = binding.playbackCover.width
180- if (lastCoverWidth != coverWidth) {
181- lastCoverWidth = coverWidth
182- } else {
183- playbackModel.song.value?.let { binding.playbackCover.bind(it) }
184- lastCoverWidth = - 1
185- }
186- }
206+ // val binding = requireBinding()
207+ // val coverWidth = binding.playbackCover.width
208+ // if (lastCoverWidth != coverWidth) {
209+ // lastCoverWidth = coverWidth
210+ // } else {
211+ // playbackModel.song.value?.let { binding.playbackCover.bind(it) }
212+ // lastCoverWidth = -1
213+ // }
214+ // }
187215
188216 override fun onDestroyBinding (binding : FragmentPlaybackPanelBinding ) {
189217 equalizerLauncher = null
@@ -192,6 +220,8 @@ class PlaybackPanelFragment :
192220 binding.playbackArtist.isSelected = false
193221 binding.playbackAlbum?.isSelected = false
194222 binding.playbackToolbar.setOnMenuItemClickListener(null )
223+ userAwarePagerCallback?.release()
224+ binding.playbackPager?.adapter = null
195225 }
196226
197227 override fun onMenuItemClick (item : MenuItem ): Boolean {
@@ -231,7 +261,6 @@ class PlaybackPanelFragment :
231261 val binding = requireBinding()
232262 val context = requireContext()
233263 L .d(" Updating song display: $song " )
234- binding.playbackCover.bind(song)
235264 binding.playbackSong.text = song.name.resolve(context)
236265 binding.playbackArtist.text = song.artists.resolveNames(context)
237266 binding.playbackAlbum?.text = song.album.name.resolve(context)
@@ -264,6 +293,40 @@ class PlaybackPanelFragment :
264293 requireBinding().playbackShuffle.isChecked = isShuffled
265294 }
266295
296+ private fun updatePager (queue : PagerQueue ) {
297+ val binding = requireBinding()
298+
299+ val command = playbackModel.pagerCommand.consume()
300+ if (command == null ) {
301+ // This probably shouldn't happen in practice, as QueueViewModel directly
302+ // attaches to PlaybackStateManager and will basically always initialize
303+ // with a command as a result.
304+ //
305+ // If it does happen we should just make sure the UI state is aligned. Don't
306+ // want broken UI.
307+ coverPagerAdapter.update(queue.queue, null )
308+ binding.playbackPager.setCurrentItem(queue.index, false )
309+ return
310+ }
311+
312+ if (command.update != null ) {
313+ // queue needs to be updated.
314+ coverPagerAdapter.update(queue.queue, command.update)
315+ }
316+
317+ if (command.scroll != null ) {
318+ // we need to scroll, however the smooth scroll only really looks best
319+ // when we are only doing next/prev due to various factors. better to
320+ // just not animate on outright gotos or queue updates
321+ val delta = binding.playbackPager.currentItem - command.scroll
322+ if (delta == 0 ) {
323+ // user scroll, carry on
324+ return
325+ }
326+ binding.playbackPager.setCurrentItem(command.scroll, command.update == null && abs(delta) == 1 )
327+ }
328+ }
329+
267330 private fun navigateToCurrentSong () {
268331 playbackModel.song.value?.let (detailModel::showAlbum)
269332 }
@@ -282,4 +345,8 @@ class PlaybackPanelFragment :
282345 Direction .BACKWARDS -> playbackModel.stepBackwards()
283346 }
284347 }
348+
349+ private companion object {
350+
351+ }
285352}
0 commit comments