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.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import androidx.activity.BackEventCompat
|
import androidx.activity.BackEventCompat
|
||||||
|
@ -32,10 +34,14 @@ import androidx.navigation.findNavController
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.R as MR
|
import com.google.android.material.R as MR
|
||||||
import com.google.android.material.bottomsheet.BackportBottomSheetBehavior
|
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.MaterialShapeDrawable
|
||||||
import com.google.android.material.shape.ShapeAppearanceModel
|
import com.google.android.material.shape.ShapeAppearanceModel
|
||||||
import com.google.android.material.transition.MaterialFadeThrough
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import java.lang.reflect.Method
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
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.HomeViewModel
|
||||||
import org.oxycblt.auxio.home.Outer
|
import org.oxycblt.auxio.home.Outer
|
||||||
import org.oxycblt.auxio.list.ListViewModel
|
import org.oxycblt.auxio.list.ListViewModel
|
||||||
|
import org.oxycblt.auxio.music.IndexingState
|
||||||
import org.oxycblt.auxio.music.Music
|
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.music.Song
|
||||||
import org.oxycblt.auxio.playback.OpenPanel
|
import org.oxycblt.auxio.playback.OpenPanel
|
||||||
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
|
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.coordinatorLayoutBehavior
|
||||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||||
import org.oxycblt.auxio.util.getDimen
|
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.logD
|
||||||
import org.oxycblt.auxio.util.navigateSafe
|
import org.oxycblt.auxio.util.navigateSafe
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
@ -69,7 +80,10 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainFragment :
|
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 detailModel: DetailViewModel by activityViewModels()
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val listModel: ListViewModel by activityViewModels()
|
private val listModel: ListViewModel by activityViewModels()
|
||||||
|
@ -78,11 +92,12 @@ class MainFragment :
|
||||||
private var detailBackCallback: DetailBackPressedCallback? = null
|
private var detailBackCallback: DetailBackPressedCallback? = null
|
||||||
private var selectionBackCallback: SelectionBackPressedCallback? = null
|
private var selectionBackCallback: SelectionBackPressedCallback? = null
|
||||||
private var speedDialBackCallback: SpeedDialBackPressedCallback? = null
|
private var speedDialBackCallback: SpeedDialBackPressedCallback? = null
|
||||||
private var selectionNavigationListener: DialogAwareNavigationListener? = null
|
private var navigationListener: DialogAwareNavigationListener? = null
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
private var elevationNormal = 0f
|
private var elevationNormal = 0f
|
||||||
private var normalCornerSize = 0f
|
private var normalCornerSize = 0f
|
||||||
private var maxScaleXDistance = 0f
|
private var maxScaleXDistance = 0f
|
||||||
|
private var sheetRising: Boolean? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -113,10 +128,9 @@ class MainFragment :
|
||||||
DetailBackPressedCallback(detailModel).also { detailBackCallback = it }
|
DetailBackPressedCallback(detailModel).also { detailBackCallback = it }
|
||||||
val selectionBackCallback =
|
val selectionBackCallback =
|
||||||
SelectionBackPressedCallback(listModel).also { selectionBackCallback = it }
|
SelectionBackPressedCallback(listModel).also { selectionBackCallback = it }
|
||||||
val speedDialBackCallback =
|
speedDialBackCallback = SpeedDialBackPressedCallback(homeModel)
|
||||||
SpeedDialBackPressedCallback(homeModel).also { speedDialBackCallback = it }
|
|
||||||
|
|
||||||
selectionNavigationListener = DialogAwareNavigationListener(listModel::dropSelection)
|
navigationListener = DialogAwareNavigationListener(::onExploreNavigate)
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
val context = requireActivity()
|
val context = requireActivity()
|
||||||
|
@ -162,8 +176,22 @@ class MainFragment :
|
||||||
|
|
||||||
binding.playbackSheet.elevation = 0f
|
binding.playbackSheet.elevation = 0f
|
||||||
|
|
||||||
binding.mainScrim.setOnClickListener { homeModel.setSpeedDialOpen(false) }
|
binding.mainScrim.setOnClickListener { binding.homeNewPlaylistFab.close() }
|
||||||
binding.sheetScrim.setOnClickListener { homeModel.setSpeedDialOpen(false) }
|
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 ---
|
// --- VIEWMODEL SETUP ---
|
||||||
// This has to be done here instead of the playback panel to make sure that it's prioritized
|
// 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)
|
collect(detailModel.toShow.flow, ::handleShow)
|
||||||
collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
|
collectImmediately(detailModel.editedPlaylist, detailBackCallback::invalidateEnabled)
|
||||||
collectImmediately(homeModel.showOuter.flow, ::handleShowOuter)
|
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(listModel.selected, selectionBackCallback::invalidateEnabled)
|
||||||
collectImmediately(playbackModel.song, ::updateSong)
|
collectImmediately(playbackModel.song, ::updateSong)
|
||||||
collectImmediately(playbackModel.openPanel.flow, ::handlePanel)
|
collectImmediately(playbackModel.openPanel.flow, ::handlePanel)
|
||||||
|
@ -184,7 +214,7 @@ class MainFragment :
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
// Once we add the destination change callback, we will receive another initialization call,
|
// Once we add the destination change callback, we will receive another initialization call,
|
||||||
// so handle that by resetting the flag.
|
// so handle that by resetting the flag.
|
||||||
requireNotNull(selectionNavigationListener) { "NavigationListener was not available" }
|
requireNotNull(navigationListener) { "NavigationListener was not available" }
|
||||||
.attach(binding.exploreNavHost.findNavController())
|
.attach(binding.exploreNavHost.findNavController())
|
||||||
// Listener could still reasonably fire even if we clear the binding, attach/detach
|
// Listener could still reasonably fire even if we clear the binding, attach/detach
|
||||||
// our pre-draw listener our listener in onStart/onStop respectively.
|
// our pre-draw listener our listener in onStart/onStop respectively.
|
||||||
|
@ -202,12 +232,23 @@ class MainFragment :
|
||||||
addCallback(viewLifecycleOwner, requireNotNull(detailBackCallback))
|
addCallback(viewLifecycleOwner, requireNotNull(detailBackCallback))
|
||||||
addCallback(viewLifecycleOwner, requireNotNull(sheetBackCallback))
|
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() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
requireNotNull(selectionNavigationListener) { "NavigationListener was not available" }
|
requireNotNull(navigationListener) { "NavigationListener was not available" }
|
||||||
.release(binding.exploreNavHost.findNavController())
|
.release(binding.exploreNavHost.findNavController())
|
||||||
binding.playbackSheet.viewTreeObserver.removeOnPreDrawListener(this)
|
binding.playbackSheet.viewTreeObserver.removeOnPreDrawListener(this)
|
||||||
}
|
}
|
||||||
|
@ -218,7 +259,9 @@ class MainFragment :
|
||||||
sheetBackCallback = null
|
sheetBackCallback = null
|
||||||
detailBackCallback = null
|
detailBackCallback = null
|
||||||
selectionBackCallback = null
|
selectionBackCallback = null
|
||||||
selectionNavigationListener = null
|
navigationListener = null
|
||||||
|
binding.homeNewPlaylistFab.setChangeListener(null)
|
||||||
|
binding.homeNewPlaylistFab.setOnActionSelectedListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreDraw(): Boolean {
|
override fun onPreDraw(): Boolean {
|
||||||
|
@ -236,13 +279,18 @@ class MainFragment :
|
||||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||||
|
|
||||||
val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f)
|
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
|
||||||
// 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
|
||||||
// dial. Filtering out ACTION_MOVE events will cause back gestures to close the
|
// speed dial, which is super finicky behavior.
|
||||||
// speed dial, which is super finicky behavior.
|
val rising = playbackRatio > 0f
|
||||||
homeModel.setSpeedDialOpen(false)
|
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 playbackOutRatio = 1 - min(playbackRatio * 2, 1f)
|
||||||
val playbackInRatio = max(playbackRatio - 0.5f, 0f) * 2
|
val playbackInRatio = max(playbackRatio - 0.5f, 0f) * 2
|
||||||
|
@ -330,6 +378,193 @@ class MainFragment :
|
||||||
return true
|
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?) {
|
private fun handleShow(show: Show?) {
|
||||||
when (show) {
|
when (show) {
|
||||||
is Show.SongAlbumDetails,
|
is Show.SongAlbumDetails,
|
||||||
|
@ -355,13 +590,6 @@ class MainFragment :
|
||||||
homeModel.showOuter.consume()
|
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?) {
|
private fun updateSong(song: Song?) {
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
tryShowSheets()
|
tryShowSheets()
|
||||||
|
@ -566,8 +794,9 @@ class MainFragment :
|
||||||
private inner class SpeedDialBackPressedCallback(private val homeModel: HomeViewModel) :
|
private inner class SpeedDialBackPressedCallback(private val homeModel: HomeViewModel) :
|
||||||
OnBackPressedCallback(false) {
|
OnBackPressedCallback(false) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
if (homeModel.speedDialOpen.value) {
|
val binding = requireBinding()
|
||||||
homeModel.setSpeedDialOpen(false)
|
if (binding.homeNewPlaylistFab.isOpen) {
|
||||||
|
binding.homeNewPlaylistFab.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -575,4 +804,13 @@ class MainFragment :
|
||||||
isEnabled = open
|
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.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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.floatingactionbutton.FloatingActionButton
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
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 dagger.hilt.android.AndroidEntryPoint
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Method
|
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.Playlist
|
||||||
import org.oxycblt.auxio.music.PlaylistDecision
|
import org.oxycblt.auxio.music.PlaylistDecision
|
||||||
import org.oxycblt.auxio.music.PlaylistMessage
|
import org.oxycblt.auxio.music.PlaylistMessage
|
||||||
import org.oxycblt.auxio.music.Song
|
|
||||||
import org.oxycblt.auxio.music.external.M3U
|
import org.oxycblt.auxio.music.external.M3U
|
||||||
import org.oxycblt.auxio.playback.PlaybackDecision
|
import org.oxycblt.auxio.playback.PlaybackDecision
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.util.collect
|
import org.oxycblt.auxio.util.collect
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.getColorCompat
|
import org.oxycblt.auxio.util.getColorCompat
|
||||||
import org.oxycblt.auxio.util.isUnder
|
|
||||||
import org.oxycblt.auxio.util.lazyReflectedField
|
import org.oxycblt.auxio.util.lazyReflectedField
|
||||||
import org.oxycblt.auxio.util.lazyReflectedMethod
|
import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -95,9 +90,7 @@ import org.oxycblt.auxio.util.showToast
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class HomeFragment :
|
class HomeFragment :
|
||||||
SelectionFragment<FragmentHomeBinding>(),
|
SelectionFragment<FragmentHomeBinding>(), AppBarLayout.OnOffsetChangedListener {
|
||||||
AppBarLayout.OnOffsetChangedListener,
|
|
||||||
SpeedDialView.OnActionSelectedListener {
|
|
||||||
override val listModel: ListViewModel by activityViewModels()
|
override val listModel: ListViewModel by activityViewModels()
|
||||||
override val musicModel: MusicViewModel by activityViewModels()
|
override val musicModel: MusicViewModel by activityViewModels()
|
||||||
override val playbackModel: PlaybackViewModel by activityViewModels()
|
override val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
@ -190,27 +183,9 @@ class HomeFragment :
|
||||||
// re-creating the ViewPager.
|
// re-creating the ViewPager.
|
||||||
setupPager(binding)
|
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 ---
|
// --- VIEWMODEL SETUP ---
|
||||||
collect(homeModel.recreateTabs.flow, ::handleRecreate)
|
collect(homeModel.recreateTabs.flow, ::handleRecreate)
|
||||||
collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
|
collectImmediately(homeModel.currentTabType, ::updateCurrentTab)
|
||||||
collectImmediately(
|
|
||||||
homeModel.songList, homeModel.isFastScrolling, homeModel.sheetObscuresFab, ::updateFab)
|
|
||||||
collect(homeModel.speedDialOpen, ::updateSpeedDial)
|
|
||||||
collect(detailModel.toShow.flow, ::handleShow)
|
collect(detailModel.toShow.flow, ::handleShow)
|
||||||
collect(listModel.menu.flow, ::handleMenu)
|
collect(listModel.menu.flow, ::handleMenu)
|
||||||
collectImmediately(listModel.selected, ::updateSelection)
|
collectImmediately(listModel.selected, ::updateSelection)
|
||||||
|
@ -220,28 +195,11 @@ class HomeFragment :
|
||||||
collect(playbackModel.playbackDecision.flow, ::handlePlaybackDecision)
|
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) {
|
override fun onDestroyBinding(binding: FragmentHomeBinding) {
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
storagePermissionLauncher = null
|
storagePermissionLauncher = null
|
||||||
binding.homeAppbar.removeOnOffsetChangedListener(this)
|
binding.homeAppbar.removeOnOffsetChangedListener(this)
|
||||||
binding.homeNormalToolbar.setOnMenuItemClickListener(null)
|
binding.homeNormalToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.homeNewPlaylistFab.setChangeListener(null)
|
|
||||||
binding.homeNewPlaylistFab.setOnActionSelectedListener(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
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) {
|
private fun setupPager(binding: FragmentHomeBinding) {
|
||||||
binding.homePager.adapter =
|
binding.homePager.adapter =
|
||||||
HomePagerAdapter(homeModel.currentTabTypes, childFragmentManager, viewLifecycleOwner)
|
HomePagerAdapter(homeModel.currentTabTypes, childFragmentManager, viewLifecycleOwner)
|
||||||
|
@ -358,12 +298,6 @@ class HomeFragment :
|
||||||
MusicType.GENRES -> R.id.home_genre_recycler
|
MusicType.GENRES -> R.id.home_genre_recycler
|
||||||
MusicType.PLAYLISTS -> R.id.home_playlist_recycler
|
MusicType.PLAYLISTS -> R.id.home_playlist_recycler
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFabVisibility(
|
|
||||||
homeModel.songList.value,
|
|
||||||
homeModel.isFastScrolling.value,
|
|
||||||
homeModel.sheetObscuresFab.value,
|
|
||||||
tabType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRecreate(recreate: Unit?) {
|
private fun handleRecreate(recreate: Unit?) {
|
||||||
|
@ -395,11 +329,6 @@ class HomeFragment :
|
||||||
private fun setupCompleteState(binding: FragmentHomeBinding, error: Exception?) {
|
private fun setupCompleteState(binding: FragmentHomeBinding, error: Exception?) {
|
||||||
if (error == null) {
|
if (error == null) {
|
||||||
logD("Received ok response")
|
logD("Received ok response")
|
||||||
updateFabVisibility(
|
|
||||||
homeModel.songList.value,
|
|
||||||
homeModel.isFastScrolling.value,
|
|
||||||
homeModel.sheetObscuresFab.value,
|
|
||||||
homeModel.currentTabType.value)
|
|
||||||
binding.homeIndexingContainer.visibility = View.INVISIBLE
|
binding.homeIndexingContainer.visibility = View.INVISIBLE
|
||||||
return
|
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?) {
|
private fun handleShow(show: Show?) {
|
||||||
when (show) {
|
when (show) {
|
||||||
is Show.SongDetails -> {
|
is Show.SongDetails -> {
|
||||||
|
|
|
@ -156,13 +156,6 @@ constructor(
|
||||||
/** A marker for whether the user is fast-scrolling in the home view or not. */
|
/** A marker for whether the user is fast-scrolling in the home view or not. */
|
||||||
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
|
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>()
|
private val _showOuter = MutableEvent<Outer>()
|
||||||
val showOuter: Event<Outer>
|
val showOuter: Event<Outer>
|
||||||
get() = _showOuter
|
get() = _showOuter
|
||||||
|
@ -300,20 +293,6 @@ constructor(
|
||||||
_isFastScrolling.value = isFastScrolling
|
_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() {
|
fun showSettings() {
|
||||||
_showOuter.put(Outer.Settings)
|
_showOuter.put(Outer.Settings)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,15 +7,55 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorSurface">
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
|
||||||
android:id="@+id/explore_nav_host"
|
<FrameLayout
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:defaultNavHost="true"
|
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior">
|
||||||
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"
|
|
||||||
app:navGraph="@navigation/inner"
|
<androidx.fragment.app.FragmentContainerView
|
||||||
tools:layout="@layout/fragment_home" />
|
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
|
<View
|
||||||
android:id="@+id/main_scrim"
|
android:id="@+id/main_scrim"
|
||||||
|
|
|
@ -148,37 +148,5 @@
|
||||||
|
|
||||||
</FrameLayout>
|
</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>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
|
@ -8,15 +8,53 @@
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
android:transitionGroup="true">
|
android:transitionGroup="true">
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<FrameLayout
|
||||||
android:id="@+id/explore_nav_host"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:defaultNavHost="true"
|
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior">
|
||||||
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"
|
|
||||||
app:navGraph="@navigation/inner"
|
<androidx.fragment.app.FragmentContainerView
|
||||||
tools:layout="@layout/fragment_home" />
|
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
|
<View
|
||||||
android:id="@+id/main_scrim"
|
android:id="@+id/main_scrim"
|
||||||
|
@ -25,17 +63,17 @@
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/main_sheet_scrim"
|
android:id="@+id/main_sheet_scrim"
|
||||||
android:background="?attr/colorSurfaceContainerLow"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
android:alpha="0"
|
android:alpha="0"
|
||||||
android:layout_height="match_parent" />
|
android:background="?attr/colorSurfaceContainerLow" />
|
||||||
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
android:id="@+id/playback_sheet"
|
android:id="@+id/playback_sheet"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior">
|
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior">
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
@ -55,10 +93,10 @@
|
||||||
android:id="@+id/queue_sheet"
|
android:id="@+id/queue_sheet"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
|
||||||
android:elevation="1dp"
|
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
|
android:elevation="1dp"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
|
android:orientation="vertical"
|
||||||
app:layout_behavior="org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior">
|
app:layout_behavior="org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|
Loading…
Reference in a new issue