playback: actually fix controls not working

Turns out playback controls wouldn't actually work because the view
would detach but not the actual fragment, resulting in onCreateView
never being called and the entire system falling apart. This fixes
it by just giving PlaybackLayout the viewmodel instance it needs.
I'll need to release a hotfix for this issue since this is really
easy the trigger and really hard to fix unless you know why it
occurs. Android lifecycles suck so much.
This commit is contained in:
OxygenCobalt 2021-11-26 10:17:46 -07:00
parent 35eb07410d
commit 949d71dbd1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 59 additions and 120 deletions

View file

@ -34,7 +34,6 @@ import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackLayout
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -43,7 +42,7 @@ import org.oxycblt.auxio.util.logD
* the more high-level navigation features. * the more high-level navigation features.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class MainFragment : Fragment(), PlaybackLayout.ActionCallback { class MainFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels()
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
@ -78,23 +77,7 @@ class MainFragment : Fragment(), PlaybackLayout.ActionCallback {
// We have to control the bar view from here since using a Fragment in PlaybackLayout // We have to control the bar view from here since using a Fragment in PlaybackLayout
// would result in annoying UI issues. // would result in annoying UI issues.
binding.playbackLayout.setActionCallback(this) binding.playbackLayout.setup(playbackModel, detailModel, viewLifecycleOwner)
binding.playbackLayout.setSong(playbackModel.song.value)
binding.playbackLayout.setPlaying(playbackModel.isPlaying.value!!)
binding.playbackLayout.setPosition(playbackModel.position.value!!)
playbackModel.song.observe(viewLifecycleOwner) { song ->
binding.playbackLayout.setSong(song)
}
playbackModel.isPlaying.observe(viewLifecycleOwner) { isPlaying ->
binding.playbackLayout.setPlaying(isPlaying)
}
playbackModel.position.observe(viewLifecycleOwner) { pos ->
binding.playbackLayout.setPosition(pos)
}
// Initialize music loading. Do it here so that it shows on every fragment that this // Initialize music loading. Do it here so that it shows on every fragment that this
// one contains. // one contains.
@ -156,29 +139,6 @@ class MainFragment : Fragment(), PlaybackLayout.ActionCallback {
callback?.isEnabled = false callback?.isEnabled = false
} }
override fun onDestroyView() {
super.onDestroyView()
// This callback has access to the binding, so make sure we clear it when we're done.
callback = null
}
override fun onNavToItem() {
detailModel.navToItem(playbackModel.song.value ?: return)
}
override fun onPrev() {
playbackModel.skipPrev()
}
override fun onPlayPauseClick() {
playbackModel.invertPlayingStatus()
}
override fun onNext() {
playbackModel.skipNext()
}
/** /**
* A back press callback that handles how to respond to backwards navigation in the detail * A back press callback that handles how to respond to backwards navigation in the detail
* fragments and the playback panel. * fragments and the playback panel.

View file

@ -23,9 +23,11 @@ import android.util.AttributeSet
import android.view.WindowInsets import android.view.WindowInsets
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ViewPlaybackBarBinding import org.oxycblt.auxio.databinding.ViewPlaybackBarBinding
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.resolveAttr import org.oxycblt.auxio.util.resolveAttr
@ -41,16 +43,10 @@ class PlaybackBarView @JvmOverloads constructor(
defStyleAttr: Int = -1 defStyleAttr: Int = -1
) : ConstraintLayout(context, attrs, defStyleAttr) { ) : ConstraintLayout(context, attrs, defStyleAttr) {
private val binding = ViewPlaybackBarBinding.inflate(context.inflater, this, true) private val binding = ViewPlaybackBarBinding.inflate(context.inflater, this, true)
private var mCallback: PlaybackLayout.ActionCallback? = null
init { init {
id = R.id.playback_bar id = R.id.playback_bar
setOnLongClickListener {
mCallback?.onNavToItem()
true
}
// Deliberately override the progress bar color [in a Lollipop-friendly way] so that // Deliberately override the progress bar color [in a Lollipop-friendly way] so that
// we use colorSecondary instead of colorSurfaceVariant. This is for two reasons: // we use colorSecondary instead of colorSurfaceVariant. This is for two reasons:
// 1. colorSurfaceVariant is used with the assumption that the view that is using it // 1. colorSurfaceVariant is used with the assumption that the view that is using it
@ -66,28 +62,45 @@ class PlaybackBarView @JvmOverloads constructor(
return insets return insets
} }
fun setup(
playbackModel: PlaybackViewModel,
detailModel: DetailViewModel,
viewLifecycleOwner: LifecycleOwner
) {
setOnLongClickListener {
playbackModel.song.value?.let { song ->
detailModel.navToItem(song)
}
true
}
binding.playbackSkipPrev?.setOnClickListener {
playbackModel.skipPrev()
}
binding.playbackPlayPause.setOnClickListener {
playbackModel.invertPlayingStatus()
}
binding.playbackSkipNext?.setOnClickListener {
playbackModel.skipNext()
}
binding.playbackPlayPause.isActivated = playbackModel.isPlaying.value!!
playbackModel.isPlaying.observe(viewLifecycleOwner) { isPlaying ->
binding.playbackPlayPause.isActivated = isPlaying
}
binding.playbackProgressBar.progress = playbackModel.position.value!!.toInt()
playbackModel.position.observe(viewLifecycleOwner) { position ->
binding.playbackProgressBar.progress = position.toInt()
}
}
fun setSong(song: Song) { fun setSong(song: Song) {
binding.song = song binding.song = song
binding.executePendingBindings() binding.executePendingBindings()
} }
fun setPlaying(isPlaying: Boolean) {
binding.playbackPlayPause.isActivated = isPlaying
}
fun setPosition(position: Long) {
binding.playbackProgressBar.progress = position.toInt()
}
fun setCallback(callback: PlaybackLayout.ActionCallback) {
mCallback = callback
binding.callback = callback
binding.executePendingBindings()
}
fun clearCallback() {
mCallback = null
binding.callback = null
binding.executePendingBindings()
}
} }

View file

@ -20,11 +20,12 @@ import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.customview.widget.ViewDragHelper import androidx.customview.widget.ViewDragHelper
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.resolveAttr import org.oxycblt.auxio.util.resolveAttr
import org.oxycblt.auxio.util.resolveDrawable import org.oxycblt.auxio.util.resolveDrawable
import org.oxycblt.auxio.util.systemBarsCompat import org.oxycblt.auxio.util.systemBarsCompat
@ -56,13 +57,6 @@ class PlaybackLayout @JvmOverloads constructor(
EXPANDED, COLLAPSED, HIDDEN, DRAGGING EXPANDED, COLLAPSED, HIDDEN, DRAGGING
} }
interface ActionCallback {
fun onNavToItem()
fun onPrev()
fun onPlayPauseClick()
fun onNext()
}
private lateinit var contentView: View private lateinit var contentView: View
private val playbackContainerView: FrameLayout private val playbackContainerView: FrameLayout
private val playbackBarView: PlaybackBarView private val playbackBarView: PlaybackBarView
@ -182,7 +176,21 @@ class PlaybackLayout @JvmOverloads constructor(
* Update the song that this layout is showing. This will be reflected in the compact view * Update the song that this layout is showing. This will be reflected in the compact view
* at the bottom of the screen. * at the bottom of the screen.
*/ */
fun setSong(song: Song?) { fun setup(
playbackModel: PlaybackViewModel,
detailModel: DetailViewModel,
viewLifecycleOwner: LifecycleOwner
) {
setSong(playbackModel.song.value)
playbackModel.song.observe(viewLifecycleOwner) { song ->
setSong(song)
}
playbackBarView.setup(playbackModel, detailModel, viewLifecycleOwner)
}
private fun setSong(song: Song?) {
if (song != null) { if (song != null) {
playbackBarView.setSong(song) playbackBarView.setSong(song)
@ -195,35 +203,11 @@ class PlaybackLayout @JvmOverloads constructor(
} }
} }
/**
* Update the playing status on this layout. This will be reflected in the compact view
* at the bottom of the screen.
*/
fun setPlaying(isPlaying: Boolean) {
playbackBarView.setPlaying(isPlaying)
}
/**
* Update the playback position on this layout. This will be reflected in the compact view
* at the bottom of the screen.
*/
fun setPosition(position: Long) {
playbackBarView.setPosition(position)
}
/**
* Add a callback for actions from the compact playback view in this layout.
*/
fun setActionCallback(callback: ActionCallback) {
playbackBarView.setCallback(callback)
}
/** /**
* Collapse the panel if it is currently expanded. * Collapse the panel if it is currently expanded.
* @return If the panel was collapsed or not. * @return If the panel was collapsed or not.
*/ */
fun collapse(): Boolean { fun collapse(): Boolean {
logD(panelState)
if (panelState == PanelState.EXPANDED) { if (panelState == PanelState.EXPANDED) {
applyState(PanelState.COLLAPSED) applyState(PanelState.COLLAPSED)
return true return true
@ -416,11 +400,6 @@ class PlaybackLayout @JvmOverloads constructor(
} }
} }
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
playbackBarView.clearCallback()
}
override fun onSaveInstanceState(): Parcelable = Bundle().apply { override fun onSaveInstanceState(): Parcelable = Bundle().apply {
putParcelable("superState", super.onSaveInstanceState()) putParcelable("superState", super.onSaveInstanceState())
putSerializable( putSerializable(

View file

@ -10,10 +10,6 @@
name="song" name="song"
type="org.oxycblt.auxio.music.Song" /> type="org.oxycblt.auxio.music.Song" />
<variable
name="callback"
type="org.oxycblt.auxio.playback.PlaybackLayout.ActionCallback" />
</data> </data>
<merge <merge
@ -69,7 +65,6 @@
android:layout_margin="@dimen/spacing_small" android:layout_margin="@dimen/spacing_small"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_skip_prev" android:src="@drawable/ic_skip_prev"
android:onClick="@{() -> callback.onPrev()}"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause" app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
app:layout_constraintEnd_toStartOf="@+id/playback_play_pause" app:layout_constraintEnd_toStartOf="@+id/playback_play_pause"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
@ -84,7 +79,6 @@
android:layout_margin="@dimen/spacing_small" android:layout_margin="@dimen/spacing_small"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state" android:src="@drawable/sel_playing_state"
android:onClick="@{() -> callback.onPlayPauseClick()}"
app:layout_constraintBottom_toTopOf="@+id/playback_progress_bar" app:layout_constraintBottom_toTopOf="@+id/playback_progress_bar"
app:layout_constraintEnd_toStartOf="@+id/playback_skip_next" app:layout_constraintEnd_toStartOf="@+id/playback_skip_next"
app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_bias="0.5"
@ -99,7 +93,6 @@
android:layout_margin="@dimen/spacing_small" android:layout_margin="@dimen/spacing_small"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_skip_next" android:src="@drawable/ic_skip_next"
android:onClick="@{() -> callback.onNext()}"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause" app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/playback_play_pause" app:layout_constraintStart_toEndOf="@+id/playback_play_pause"

View file

@ -10,10 +10,6 @@
name="song" name="song"
type="org.oxycblt.auxio.music.Song" /> type="org.oxycblt.auxio.music.Song" />
<variable
name="callback"
type="org.oxycblt.auxio.playback.PlaybackLayout.ActionCallback" />
</data> </data>
<merge <merge
@ -70,7 +66,6 @@
android:layout_margin="@dimen/spacing_small" android:layout_margin="@dimen/spacing_small"
android:contentDescription="@string/desc_play_pause" android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state" android:src="@drawable/sel_playing_state"
android:onClick="@{() -> callback.onPlayPauseClick()}"
app:layout_constraintBottom_toTopOf="@+id/playback_progress_bar" app:layout_constraintBottom_toTopOf="@+id/playback_progress_bar"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -275,8 +275,7 @@ This module not only contains the playback system described above, but also mult
- `system` contains the system-facing playback system - `system` contains the system-facing playback system
The most important part of this module is `PlaybackLayout`, which is a custom `ViewGroup` that implements the playback bar and it's ability to The most important part of this module is `PlaybackLayout`, which is a custom `ViewGroup` that implements the playback bar and it's ability to
slide up into the full playback view. `MainFragment` controls this `ViewGroup`, more specifically the bar view, as it can't be an independent slide up into the full playback view. `MainFragment` controls this `ViewGroup`.
fragment due to a couple of annoying reasons.
#### `.search` #### `.search`