search: add selection

Add selection to the search view.
This commit is contained in:
Alexander Capehart 2022-12-17 16:15:47 -07:00
parent 3d03194878
commit 8aeb6d092e
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 149 additions and 100 deletions

View file

@ -59,6 +59,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
import org.oxycblt.auxio.ui.selection.SelectionViewModel
import org.oxycblt.auxio.util.*
@ -67,7 +68,7 @@ import org.oxycblt.auxio.util.*
* respective item.
* @author OxygenCobalt
*/
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener {
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener, SelectionToolbarOverlay.Callback {
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val homeModel: HomeViewModel by androidActivityViewModels()
private val musicModel: MusicViewModel by activityViewModels()
@ -114,9 +115,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
}
}
binding.homeToolbarOverlay.registerListeners(
onExit = { selectionModel.consume() }, onMenuItemClick = this)
binding.homeToolbarOverlay.callback = this
binding.homeToolbar.setOnMenuItemClickListener(this@HomeFragment)
updateTabConfiguration()
@ -174,14 +173,12 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
override fun onDestroyBinding(binding: FragmentHomeBinding) {
super.onDestroyBinding(binding)
binding.homeToolbarOverlay.unregisterListeners()
binding.homeToolbarOverlay.callback = null
binding.homeToolbar.setOnMenuItemClickListener(null)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
// HOME
R.id.action_search -> {
logD("Navigating to search")
// Reset selection (navigating to another selectable screen)
@ -210,16 +207,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
.withAscending(item.isChecked))
}
// SELECTION
R.id.action_play_next_selection -> {
playbackModel.playNext(selectionModel.consume())
requireContext().showToast(R.string.lng_queue_added)
}
R.id.action_queue_add_selection -> {
playbackModel.addToQueue(selectionModel.consume())
requireContext().showToast(R.string.lng_queue_added)
}
else -> {
// Sorting option was selected, mark it as selected and update the mode
item.isChecked = true
@ -230,9 +217,24 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
}
}
// Always handling an item
return true
}
override fun onClearSelection() {
selectionModel.consume()
}
override fun onPlaySelectionNext() {
playbackModel.playNext(selectionModel.consume())
requireContext().showToast(R.string.lng_queue_added)
}
override fun onAddSelectionToQueue() {
playbackModel.addToQueue(selectionModel.consume())
requireContext().showToast(R.string.lng_queue_added)
}
private fun updateCurrentTab(tab: MusicMode) {
// Make sure that we update the scrolling view and allowed menu items whenever
// the tab changes.
@ -453,9 +455,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
* https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414
*/
private fun ViewPager2.reduceSensitivity(by: Int) {
val recycler = VIEW_PAGER_RECYCLER_FIELD.get(this@reduceSensitivity)
val slop = VIEW_PAGER_TOUCH_SLOP_FIELD.get(recycler) as Int
VIEW_PAGER_TOUCH_SLOP_FIELD.set(recycler, slop * by)
val recycler = VP_RECYCLER_FIELD.get(this@reduceSensitivity)
val slop = RV_TOUCH_SLOP_FIELD.get(recycler) as Int
RV_TOUCH_SLOP_FIELD.set(recycler, slop * by)
}
/** Forces the view to recreate all fragments contained within it. */
@ -480,9 +482,9 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
}
companion object {
private val VIEW_PAGER_RECYCLER_FIELD: Field by
private val VP_RECYCLER_FIELD: Field by
lazyReflectedField(ViewPager2::class, "mRecyclerView")
private val VIEW_PAGER_TOUCH_SLOP_FIELD: Field by
private val RV_TOUCH_SLOP_FIELD: Field by
lazyReflectedField(RecyclerView::class, "mTouchSlop")
private const val KEY_LAST_TRANSITION_AXIS =
BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS"

View file

@ -24,20 +24,10 @@ 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.ui.recycler.AlbumViewHolder
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.ui.recycler.SongViewHolder
import org.oxycblt.auxio.ui.recycler.*
class SearchAdapter(private val listener: MenuItemListener) :
PlayingIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
private val differ = AsyncListDiffer(this, DIFFER)
override fun getItemCount() = differ.currentList.size

View file

@ -27,6 +27,7 @@ import androidx.core.view.isInvisible
import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.transition.MaterialSharedAxis
import org.oxycblt.auxio.R
@ -42,22 +43,21 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.util.androidViewModels
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
import org.oxycblt.auxio.ui.selection.SelectionViewModel
import org.oxycblt.auxio.util.*
/**
* A [Fragment] that allows for the searching of the entire music library.
* FIXME: Keyboard logic is really wonky
* @author OxygenCobalt
*/
class SearchFragment :
MenuFragment<FragmentSearchBinding>(), MenuItemListener, Toolbar.OnMenuItemClickListener {
MenuFragment<FragmentSearchBinding>(), MenuItemListener, Toolbar.OnMenuItemClickListener, SelectionToolbarOverlay.Callback {
// SearchViewModel is only scoped to this Fragment
private val searchModel: SearchViewModel by androidViewModels()
private val selectionModel: SelectionViewModel by activityViewModels()
private val searchAdapter = SearchAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
@ -78,6 +78,8 @@ class SearchFragment :
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
binding.searchToolbarOverlay.callback = this
binding.searchToolbar.apply {
val itemIdToSelect =
when (searchModel.filterMode) {
@ -91,7 +93,12 @@ class SearchFragment :
menu.findItem(itemIdToSelect).isChecked = true
setNavigationOnClickListener {
// Reset selection (navigating to another selectable screen)
selectionModel.consume()
// Drop keyboard as it's no longer needed
imm.hide()
findNavController().navigateUp()
}
@ -121,40 +128,61 @@ class SearchFragment :
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
collectImmediately(selectionModel.selected, ::handleSelection)
}
override fun onDestroyBinding(binding: FragmentSearchBinding) {
binding.searchToolbarOverlay.callback = null
binding.searchToolbar.setOnMenuItemClickListener(null)
binding.searchRecycler.adapter = null
}
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.submenu_filtering -> {}
else -> {
if (item.itemId != R.id.submenu_filtering) {
searchModel.updateFilterModeWithId(item.itemId)
item.isChecked = true
}
}
if (item.itemId != R.id.submenu_filtering) {
searchModel.updateFilterModeWithId(item.itemId)
item.isChecked = true
}
return true
}
override fun onClearSelection() {
selectionModel.consume()
}
override fun onPlaySelectionNext() {
playbackModel.playNext(selectionModel.consume())
requireContext().showToast(R.string.lng_queue_added)
}
override fun onAddSelectionToQueue() {
playbackModel.addToQueue(selectionModel.consume())
requireContext().showToast(R.string.lng_queue_added)
}
override fun onItemClick(item: Item) {
when (item) {
is Song ->
when (settings.libPlaybackMode) {
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
}
is MusicParent -> navModel.exploreNavigateTo(item)
check(item is Music) { "Unexpected datatype ${item::class.simpleName}"}
if (selectionModel.selected.value.isEmpty()) {
when (item) {
is Song ->
when (settings.libPlaybackMode) {
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
}
is MusicParent -> navModel.exploreNavigateTo(item)
}
} else {
selectionModel.select(item)
}
}
override fun onSelect(item: Item) {
check(item is Music) { "Unexpected datatype ${item::class.simpleName}"}
selectionModel.select(item)
}
override fun onOpenMenu(item: Item, anchor: View) {
when (item) {
is Song -> musicMenu(anchor, R.menu.menu_song_actions, item)
@ -193,9 +221,20 @@ class SearchFragment :
else -> return
})
// Reset selection (navigating to another selectable screen)
selectionModel.consume()
// Drop keyboard as it's no longer needed
imm.hide()
}
private fun handleSelection(selected: List<Music>) {
searchAdapter.updateSelection(selected)
if (requireBinding().searchToolbarOverlay.updateSelectionAmount(selected.size) && selected.isNotEmpty()) {
imm.hide()
}
}
private fun InputMethodManager.hide() {
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
}

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio.ui.selection
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.MenuItem
import android.widget.FrameLayout
import androidx.annotation.AttrRes
import androidx.appcompat.widget.Toolbar
@ -37,11 +38,29 @@ class SelectionToolbarOverlay
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) {
var callback: Callback? = null
private lateinit var innerToolbar: MaterialToolbar
private val selectionToolbar =
MaterialToolbar(context).apply {
inflateMenu(R.menu.menu_selection_actions)
setNavigationIcon(R.drawable.ic_close_24)
setNavigationOnClickListener {
callback?.onClearSelection()
}
inflateMenu(R.menu.menu_selection_actions)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_play_next -> {
callback?.onPlaySelectionNext()
}
R.id.action_queue_add -> {
callback?.onAddSelectionToQueue()
}
}
true
}
}
private var fadeThroughAnimator: ValueAnimator? = null
@ -56,23 +75,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
addView(selectionToolbar)
}
/** Add listeners for the selection toolbar. */
fun registerListeners(
onExit: OnClickListener,
onMenuItemClick: Toolbar.OnMenuItemClickListener
) {
selectionToolbar.apply {
setNavigationOnClickListener(onExit)
setOnMenuItemClickListener(onMenuItemClick)
}
}
/** Unregister listeners for this instance. */
fun unregisterListeners() {
selectionToolbar.apply {
setNavigationOnClickListener(null)
setOnMenuItemClickListener(null)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
callback = null
}
/**
@ -146,4 +151,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
isInvisible = innerAlpha == 1f
}
}
interface Callback {
fun onClearSelection()
fun onPlaySelectionNext()
fun onAddSelectionToQueue()
}
}

View file

@ -12,37 +12,44 @@
app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/search_recycler">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/search_toolbar"
<org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
android:id="@+id/search_toolbar_overlay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/menu_search"
app:navigationIcon="@drawable/ic_back_24">
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/search_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:endIconContentDescription="@string/desc_clear_search"
app:endIconDrawable="@drawable/ic_close_24"
app:endIconMode="clear_text"
app:endIconTint="?attr/colorControlNormal"
app:errorEnabled="false"
app:hintEnabled="false">
app:menu="@menu/menu_search"
app:navigationIcon="@drawable/ic_back_24">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/search_edit_text"
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="@string/lng_search_library"
android:imeOptions="actionSearch|flagNoExtractUi"
android:inputType="textFilter"
android:paddingStart="@dimen/spacing_tiny"
android:paddingEnd="0dp" />
app:endIconContentDescription="@string/desc_clear_search"
app:endIconDrawable="@drawable/ic_close_24"
app:endIconMode="clear_text"
app:endIconTint="?attr/colorControlNormal"
app:errorEnabled="false"
app:hintEnabled="false">
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/search_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="@string/lng_search_library"
android:imeOptions="actionSearch|flagNoExtractUi"
android:inputType="textFilter"
android:paddingStart="@dimen/spacing_tiny"
android:paddingEnd="0dp" />
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.textfield.TextInputLayout>
</com.google.android.material.appbar.MaterialToolbar>
</org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay>
</org.oxycblt.auxio.ui.AuxioAppBarLayout>

View file

@ -2,12 +2,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_play_next_selection"
android:id="@+id/action_play_next"
android:title="@string/lbl_play_next"
android:icon="@drawable/ic_play_next"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_queue_add_selection"
android:id="@+id/action_queue_add"
android:icon="@drawable/ic_play_next"
android:title="@string/lbl_queue_add"
app:showAsAction="never" />