home: add selection framework

Add the basic selection flow to the home UI.

This has no function yet. Further work needs to be done first.
This commit is contained in:
Alexander Capehart 2022-11-22 16:21:28 -07:00
parent 361ca422e3
commit 6f05697088
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
9 changed files with 83 additions and 22 deletions

View file

@ -18,9 +18,11 @@
package org.oxycblt.auxio.home package org.oxycblt.auxio.home
import android.app.Application import android.app.Application
import androidx.collection.arraySetOf
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.getAndUpdate
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -60,6 +62,10 @@ class HomeViewModel(application: Application) :
val genres: StateFlow<List<Genre>> val genres: StateFlow<List<Genre>>
get() = _genres get() = _genres
private val _selected = MutableStateFlow(listOf<Music>())
val selected: StateFlow<List<Music>>
get() = _selected
var tabs: List<MusicMode> = visibleTabs var tabs: List<MusicMode> = visibleTabs
private set private set
@ -84,6 +90,19 @@ class HomeViewModel(application: Application) :
musicStore.addCallback(this) musicStore.addCallback(this)
} }
/** Select a music item. */
fun select(item: Music) {
val items = _selected.value.toMutableList()
if (items.remove(item)) {
logD("Unselecting item $item")
_selected.value = items
} else {
logD("Selecting item $item")
items.add(item)
_selected.value = items
}
}
/** Update the current tab based off of the new ViewPager position. */ /** Update the current tab based off of the new ViewPager position. */
fun updateCurrentTab(pos: Int) { fun updateCurrentTab(pos: Int) {
logD("Updating current tab to ${tabs[pos]}") logD("Updating current tab to ${tabs[pos]}")

View file

@ -25,6 +25,7 @@ import java.util.Formatter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
@ -97,9 +98,9 @@ class AlbumListFragment : HomeListFragment<Album>() {
} }
} }
override fun onItemClick(item: Item) { override fun onItemClick(music: Music) {
check(item is Album) { "Unexpected datatype: ${item::class.java}" } check(music is Album) { "Unexpected datatype: ${music::class.java}" }
navModel.exploreNavigateTo(item) navModel.exploreNavigateTo(music)
} }
override fun onOpenMenu(item: Item, anchor: View) { override fun onOpenMenu(item: Item, anchor: View) {

View file

@ -23,6 +23,7 @@ import android.view.ViewGroup
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
@ -73,9 +74,9 @@ class ArtistListFragment : HomeListFragment<Artist>() {
} }
} }
override fun onItemClick(item: Item) { override fun onItemClick(music: Music) {
check(item is Artist) { "Unexpected datatype: ${item::class.java}" } check(music is Artist) { "Unexpected datatype: ${music::class.java}" }
navModel.exploreNavigateTo(item) navModel.exploreNavigateTo(music)
} }
override fun onOpenMenu(item: Item, anchor: View) { override fun onOpenMenu(item: Item, anchor: View) {

View file

@ -23,6 +23,7 @@ import android.view.ViewGroup
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
@ -72,9 +73,9 @@ class GenreListFragment : HomeListFragment<Genre>() {
} }
} }
override fun onItemClick(item: Item) { override fun onItemClick(music: Music) {
check(item is Genre) { "Unexpected datatype: ${item::class.java}" } check(music is Genre) { "Unexpected datatype: ${music::class.java}" }
navModel.exploreNavigateTo(item) navModel.exploreNavigateTo(music)
} }
override fun onOpenMenu(item: Item, anchor: View) { override fun onOpenMenu(item: Item, anchor: View) {

View file

@ -22,6 +22,8 @@ import android.view.LayoutInflater
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.ui.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.ui.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.ui.fragment.MenuFragment import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
@ -63,4 +65,25 @@ abstract class HomeListFragment<T : Item> :
override fun onFastScrollStop() { override fun onFastScrollStop() {
homeModel.updateFastScrolling(false) homeModel.updateFastScrolling(false)
} }
abstract fun onItemClick(music: Music)
override fun onItemClick(item: Item) {
check(item is Music) { "Unexpected datatype: ${item::class.java}" }
if(homeModel.selected.value.isEmpty()) {
onItemClick(item)
} else {
homeModel.select(item)
}
}
override fun onItemLongClick(item: Item) {
check(item is Music) { "Unexpected datatype: ${item::class.java}" }
if (homeModel.selected.value.isEmpty()) {
homeModel.select(item)
} else {
onItemClick(item)
}
}
} }

View file

