home: extract fab system to home
This commit is contained in:
parent
80dac7d9e9
commit
f3b73a5196
6 changed files with 363 additions and 283 deletions
|
@ -20,6 +20,8 @@ package org.oxycblt.auxio
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.WindowInsets
|
||||
import androidx.activity.BackEventCompat
|
||||
|
@ -32,10 +34,14 @@ import androidx.navigation.findNavController
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.R as MR
|
||||
import com.google.android.material.bottomsheet.BackportBottomSheetBehavior
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||
import com.leinardi.android.speeddial.SpeedDialView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.lang.reflect.Method
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||
|
@ -44,7 +50,10 @@ import org.oxycblt.auxio.detail.Show
|
|||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.home.Outer
|
||||
import org.oxycblt.auxio.list.ListViewModel
|
||||
import org.oxycblt.auxio.music.IndexingState
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicType
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.OpenPanel
|
||||
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
|
||||
|
@ -58,6 +67,8 @@ import org.oxycblt.auxio.util.context
|
|||
import org.oxycblt.auxio.util.coordinatorLayoutBehavior
|
||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||
import org.oxycblt.auxio.util.getDimen
|
||||
import org.oxycblt.auxio.util.isUnder
|
||||
import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.navigateSafe
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
@ -69,7 +80,10 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
*/
|
||||
@AndroidEntryPoint
|
||||
class MainFragment :
|
||||
ViewBindingFragment<FragmentMainBinding>(), ViewTreeObserver.OnPreDrawListener {
|
||||
ViewBindingFragment<FragmentMainBinding>(),
|
||||
ViewTreeObserver.OnPreDrawListener,
|
||||
SpeedDialView.OnActionSelectedListener {
|
||||
private val musicModel: MusicViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
private val listModel: ListViewModel by activityViewModels()
|
||||
|
@ -78,11 +92,12 @@ class MainFragment :
|
|||
private var detailBackCallback: DetailBackPressedCallback? = null
|
||||
private var selectionBackCallback: SelectionBackPressedCallback? = null
|
||||
private var speedDialBackCallback: SpeedDialBackPressedCallback? = null
|
||||
private var selectionNavigationListener: DialogAwareNavigationListener? = null
|
||||
private var navigationListener: DialogAwareNavigationListener? = null
|
||||
private var lastInsets: WindowInsets? = null
|
||||
private var elevationNormal = 0f
|
||||
private var normalCornerSize = 0f
|
||||
private var maxScaleXDistance = 0f
|
||||
private var sheetRising: Boolean? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -113,10 +128,9 @@ class MainFragment :
|
|||
DetailBackPressedCallback(detailModel).also { detailBackCallback = it }
|
||||
val selectionBackCallback =
|
||||
SelectionBackPressedCallback(listModel).also { selectionBackCallback = it }
|
||||
val speedDialBackCallback =
|
||||
SpeedDialBackPressedCallback(homeModel).also { speedDialBackCallback = it }
|
||||
speedDialBackCallback = SpeedDialBackPressedCallback(homeModel)
|
||||
|
||||
selectionNavigationListener = DialogAwareNavigationListener(listModel::dropSelection)
|
||||
navigationListener = DialogAwareNavigationListener(::onExploreNavigate)
|
||||
|
||||
// --- UI SETUP ---
|
||||
val context = requireActivity()
|
||||
|
@ -162,8 +176,22 @@ class MainFragment :
|
|||
|
||||
binding.playbackSheet.elevation = 0f
|
||||
|
||||
binding.mainScrim.setOnClickListener { homeModel.setSpeedDialOpen(false) }
|
||||
binding.sheetScrim.setOnClickListener { homeModel.setSpeedDialOpen(false) }
|
||||
binding.mainScrim.setOnClickListener { binding.homeNewPlaylistFab.close() }
|
||||
binding.sheetScrim.setOnClickListener { binding.homeNewPlaylistFab.close() }
|
||||
binding.homeShuffleFab.setOnClickListener { playbackModel.shuffleAll() }
|
||||
binding.homeNewPlaylistFab.apply {
|
||||
inflate(R.menu.new_playlist_actions)
|
||||
setOnActionSelectedListener(this@MainFragment)
|
||||
setChangeListener(::updateSpeedDial)
|
||||
}
|
||||
|
||||
forceHideAllFabs()
|
||||
updateSpeedDial(false)
|
||||
updateFabVisibility(
|
||||
binding,
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value,
|
||||
homeModel.currentTabType.value)
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
// This has to be done here instead of the playback panel to make sure that it's prioritized
|
||||
|
@ -173,7 +201,9 @@ class MainFragment :
|
|||
collect(detailModel.toShow.flow, ::handleShow)
|
||||
collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
|
||||
collectImmediately(homeModel.showOuter.flow, ::handleShowOuter)
|
||||
collectImmediately(homeModel.speedDialOpen, ::handleSpeedDialState)
|
||||
collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
|
||||
collectImmediately(homeModel.songList, homeModel.isFastScrolling, ::updateFab)
|
||||
collectImmediately(musicModel.indexingState, ::updateIndexerState)
|
||||
collectImmediately(listModel.selected, selectionBackCallback::invalidateEnabled)
|
||||
collectImmediately(playbackModel.song, ::updateSong)
|
||||
collectImmediately(playbackModel.openPanel.flow, ::handlePanel)
|
||||
|
@ -184,7 +214,7 @@ class MainFragment :
|
|||
val binding = requireBinding()
|
||||
// Once we add the destination change callback, we will receive another initialization call,
|
||||
// so handle that by resetting the flag.
|
||||
requireNotNull(selectionNavigationListener) { "NavigationListener was not available" }
|
||||
requireNotNull(navigationListener) { "NavigationListener was not available" }
|
||||
.attach(binding.exploreNavHost.findNavController())
|
||||
// Listener could still reasonably fire even if we clear the binding, attach/detach
|
||||
// our pre-draw listener our listener in onStart/onStop respectively.
|
||||
|
@ -202,12 +232,23 @@ class MainFragment :
|
|||
addCallback(viewLifecycleOwner, requireNotNull(detailBackCallback))
|
||||
addCallback(viewLifecycleOwner, requireNotNull(sheetBackCallback))
|
||||
}
|
||||
|
||||
// Stock bottom sheet overlay won't work with our nested UI setup, have to replicate
|
||||
// it ourselves.
|
||||
requireBinding().root.rootView.apply {
|
||||
findViewById<View>(R.id.main_scrim).setOnTouchListener { _, event ->
|
||||
handleSpeedDialBoundaryTouch(event)
|
||||
}
|
||||
findViewById<View>(R.id.sheet_scrim).setOnTouchListener { _, event ->
|
||||
handleSpeedDialBoundaryTouch(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
val binding = requireBinding()
|
||||
requireNotNull(selectionNavigationListener) { "NavigationListener was not available" }
|
||||
requireNotNull(navigationListener) { "NavigationListener was not available" }
|
||||
.release(binding.exploreNavHost.findNavController())
|
||||
binding.playbackSheet.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
}
|
||||
|
@ -218,7 +259,9 @@ class MainFragment :
|
|||
sheetBackCallback = null
|
||||
detailBackCallback = null
|
||||
selectionBackCallback = null
|
||||
selectionNavigationListener = null
|
||||
navigationListener = null
|
||||
binding.homeNewPlaylistFab.setChangeListener(null)
|
||||
binding.homeNewPlaylistFab.setOnActionSelectedListener(null)
|
||||
}
|
||||
|
||||
override fun onPreDraw(): Boolean {
|
||||
|
@ -236,13 +279,18 @@ class MainFragment :
|
|||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f)
|
||||
if (playbackRatio > 0f && homeModel.speedDialOpen.value) {
|
||||
// Stupid hack to prevent you from sliding the sheet up without closing the speed
|
||||
// dial. Filtering out ACTION_MOVE events will cause back gestures to close the
|
||||
// speed dial, which is super finicky behavior.
|
||||
homeModel.setSpeedDialOpen(false)
|
||||
// Stupid hack to prevent you from sliding the sheet up without closing the speed
|
||||
// dial. Filtering out ACTION_MOVE events will cause back gestures to close the
|
||||
// speed dial, which is super finicky behavior.
|
||||
val rising = playbackRatio > 0f
|
||||
if (rising != sheetRising) {
|
||||
sheetRising = rising
|
||||
updateFabVisibility(
|
||||
binding,
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value,
|
||||
homeModel.currentTabType.value)
|
||||
}
|
||||
homeModel.setSheetObscuresFab(playbackRatio > 0f)
|
||||
|
||||
val playbackOutRatio = 1 - min(playbackRatio * 2, 1f)
|
||||
val playbackInRatio = max(playbackRatio - 0.5f, 0f) * 2
|
||||
|
@ -330,6 +378,193 @@ class MainFragment :
|
|||
return true
|
||||
}
|
||||
|
||||
override fun onActionSelected(actionItem: SpeedDialActionItem): Boolean {
|
||||
when (actionItem.id) {
|
||||
R.id.action_new_playlist -> {
|
||||
logD("Creating playlist")
|
||||
musicModel.createPlaylist()
|
||||
}
|
||||
R.id.action_import_playlist -> {
|
||||
logD("Importing playlist")
|
||||
musicModel.importPlaylist()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
// Returning false to close the speed dial results in no animation, manually close instead.
|
||||
// Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
|
||||
requireBinding().homeNewPlaylistFab.close()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onExploreNavigate() {
|
||||
listModel.dropSelection()
|
||||
updateFabVisibility(
|
||||
requireBinding(),
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value,
|
||||
homeModel.currentTabType.value)
|
||||
}
|
||||
|
||||
private fun updateCurrentTab(tabType: MusicType) {
|
||||
val binding = requireBinding()
|
||||
updateFabVisibility(
|
||||
binding, homeModel.songList.value, homeModel.isFastScrolling.value, tabType)
|
||||
}
|
||||
|
||||
private fun updateIndexerState(state: IndexingState?) {
|
||||
// TODO: Make music loading experience a bit more pleasant
|
||||
// 1. Loading placeholder for item lists
|
||||
// 2. Rework the "No Music" case to not be an error and instead result in a placeholder
|
||||
if (state is IndexingState.Completed && state.error == null) {
|
||||
logD("Received ok response")
|
||||
val binding = requireBinding()
|
||||
updateFabVisibility(
|
||||
binding,
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value,
|
||||
homeModel.currentTabType.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFab(songs: List<Song>, isFastScrolling: Boolean) {
|
||||
val binding = requireBinding()
|
||||
updateFabVisibility(binding, songs, isFastScrolling, homeModel.currentTabType.value)
|
||||
}
|
||||
|
||||
private fun updateFabVisibility(
|
||||
binding: FragmentMainBinding,
|
||||
songs: List<Song>,
|
||||
isFastScrolling: Boolean,
|
||||
tabType: MusicType
|
||||
) {
|
||||
// If there are no songs, it's likely that the library has not been loaded, so
|
||||
// displaying the shuffle FAB makes no sense. We also don't want the fast scroll
|
||||
// popup to overlap with the FAB, so we hide the FAB when fast scrolling too.
|
||||
if (shouldHideAllFabs(binding, songs, isFastScrolling)) {
|
||||
logD("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling]")
|
||||
forceHideAllFabs()
|
||||
} else {
|
||||
if (tabType != MusicType.PLAYLISTS) {
|
||||
if (binding.homeShuffleFab.isOrWillBeShown) {
|
||||
return
|
||||
}
|
||||
|
||||
if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) {
|
||||
logD("Animating transition")
|
||||
binding.homeNewPlaylistFab.hide(
|
||||
object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton) {
|
||||
super.onHidden(fab)
|
||||
if (shouldHideAllFabs(
|
||||
binding,
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value)) {
|
||||
return
|
||||
}
|
||||
binding.homeShuffleFab.show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
logD("Showing immediately")
|
||||
binding.homeShuffleFab.show()
|
||||
}
|
||||
} else {
|
||||
logD("Showing playlist button")
|
||||
if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) {
|
||||
return
|
||||
}
|
||||
|
||||
if (binding.homeShuffleFab.isOrWillBeShown) {
|
||||
logD("Animating transition")
|
||||
binding.homeShuffleFab.hide(
|
||||
object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton) {
|
||||
super.onHidden(fab)
|
||||
if (shouldHideAllFabs(
|
||||
binding,
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value)) {
|
||||
return
|
||||
}
|
||||
binding.homeNewPlaylistFab.show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
logD("Showing immediately")
|
||||
binding.homeNewPlaylistFab.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldHideAllFabs(
|
||||
binding: FragmentMainBinding,
|
||||
songs: List<Song>,
|
||||
isFastScrolling: Boolean
|
||||
) =
|
||||
binding.exploreNavHost.findNavController().currentDestination?.id != R.id.home_fragment ||
|
||||
sheetRising == true ||
|
||||
songs.isEmpty() ||
|
||||
isFastScrolling
|
||||
|
||||
private fun forceHideAllFabs() {
|
||||
val binding = requireBinding()
|
||||
if (binding.homeShuffleFab.isOrWillBeShown) {
|
||||
FAB_HIDE_FROM_USER_FIELD.invoke(binding.homeShuffleFab, null, false)
|
||||
}
|
||||
if (binding.homeNewPlaylistFab.isOpen) {
|
||||
binding.homeNewPlaylistFab.close()
|
||||
}
|
||||
if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) {
|
||||
FAB_HIDE_FROM_USER_FIELD.invoke(binding.homeNewPlaylistFab.mainFab, null, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSpeedDial(open: Boolean) {
|
||||
requireNotNull(speedDialBackCallback) { "SpeedDialBackPressedCallback was not available" }
|
||||
.invalidateEnabled(open)
|
||||
val binding = requireBinding()
|
||||
logD(open)
|
||||
binding.mainScrim.isVisible = open
|
||||
binding.sheetScrim.isVisible = open
|
||||
if (open) {
|
||||
binding.homeNewPlaylistFab.open(true)
|
||||
} else {
|
||||
binding.homeNewPlaylistFab.close(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSpeedDialBoundaryTouch(event: MotionEvent): Boolean {
|
||||
val binding = binding ?: return false
|
||||
|
||||
if (binding.homeNewPlaylistFab.isOpen &&
|
||||
binding.homeNewPlaylistFab.isUnder(event.x, event.y)) {
|
||||
// Convert absolute coordinates to relative coordinates
|
||||
val offsetX = event.x - binding.homeNewPlaylistFab.x
|
||||
val offsetY = event.y - binding.homeNewPlaylistFab.y
|
||||
|
||||
// Create a new MotionEvent with relative coordinates
|
||||
val relativeEvent =
|
||||
MotionEvent.obtain(
|
||||
event.downTime,
|
||||
event.eventTime,
|
||||
event.action,
|
||||
offsetX,
|
||||
offsetY,
|
||||
event.metaState)
|
||||
|
||||
// Dispatch the relative MotionEvent to the target child view
|
||||
val handled = binding.homeNewPlaylistFab.dispatchTouchEvent(relativeEvent)
|
||||
|
||||
// Recycle the relative MotionEvent
|
||||
relativeEvent.recycle()
|
||||
|
||||
return handled
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun handleShow(show: Show?) {
|
||||
when (show) {
|
||||
is Show.SongAlbumDetails,
|
||||
|
@ -355,13 +590,6 @@ class MainFragment :
|
|||
homeModel.showOuter.consume()
|
||||
}
|
||||
|
||||
private fun handleSpeedDialState(open: Boolean) {
|
||||
requireNotNull(speedDialBackCallback) { "SpeedDialBackPressedCallback was not available" }
|
||||
.invalidateEnabled(open)
|
||||
requireBinding().mainScrim.isVisible = open
|
||||
requireBinding().sheetScrim.isVisible = open
|
||||
}
|
||||
|
||||
private fun updateSong(song: Song?) {
|
||||
if (song != null) {
|
||||
tryShowSheets()
|
||||
|
@ -566,8 +794,9 @@ class MainFragment :
|
|||
private inner class SpeedDialBackPressedCallback(private val homeModel: HomeViewModel) :
|
||||
OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (homeModel.speedDialOpen.value) {
|
||||
homeModel.setSpeedDialOpen(false)
|
||||
val binding = requireBinding()
|
||||
if (binding.homeNewPlaylistFab.isOpen) {
|
||||
binding.homeNewPlaylistFab.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -575,4 +804,13 @@ class MainFragment :
|
|||
isEnabled = open
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val FAB_HIDE_FROM_USER_FIELD: Method by
|
||||
lazyReflectedMethod(
|
||||
FloatingActionButton::class,
|
||||
"hide",
|
||||
FloatingActionButton.OnVisibilityChangedListener::class,
|
||||
Boolean::class)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import android.annotation.SuppressLint
|
|||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
@ -41,8 +40,6 @@ import com.google.android.material.appbar.AppBarLayout
|
|||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||
import com.leinardi.android.speeddial.SpeedDialView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
|
@ -72,14 +69,12 @@ import org.oxycblt.auxio.music.PERMISSION_READ_AUDIO
|
|||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.PlaylistDecision
|
||||
import org.oxycblt.auxio.music.PlaylistMessage
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.external.M3U
|
||||
import org.oxycblt.auxio.playback.PlaybackDecision
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.getColorCompat
|
||||
import org.oxycblt.auxio.util.isUnder
|
||||
import org.oxycblt.auxio.util.lazyReflectedField
|
||||
import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -95,9 +90,7 @@ import org.oxycblt.auxio.util.showToast
|
|||
*/
|
||||
@AndroidEntryPoint
|
||||
class HomeFragment :
|
||||
SelectionFragment<FragmentHomeBinding>(),
|
||||
AppBarLayout.OnOffsetChangedListener,
|
||||
SpeedDialView.OnActionSelectedListener {
|
||||
SelectionFragment<FragmentHomeBinding>(), AppBarLayout.OnOffsetChangedListener {
|
||||
override val listModel: ListViewModel by activityViewModels()
|
||||
override val musicModel: MusicViewModel by activityViewModels()
|
||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
@ -190,27 +183,9 @@ class HomeFragment :
|
|||
// re-creating the ViewPager.
|
||||
setupPager(binding)
|
||||
|
||||
binding.homeShuffleFab.setOnClickListener { playbackModel.shuffleAll() }
|
||||
|
||||
binding.homeNewPlaylistFab.apply {
|
||||
inflate(R.menu.new_playlist_actions)
|
||||
setOnActionSelectedListener(this@HomeFragment)
|
||||
setChangeListener(homeModel::setSpeedDialOpen)
|
||||
}
|
||||
|
||||
hideAllFabs()
|
||||
updateFabVisibility(
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value,
|
||||
homeModel.sheetObscuresFab.value,
|
||||
homeModel.currentTabType.value)
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
collect(homeModel.recreateTabs.flow, ::handleRecreate)
|
||||
collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
|
||||
collectImmediately(
|
||||
homeModel.songList, homeModel.isFastScrolling, homeModel.sheetObscuresFab, ::updateFab)
|
||||
collect(homeModel.speedDialOpen, ::updateSpeedDial)
|
||||
collect(detailModel.toShow.flow, ::handleShow)
|
||||
collect(listModel.menu.flow, ::handleMenu)
|
||||
collectImmediately(listModel.selected, ::updateSelection)
|
||||
|
@ -220,28 +195,11 @@ class HomeFragment :
|
|||
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Stock bottom sheet overlay won't work with our nested UI setup, have to replicate
|
||||
// it ourselves.
|
||||
requireBinding().root.rootView.apply {
|
||||
findViewById<View>(R.id.main_scrim).setOnTouchListener { _, event ->
|
||||
handleSpeedDialBoundaryTouch(event)
|
||||
}
|
||||
findViewById<View>(R.id.sheet_scrim).setOnTouchListener { _, event ->
|
||||
handleSpeedDialBoundaryTouch(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentHomeBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
storagePermissionLauncher = null
|
||||
binding.homeAppbar.removeOnOffsetChangedListener(this)
|
||||
binding.homeNormalToolbar.setOnMenuItemClickListener(null)
|
||||
binding.homeNewPlaylistFab.setChangeListener(null)
|
||||
binding.homeNewPlaylistFab.setOnActionSelectedListener(null)
|
||||
}
|
||||
|
||||
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
||||
|
@ -299,24 +257,6 @@ class HomeFragment :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onActionSelected(actionItem: SpeedDialActionItem): Boolean {
|
||||
when (actionItem.id) {
|
||||
R.id.action_new_playlist -> {
|
||||
logD("Creating playlist")
|
||||
musicModel.createPlaylist()
|
||||
}
|
||||
R.id.action_import_playlist -> {
|
||||
logD("Importing playlist")
|
||||
musicModel.importPlaylist()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
// Returning false to close th speed dial results in no animation, manually close instead.
|
||||
// Adapted from Material Files: https://github.com/zhanghai/MaterialFiles
|
||||
requireBinding().homeNewPlaylistFab.close()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setupPager(binding: FragmentHomeBinding) {
|
||||
binding.homePager.adapter =
|
||||
HomePagerAdapter(homeModel.currentTabTypes, childFragmentManager, viewLifecycleOwner)
|
||||
|
@ -358,12 +298,6 @@ class HomeFragment :
|
|||
MusicType.GENRES -> R.id.home_genre_recycler
|
||||
MusicType.PLAYLISTS -> R.id.home_playlist_recycler
|
||||
}
|
||||
|
||||
updateFabVisibility(
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value,
|
||||
homeModel.sheetObscuresFab.value,
|
||||
tabType)
|
||||
}
|
||||
|
||||
private fun handleRecreate(recreate: Unit?) {
|
||||
|
@ -395,11 +329,6 @@ class HomeFragment :
|
|||
private fun setupCompleteState(binding: FragmentHomeBinding, error: Exception?) {
|
||||
if (error == null) {
|
||||
logD("Received ok response")
|
||||
updateFabVisibility(
|
||||
homeModel.songList.value,
|
||||
homeModel.isFastScrolling.value,
|
||||
homeModel.sheetObscuresFab.value,
|
||||
homeModel.currentTabType.value)
|
||||
binding.homeIndexingContainer.visibility = View.INVISIBLE
|
||||
return
|
||||
}
|
||||
|
@ -544,118 +473,6 @@ class HomeFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateFab(songs: List<Song>, isFastScrolling: Boolean, sheetRising: Boolean) {
|
||||
updateFabVisibility(songs, isFastScrolling, sheetRising, homeModel.currentTabType.value)
|
||||
}
|
||||
|
||||
private fun updateFabVisibility(
|
||||
songs: List<Song>,
|
||||
isFastScrolling: Boolean,
|
||||
sheetRising: Boolean,
|
||||
tabType: MusicType
|
||||
) {
|
||||
val binding = requireBinding()
|
||||
// If there are no songs, it's likely that the library has not been loaded, so
|
||||
// displaying the shuffle FAB makes no sense. We also don't want the fast scroll
|
||||
// popup to overlap with the FAB, so we hide the FAB when fast scrolling too.
|
||||
if (songs.isEmpty() || isFastScrolling || sheetRising) {
|
||||
logD("Hiding fab: [empty: ${songs.isEmpty()} scrolling: $isFastScrolling]")
|
||||
hideAllFabs()
|
||||
} else {
|
||||
if (tabType != MusicType.PLAYLISTS) {
|
||||
logD("Showing shuffle button")
|
||||
if (binding.homeShuffleFab.isOrWillBeShown) {
|
||||
logD("Nothing to do")
|
||||
return
|
||||
}
|
||||
|
||||
if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) {
|
||||
logD("Animating transition")
|
||||
binding.homeNewPlaylistFab.hide(
|
||||
object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton) {
|
||||
super.onHidden(fab)
|
||||
binding.homeShuffleFab.show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
logD("Showing immediately")
|
||||
binding.homeShuffleFab.show()
|
||||
}
|
||||
} else {
|
||||
logD("Showing playlist button")
|
||||
if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) {
|
||||
logD("Nothing to do")
|
||||
return
|
||||
}
|
||||
|
||||
if (binding.homeShuffleFab.isOrWillBeShown) {
|
||||
logD("Animating transition")
|
||||
binding.homeShuffleFab.hide(
|
||||
object : FloatingActionButton.OnVisibilityChangedListener() {
|
||||
override fun onHidden(fab: FloatingActionButton) {
|
||||
super.onHidden(fab)
|
||||
binding.homeNewPlaylistFab.show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
logD("Showing immediately")
|
||||
binding.homeNewPlaylistFab.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideAllFabs() {
|
||||
val binding = requireBinding()
|
||||
if (binding.homeShuffleFab.isOrWillBeShown) {
|
||||
FAB_HIDE_FROM_USER_FIELD.invoke(binding.homeShuffleFab, null, false)
|
||||
}
|
||||
if (binding.homeNewPlaylistFab.mainFab.isOrWillBeShown) {
|
||||
FAB_HIDE_FROM_USER_FIELD.invoke(binding.homeNewPlaylistFab.mainFab, null, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSpeedDial(open: Boolean) {
|
||||
val binding = requireBinding()
|
||||
|
||||
if (open) {
|
||||
binding.homeNewPlaylistFab.open(true)
|
||||
} else {
|
||||
binding.homeNewPlaylistFab.close(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSpeedDialBoundaryTouch(event: MotionEvent): Boolean {
|
||||
val binding = binding ?: return false
|
||||
|
||||
if (homeModel.speedDialOpen.value && binding.homeNewPlaylistFab.isUnder(event.x, event.y)) {
|
||||
// Convert absolute coordinates to relative coordinates
|
||||
val offsetX = event.x - binding.homeNewPlaylistFab.x
|
||||
val offsetY = event.y - binding.homeNewPlaylistFab.y
|
||||
|
||||
// Create a new MotionEvent with relative coordinates
|
||||
val relativeEvent =
|
||||
MotionEvent.obtain(
|
||||
event.downTime,
|
||||
event.eventTime,
|
||||
event.action,
|
||||
offsetX,
|
||||
offsetY,
|
||||
event.metaState)
|
||||
|
||||
// Dispatch the relative MotionEvent to the target child view
|
||||
val handled = binding.homeNewPlaylistFab.dispatchTouchEvent(relativeEvent)
|
||||
|
||||
// Recycle the relative MotionEvent
|
||||
relativeEvent.recycle()
|
||||
|
||||
return handled
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun handleShow(show: Show?) {
|
||||
when (show) {
|
||||
is Show.SongDetails -> {
|
||||
|
|
|
@ -156,13 +156,6 @@ constructor(
|
|||
/** A marker for whether the user is fast-scrolling in the home view or not. */
|
||||
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
|
||||
|
||||
private val _speedDialOpen = MutableStateFlow(false)
|
||||
/** A marker for whether the speed dial is open or not. */
|
||||
val speedDialOpen: StateFlow<Boolean> = _speedDialOpen
|
||||
|
||||
private val _sheetObscuresFab = MutableStateFlow(false)
|
||||
val sheetObscuresFab: StateFlow<Boolean> = _sheetObscuresFab
|
||||
|
||||
private val _showOuter = MutableEvent<Outer>()
|
||||
val showOuter: Event<Outer>
|
||||
get() = _showOuter
|
||||
|
@ -300,20 +293,6 @@ constructor(
|
|||
_isFastScrolling.value = isFastScrolling
|
||||
}
|
||||
|
||||
/**
|
||||
* Update whether the speed dial is open or not.
|
||||
*
|
||||
* @param speedDialOpen true if the speed dial is open, false otherwise.
|
||||
*/
|
||||
fun setSpeedDialOpen(speedDialOpen: Boolean) {
|
||||
logD("Updating speed dial state: $speedDialOpen")
|
||||
_speedDialOpen.value = speedDialOpen
|
||||
}
|
||||
|
||||
fun setSheetObscuresFab(sheetRising: Boolean) {
|
||||
_sheetObscuresFab.value = sheetRising
|
||||
}
|
||||
|
||||
fun showSettings() {
|
||||
_showOuter.put(Outer.Settings)
|
||||
}
|
||||
|
|
|
@ -7,15 +7,55 @@
|
|||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/explore_nav_host"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"
|
||||
app:navGraph="@navigation/inner"
|
||||
tools:layout="@layout/fragment_home" />
|
||||
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/explore_nav_host"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/inner"
|
||||
tools:layout="@layout/fragment_home" />
|
||||
|
||||
<org.oxycblt.auxio.home.EdgeFrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_anchor="@id/home_content">
|
||||
|
||||
<org.oxycblt.auxio.home.ThemedSpeedDialView
|
||||
android:id="@+id/home_new_playlist_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/lbl_new_playlist"
|
||||
android:focusable="true"
|
||||
android:gravity="bottom|end"
|
||||
app:sdMainFabAnimationRotateAngle="135"
|
||||
app:sdMainFabClosedIconColor="@android:color/white"
|
||||
app:sdMainFabClosedSrc="@drawable/ic_add_24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/home_shuffle_fab"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/spacing_medium"
|
||||
android:contentDescription="@string/lbl_shuffle"
|
||||
android:src="@drawable/ic_shuffle_off_24" />
|
||||
|
||||
</org.oxycblt.auxio.home.EdgeFrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<View
|
||||
android:id="@+id/main_scrim"
|
||||
|
|
|
@ -148,37 +148,5 @@
|
|||
|
||||
</FrameLayout>
|
||||
|
||||
<org.oxycblt.auxio.home.EdgeFrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_anchor="@id/home_content">
|
||||
|
||||
|
||||
<org.oxycblt.auxio.home.ThemedSpeedDialView
|
||||
android:id="@+id/home_new_playlist_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="bottom|end"
|
||||
android:contentDescription="@string/lbl_new_playlist"
|
||||
app:sdMainFabAnimationRotateAngle="135"
|
||||
app:sdMainFabClosedIconColor="@android:color/white"
|
||||
app:sdMainFabClosedSrc="@drawable/ic_add_24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/home_shuffle_fab"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/lbl_shuffle"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/spacing_medium"
|
||||
android:src="@drawable/ic_shuffle_off_24" />
|
||||
|
||||
</org.oxycblt.auxio.home.EdgeFrameLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
@ -8,15 +8,53 @@
|
|||
android:background="?attr/colorSurface"
|
||||
android:transitionGroup="true">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/explore_nav_host"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"
|
||||
app:navGraph="@navigation/inner"
|
||||
tools:layout="@layout/fragment_home" />
|
||||
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/explore_nav_host"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/inner"
|
||||
tools:layout="@layout/fragment_home" />
|
||||
|
||||
<org.oxycblt.auxio.home.EdgeFrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
app:layout_anchor="@id/home_content">
|
||||
|
||||
<org.oxycblt.auxio.home.ThemedSpeedDialView
|
||||
android:id="@+id/home_new_playlist_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/lbl_new_playlist"
|
||||
android:focusable="true"
|
||||
android:gravity="bottom|end"
|
||||
app:sdMainFabAnimationRotateAngle="135"
|
||||
app:sdMainFabClosedIconColor="@android:color/white"
|
||||
app:sdMainFabClosedSrc="@drawable/ic_add_24" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/home_shuffle_fab"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/spacing_medium"
|
||||
android:contentDescription="@string/lbl_shuffle"
|
||||
android:src="@drawable/ic_shuffle_off_24" />
|
||||
|
||||
</org.oxycblt.auxio.home.EdgeFrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/main_scrim"
|
||||
|
@ -25,17 +63,17 @@
|
|||
|
||||
<View
|
||||
android:id="@+id/main_sheet_scrim"
|
||||
android:background="?attr/colorSurfaceContainerLow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0"
|
||||
android:layout_height="match_parent" />
|
||||
android:background="?attr/colorSurfaceContainerLow" />
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/playback_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
|
@ -55,10 +93,10 @@
|
|||
android:id="@+id/queue_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:elevation="1dp"
|
||||
android:clickable="true"
|
||||
android:elevation="1dp"
|
||||
android:focusable="true"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
|
Loading…
Reference in a new issue