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.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.SortMode
|
||||
import org.oxycblt.auxio.util.applyEdge
|
||||
|
@ -59,6 +60,7 @@ import org.oxycblt.auxio.util.logE
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class HomeFragment : Fragment() {
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
|
@ -68,6 +70,7 @@ class HomeFragment : Fragment() {
|
|||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val binding = FragmentHomeBinding.inflate(inflater)
|
||||
var bottomPadding = 0
|
||||
val sortItem: MenuItem
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
@ -75,6 +78,8 @@ class HomeFragment : Fragment() {
|
|||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.applyEdge { bars ->
|
||||
bottomPadding = bars.bottom
|
||||
updateFabPadding(binding, bottomPadding)
|
||||
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 ->
|
||||
tab.setText(homeModel.tabs[pos].string)
|
||||
}.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
|
||||
// the tab changes.
|
||||
val targetView = when (requireNotNull(tab)) {
|
||||
DisplayMode.SHOW_SONGS -> {
|
||||
updateSortMenu(sortItem, tab)
|
||||
R.id.home_song_list
|
||||
when (tab) {
|
||||
DisplayMode.SHOW_SONGS -> updateSortMenu(sortItem, tab)
|
||||
|
||||
DisplayMode.SHOW_ALBUMS -> updateSortMenu(sortItem, tab) { id ->
|
||||
id != R.id.option_sort_album
|
||||
}
|
||||
|
||||
DisplayMode.SHOW_ALBUMS -> {
|
||||
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_album }
|
||||
R.id.home_album_list
|
||||
DisplayMode.SHOW_ARTISTS -> updateSortMenu(sortItem, tab) { id ->
|
||||
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
||||
}
|
||||
|
||||
DisplayMode.SHOW_ARTISTS -> {
|
||||
updateSortMenu(sortItem, tab) { id ->
|
||||
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
|
||||
DisplayMode.SHOW_GENRES -> updateSortMenu(sortItem, tab) { id ->
|
||||
id == R.id.option_sort_asc || id == R.id.option_sort_dsc
|
||||
}
|
||||
}
|
||||
|
||||
binding.homeAppbar.liftOnScrollTargetViewId = targetView
|
||||
binding.homeAppbar.liftOnScrollTargetViewId = tab.viewId
|
||||
}
|
||||
|
||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||
|
@ -267,6 +266,10 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
playbackModel.song.observe(viewLifecycleOwner) {
|
||||
updateFabPadding(binding, bottomPadding)
|
||||
}
|
||||
|
||||
logD("Fragment Created.")
|
||||
|
||||
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 :
|
||||
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ import kotlin.math.sqrt
|
|||
* The custom drawable used as FastScrollRecyclerView's popup background.
|
||||
* 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]
|
||||
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
|
||||
* MODIFIER: OxygenCobalt [https://github.com/]
|
||||
|
|
|
@ -22,9 +22,7 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemPlayShuffleBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
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.sliceArticle
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
class SongListFragment : HomeListFragment() {
|
||||
override fun onCreateView(
|
||||
|
@ -57,95 +54,34 @@ class SongListFragment : HomeListFragment() {
|
|||
|
||||
override val popupProvider: (Int) -> String
|
||||
get() = { idx ->
|
||||
if (idx != 0) {
|
||||
val song = homeModel.songs.value!![idx]
|
||||
val song = homeModel.songs.value!![idx]
|
||||
|
||||
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
||||
SortMode.ASCENDING, SortMode.DESCENDING -> song.name.sliceArticle()
|
||||
.first().uppercase()
|
||||
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
||||
SortMode.ASCENDING, SortMode.DESCENDING -> song.name.sliceArticle()
|
||||
.first().uppercase()
|
||||
|
||||
SortMode.ARTIST -> song.album.artist.name.sliceArticle()
|
||||
.first().uppercase()
|
||||
SortMode.ARTIST -> song.album.artist.name.sliceArticle()
|
||||
.first().uppercase()
|
||||
|
||||
SortMode.ALBUM -> song.album.name.sliceArticle()
|
||||
.first().uppercase()
|
||||
SortMode.ALBUM -> song.album.name.sliceArticle()
|
||||
.first().uppercase()
|
||||
|
||||
SortMode.YEAR -> song.album.year.toString()
|
||||
}
|
||||
} else {
|
||||
""
|
||||
SortMode.YEAR -> song.album.year.toString()
|
||||
}
|
||||
}
|
||||
|
||||
inner class SongsAdapter(
|
||||
private val doOnClick: (data: Song) -> Unit,
|
||||
private val doOnLongClick: (view: View, data: Song) -> Unit,
|
||||
) : HomeAdapter<Song, RecyclerView.ViewHolder>() {
|
||||
override fun getItemCount(): Int {
|
||||
return if (data.isNotEmpty()) {
|
||||
data.size + 1 // Make space for the play/shuffle header
|
||||
} else {
|
||||
data.size
|
||||
}
|
||||
) : HomeAdapter<Song, SongViewHolder>() {
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
|
||||
return SongViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == 0) {
|
||||
PLAY_ITEM_TYPE
|
||||
} else {
|
||||
SongViewHolder.ITEM_TYPE
|
||||
}
|
||||
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
|
||||
holder.bind(data[position])
|
||||
}
|
||||
|
||||
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.
|
||||
* TODO: Get rid of this get rid of this get rid of this
|
||||
*/
|
||||
fun RecyclerView.applyEdgeRespectingBar(
|
||||
fun View.applyEdgeRespectingBar(
|
||||
playbackModel: PlaybackViewModel,
|
||||
viewLifecycleOwner: LifecycleOwner
|
||||
viewLifecycleOwner: LifecycleOwner,
|
||||
initialPadding: Int = 0
|
||||
) {
|
||||
var bottomPadding = 0
|
||||
|
||||
|
@ -208,7 +209,7 @@ fun RecyclerView.applyEdgeRespectingBar(
|
|||
if (playbackModel.song.value == null) {
|
||||
updatePadding(bottom = bottomPadding)
|
||||
} else {
|
||||
updatePadding(bottom = 0)
|
||||
updatePadding(bottom = initialPadding)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,7 +217,7 @@ fun RecyclerView.applyEdgeRespectingBar(
|
|||
if (song == null) {
|
||||
updatePadding(bottom = bottomPadding)
|
||||
} else {
|
||||
updatePadding(bottom = 0)
|
||||
updatePadding(bottom = initialPadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,5 +48,16 @@
|
|||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
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>
|
||||
</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_change_loop">Change repeat mode</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_queue_item">Remove this queue item</string>
|
||||
|
|
Loading…
Reference in a new issue