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:
OxygenCobalt 2021-10-24 10:17:55 -06:00
parent 97808ce1c3
commit 71480d0299
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 84 additions and 138 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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