home: extract fab system to home

This commit is contained in:
Alexander Capehart 2024-07-20 14:52:03 -06:00
parent 80dac7d9e9
commit f3b73a5196
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 363 additions and 283 deletions

View file

@ -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)
}
}

View file

@ -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 -> {

View file

@ -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)
}

View file

@ -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"

View file

@ -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>

View file

@ -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