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:
parent
35eb07410d
commit
949d71dbd1
6 changed files with 59 additions and 120 deletions
|
@ -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.
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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`
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue