home: move shuffle control to fab
Instead of having a play/pause header at the top of the song list, use a FAB instead. This allows people to shuffle all of their songs even if the songs tab isn't enabled, and it can be tranformed into a create FAB when playlists are added.
This commit is contained in:
parent
97808ce1c3
commit
71480d0299
7 changed files with 84 additions and 138 deletions
|
@ -47,6 +47,7 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.SortMode
|
import org.oxycblt.auxio.ui.SortMode
|
||||||
import org.oxycblt.auxio.util.applyEdge
|
import org.oxycblt.auxio.util.applyEdge
|
||||||
|
@ -59,6 +60,7 @@ import org.oxycblt.auxio.util.logE
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
|
private val playbackModel: PlaybackViewModel 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()
|
||||||
|
|
||||||
|
@ -68,6 +70,7 @@ class HomeFragment : Fragment() {
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val binding = FragmentHomeBinding.inflate(inflater)
|
val binding = FragmentHomeBinding.inflate(inflater)
|
||||||
|
var bottomPadding = 0
|
||||||
val sortItem: MenuItem
|
val sortItem: MenuItem
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
@ -75,6 +78,8 @@ class HomeFragment : Fragment() {
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
binding.applyEdge { bars ->
|
binding.applyEdge { bars ->
|
||||||
|
bottomPadding = bars.bottom
|
||||||
|
updateFabPadding(binding, bottomPadding)
|
||||||
binding.homeAppbar.updatePadding(top = bars.top)
|
binding.homeAppbar.updatePadding(top = bars.top)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,6 +195,10 @@ class HomeFragment : Fragment() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.homeFab.setOnClickListener {
|
||||||
|
playbackModel.shuffleAll()
|
||||||
|
}
|
||||||
|
|
||||||
TabLayoutMediator(binding.homeTabs, binding.homePager) { tab, pos ->
|
TabLayoutMediator(binding.homeTabs, binding.homePager) { tab, pos ->
|
||||||
tab.setText(homeModel.tabs[pos].string)
|
tab.setText(homeModel.tabs[pos].string)
|
||||||
}.attach()
|
}.attach()
|
||||||
|
@ -206,38 +215,28 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
homeModel.curTab.observe(viewLifecycleOwner) { tab ->
|
homeModel.curTab.observe(viewLifecycleOwner) { t ->
|
||||||
|
val tab = requireNotNull(t)
|
||||||
|
|
||||||
// Make sure that we update the scrolling view and allowed menu items before whenever
|
// Make sure that we update the scrolling view and allowed menu items before whenever
|
||||||
// the tab changes.
|
// the tab changes.
|
||||||
val targetView = when (requireNotNull(tab)) {
|
when (tab) {
|
||||||
DisplayMode.SHOW_SONGS -> {
|
DisplayMode.SHOW_SONGS -> updateSortMenu(sortItem, tab)
|
||||||
updateSortMenu(sortItem, tab)
|
|
||||||
R.id.home_song_list
|
DisplayMode.SHOW_ALBUMS -> updateSortMenu(sortItem, tab) { id ->
|
||||||
|
id != R.id.option_sort_album
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayMode.SHOW_ALBUMS -> {
|
DisplayMode.SHOW_ARTISTS -> updateSortMenu(sortItem, tab) { id ->
|
||||||
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_album }
|
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
||||||
R.id.home_album_list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayMode.SHOW_ARTISTS -> {
|
DisplayMode.SHOW_GENRES -> updateSortMenu(sortItem, tab) { id ->
|
||||||
updateSortMenu(sortItem, tab) { id ->
|
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
||||||
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.home_artist_list
|
|
||||||
}
|
|
||||||
|
|
||||||
DisplayMode.SHOW_GENRES -> {
|
|
||||||
updateSortMenu(sortItem, tab) { id ->
|
|
||||||
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.home_genre_list
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeAppbar.liftOnScrollTargetViewId = targetView
|
binding.homeAppbar.liftOnScrollTargetViewId = tab.viewId
|
||||||
}
|
}
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||||
|
@ -267,6 +266,10 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playbackModel.song.observe(viewLifecycleOwner) {
|
||||||
|
updateFabPadding(binding, bottomPadding)
|
||||||
|
}
|
||||||
|
|
||||||
logD("Fragment Created.")
|
logD("Fragment Created.")
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
@ -288,6 +291,30 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateFabPadding(
|
||||||
|
binding: FragmentHomeBinding,
|
||||||
|
bottomPadding: Int
|
||||||
|
) {
|
||||||
|
// To get our FAB to work with edge-to-edge, we need keep track of the bar view and update
|
||||||
|
// the padding based off of that. However, we can't use the shared method here since FABs
|
||||||
|
// don't respect padding, so we duplicate the code here except with the margins instead.
|
||||||
|
val fabParams = binding.homeFab.layoutParams as CoordinatorLayout.LayoutParams
|
||||||
|
val baseSpacing = resources.getDimensionPixelSize(R.dimen.spacing_medium)
|
||||||
|
|
||||||
|
if (playbackModel.song.value == null) {
|
||||||
|
fabParams.bottomMargin = baseSpacing + bottomPadding
|
||||||
|
} else {
|
||||||
|
fabParams.bottomMargin = baseSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DisplayMode.viewId: Int get() = when (this) {
|
||||||
|
DisplayMode.SHOW_SONGS -> R.id.home_song_list
|
||||||
|
DisplayMode.SHOW_ALBUMS -> R.id.home_album_list
|
||||||
|
DisplayMode.SHOW_ARTISTS -> R.id.home_artist_list
|
||||||
|
DisplayMode.SHOW_GENRES -> R.id.home_genre_list
|
||||||
|
}
|
||||||
|
|
||||||
private inner class HomePagerAdapter :
|
private inner class HomePagerAdapter :
|
||||||
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
|
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ import kotlin.math.sqrt
|
||||||
* The custom drawable used as FastScrollRecyclerView's popup background.
|
* The custom drawable used as FastScrollRecyclerView's popup background.
|
||||||
* This is an adaptation from AndroidFastScroll's MD2 theme.
|
* This is an adaptation from AndroidFastScroll's MD2 theme.
|
||||||
*
|
*
|
||||||
* Attributions as per the Apache 2.0 license:
|
* Attributions as per the Apache 2.0 license:
|
||||||
* ORIGINAL AUTHOR: Zhanghai [https://github.com/zhanghai]
|
* ORIGINAL AUTHOR: Zhanghai [https://github.com/zhanghai]
|
||||||
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
|
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
|
||||||
* MODIFIER: OxygenCobalt [https://github.com/]
|
* MODIFIER: OxygenCobalt [https://github.com/]
|
||||||
|
|
|
@ -22,9 +22,7 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemPlayShuffleBinding
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.SongViewHolder
|
import org.oxycblt.auxio.ui.SongViewHolder
|
||||||
|
@ -32,7 +30,6 @@ import org.oxycblt.auxio.ui.SortMode
|
||||||
import org.oxycblt.auxio.ui.newMenu
|
import org.oxycblt.auxio.ui.newMenu
|
||||||
import org.oxycblt.auxio.ui.sliceArticle
|
import org.oxycblt.auxio.ui.sliceArticle
|
||||||
import org.oxycblt.auxio.util.applySpans
|
import org.oxycblt.auxio.util.applySpans
|
||||||
import org.oxycblt.auxio.util.inflater
|
|
||||||
|
|
||||||
class SongListFragment : HomeListFragment() {
|
class SongListFragment : HomeListFragment() {
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
@ -57,95 +54,34 @@ class SongListFragment : HomeListFragment() {
|
||||||
|
|
||||||
override val popupProvider: (Int) -> String
|
override val popupProvider: (Int) -> String
|
||||||
get() = { idx ->
|
get() = { idx ->
|
||||||
if (idx != 0) {
|
val song = homeModel.songs.value!![idx]
|
||||||
val song = homeModel.songs.value!![idx]
|
|
||||||
|
|
||||||
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
||||||
SortMode.ASCENDING, SortMode.DESCENDING -> song.name.sliceArticle()
|
SortMode.ASCENDING, SortMode.DESCENDING -> song.name.sliceArticle()
|
||||||
.first().uppercase()
|
.first().uppercase()
|
||||||
|
|
||||||
SortMode.ARTIST -> song.album.artist.name.sliceArticle()
|
SortMode.ARTIST -> song.album.artist.name.sliceArticle()
|
||||||
.first().uppercase()
|
.first().uppercase()
|
||||||
|
|
||||||
SortMode.ALBUM -> song.album.name.sliceArticle()
|
SortMode.ALBUM -> song.album.name.sliceArticle()
|
||||||
.first().uppercase()
|
.first().uppercase()
|
||||||
|
|
||||||
SortMode.YEAR -> song.album.year.toString()
|
SortMode.YEAR -> song.album.year.toString()
|
||||||
}
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class SongsAdapter(
|
inner class SongsAdapter(
|
||||||
private val doOnClick: (data: Song) -> Unit,
|
private val doOnClick: (data: Song) -> Unit,
|
||||||
private val doOnLongClick: (view: View, data: Song) -> Unit,
|
private val doOnLongClick: (view: View, data: Song) -> Unit,
|
||||||
) : HomeAdapter<Song, RecyclerView.ViewHolder>() {
|
) : HomeAdapter<Song, SongViewHolder>() {
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int = data.size
|
||||||
return if (data.isNotEmpty()) {
|
|
||||||
data.size + 1 // Make space for the play/shuffle header
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
|
||||||
} else {
|
return SongViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||||
data.size
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
|
||||||
return if (position == 0) {
|
holder.bind(data[position])
|
||||||
PLAY_ITEM_TYPE
|
|
||||||
} else {
|
|
||||||
SongViewHolder.ITEM_TYPE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
return when (viewType) {
|
|
||||||
PLAY_ITEM_TYPE -> PlayViewHolder(
|
|
||||||
ItemPlayShuffleBinding.inflate(parent.context.inflater)
|
|
||||||
)
|
|
||||||
|
|
||||||
SongViewHolder.ITEM_TYPE -> SongViewHolder.from(
|
|
||||||
parent.context, doOnClick, doOnLongClick
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> error("Invalid viewholder item type.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
if (holder is SongViewHolder) {
|
|
||||||
holder.bind(data[position - 1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The viewholder for the play/shuffle header on the song header.
|
|
||||||
* Using a FAB would have been more conventional here, but it's so difficult to get a FAB
|
|
||||||
* to play along with edge-to-edge and nested RecyclerView instances to the point where I
|
|
||||||
* may as well not bother.
|
|
||||||
*/
|
|
||||||
private inner class PlayViewHolder(
|
|
||||||
binding: ItemPlayShuffleBinding
|
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
init {
|
|
||||||
// Force the layout to *actually* be the screen width.
|
|
||||||
// We can't inherit BaseViewHolder here since this ViewHolder isn't really connected
|
|
||||||
// to an item.
|
|
||||||
binding.root.layoutParams = RecyclerView.LayoutParams(
|
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.playButton.setOnClickListener {
|
|
||||||
playbackModel.playAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.shuffleButton.setOnClickListener {
|
|
||||||
playbackModel.shuffleAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val PLAY_ITEM_TYPE = 0xA00E
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -196,9 +196,10 @@ fun View.applyEdge(onApply: (Rect) -> Unit) {
|
||||||
* Auxio things should be better.
|
* Auxio things should be better.
|
||||||
* TODO: Get rid of this get rid of this get rid of this
|
* TODO: Get rid of this get rid of this get rid of this
|
||||||
*/
|
*/
|
||||||
fun RecyclerView.applyEdgeRespectingBar(
|
fun View.applyEdgeRespectingBar(
|
||||||
playbackModel: PlaybackViewModel,
|
playbackModel: PlaybackViewModel,
|
||||||
viewLifecycleOwner: LifecycleOwner
|
viewLifecycleOwner: LifecycleOwner,
|
||||||
|
initialPadding: Int = 0
|
||||||
) {
|
) {
|
||||||
var bottomPadding = 0
|
var bottomPadding = 0
|
||||||
|
|
||||||
|
@ -208,7 +209,7 @@ fun RecyclerView.applyEdgeRespectingBar(
|
||||||
if (playbackModel.song.value == null) {
|
if (playbackModel.song.value == null) {
|
||||||
updatePadding(bottom = bottomPadding)
|
updatePadding(bottom = bottomPadding)
|
||||||
} else {
|
} else {
|
||||||
updatePadding(bottom = 0)
|
updatePadding(bottom = initialPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +217,7 @@ fun RecyclerView.applyEdgeRespectingBar(
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
updatePadding(bottom = bottomPadding)
|
updatePadding(bottom = bottomPadding)
|
||||||
} else {
|
} else {
|
||||||
updatePadding(bottom = 0)
|
updatePadding(bottom = initialPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,5 +48,16 @@
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||||
tools:layout="@layout/fragment_home_list" />
|
tools:layout="@layout/fragment_home_list" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/home_fab"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:layout_margin="@dimen/spacing_medium"
|
||||||
|
android:contentDescription="@string/desc_shuffle_all"
|
||||||
|
app:layout_anchor="@id/home_pager"
|
||||||
|
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
|
||||||
|
app:layout_anchorGravity="bottom|end" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
</layout>
|
</layout>
|
|
@ -1,30 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:background="@drawable/ui_header_dividers"
|
|
||||||
android:padding="@dimen/spacing_medium"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/play_button"
|
|
||||||
style="@style/Widget.Auxio.Button.Secondary"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_marginEnd="@dimen/spacing_small"
|
|
||||||
android:text="@string/lbl_play" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/shuffle_button"
|
|
||||||
style="@style/Widget.Auxio.Button.Primary"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_marginStart="@dimen/spacing_small"
|
|
||||||
android:text="@string/lbl_shuffle" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</layout>
|
|
|
@ -119,6 +119,7 @@
|
||||||
<string name="desc_skip_prev">Skip to last song</string>
|
<string name="desc_skip_prev">Skip to last song</string>
|
||||||
<string name="desc_change_loop">Change repeat mode</string>
|
<string name="desc_change_loop">Change repeat mode</string>
|
||||||
<string name="desc_shuffle">Turn shuffle on or off</string>
|
<string name="desc_shuffle">Turn shuffle on or off</string>
|
||||||
|
<string name="desc_shuffle_all">Shuffle all songs</string>
|
||||||
|
|
||||||
<string name="desc_clear_user_queue">Clear queue</string>
|
<string name="desc_clear_user_queue">Clear queue</string>
|
||||||
<string name="desc_clear_queue_item">Remove this queue item</string>
|
<string name="desc_clear_queue_item">Remove this queue item</string>
|
||||||
|
|
Loading…
Reference in a new issue