@ -24,6 +24,7 @@ import android.view.ViewGroup
import java.util.Formatter import java.util.Formatter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
@ -34,6 +35,7 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SongViewHolder import org.oxycblt.auxio.ui.recycler.SongViewHolder
import org.oxycblt.auxio.ui.recycler.SyncListDiffer import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
@ -58,6 +60,7 @@ class SongListFragment : HomeListFragment<Song>() {
} }
collectImmediately(homeModel.songs, homeAdapter::replaceList) collectImmediately(homeModel.songs, homeAdapter::replaceList)
collectImmediately(homeModel.selected, homeAdapter::updateSelection)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
} }
@ -104,12 +107,12 @@ class SongListFragment : HomeListFragment<Song>() {
} }
} }
override fun onItemClick(item: Item) { override fun onItemClick(music: Music) {
check(item is Song) { "Unexpected datatype: ${item::class.java}" } check(music is Song) { "Unexpected datatype: ${music::class.java}" }
when (settings.libPlaybackMode) { when (settings.libPlaybackMode) {
MusicMode.SONGS -> playbackModel.playFromAll(item) MusicMode.SONGS -> playbackModel.playFromAll(music)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item) MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}") else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
} }
} }
@ -129,7 +132,7 @@ class SongListFragment : HomeListFragment<Song>() {
} }
private class SongAdapter(private val listener: MenuItemListener) : private class SongAdapter(private val listener: MenuItemListener) :
PlayingIndicatorAdapter<SongViewHolder>() { SelectionIndicatorAdapter<SongViewHolder>() {
private val differ = SyncListDiffer(this, SongViewHolder.DIFFER) private val differ = SyncListDiffer(this, SongViewHolder.DIFFER)
override val currentList: List<Item> override val currentList: List<Item>

View file

@ -40,6 +40,9 @@ interface ItemClickListener {
/** An interface for detecting if an item has had it's menu opened. */ /** An interface for detecting if an item has had it's menu opened. */
interface MenuItemListener : ItemClickListener { interface MenuItemListener : ItemClickListener {
/** Called when an item is long-clicked. */
fun onItemLongClick(item: Item) {}
/** Called when an item desires to open a menu relating to it. */ /** Called when an item desires to open a menu relating to it. */
fun onOpenMenu(item: Item, anchor: View) fun onOpenMenu(item: Item, anchor: View)
} }

View file

@ -3,6 +3,7 @@ package org.oxycblt.auxio.ui.recycler
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.util.logD
/** /**
* An adapter that implements selection indicators. * An adapter that implements selection indicators.
@ -23,14 +24,14 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> : Playing
} }
} }
fun updateSelection(items: Set<Music>) { fun updateSelection(items: List<Music>) {
val oldSelectedItems = selectedItems val oldSelectedItems = selectedItems
val newSelectedItems = items.toSet()
if (items == oldSelectedItems) { if (newSelectedItems == oldSelectedItems) {
return return
} }
selectedItems = items selectedItems = newSelectedItems
for (i in currentList.indices) { for (i in currentList.indices) {
// TODO: Perhaps add an optimization that allows me to avoid the O(n) iteration // TODO: Perhaps add an optimization that allows me to avoid the O(n) iteration
// assuming all list items are unique? // assuming all list items are unique?
@ -39,14 +40,16 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> : Playing
continue continue
} }
if (oldSelectedItems.contains(item) || items.contains(item)) { val added = !oldSelectedItems.contains(item) && newSelectedItems.contains(item)
val removed = oldSelectedItems.contains(item) && !newSelectedItems.contains(item)
if (added || removed) {
notifyItemChanged(i, PAYLOAD_INDICATOR_CHANGED) notifyItemChanged(i, PAYLOAD_INDICATOR_CHANGED)
} }
} }
} }
/** A ViewHolder that can respond to selection indicator updates. */ /** A ViewHolder that can respond to selection indicator updates. */
abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) { abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) {
abstract fun updateSelectionIndicator(isSelected: Boolean) abstract fun updateSelectionIndicator(isSelected: Boolean)
} }
} }

View file

@ -31,17 +31,19 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
/** /**
* The shared ViewHolder for a [Song]. * The shared ViewHolder for a [Song].
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class SongViewHolder private constructor(private val binding: ItemSongBinding) : class SongViewHolder private constructor(private val binding: ItemSongBinding) :
PlayingIndicatorAdapter.ViewHolder(binding.root) { SelectionIndicatorAdapter.ViewHolder(binding.root) {
fun bind(item: Song, listener: MenuItemListener) { fun bind(item: Song, listener: MenuItemListener) {
binding.songAlbumCover.bind(item) binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context) binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.resolveArtistContents(binding.context) binding.songInfo.text = item.resolveArtistContents(binding.context)
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) } binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) } binding.root.setOnClickListener { listener.onItemClick(item) }
} }
@ -51,6 +53,11 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
binding.songAlbumCover.isPlaying = isPlaying binding.songAlbumCover.isPlaying = isPlaying
} }
override fun updateSelectionIndicator(isSelected: Boolean) {
logD("Selected")
binding.root.isActivated = isSelected
}
companion object { companion object {
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_SONG const val VIEW_TYPE = IntegerTable.VIEW_TYPE_SONG