home: expand appbar when selection starts

Expand the home AppBarLayout when a selection begins (Excluding
initialization)
This commit is contained in:
Alexander Capehart 2022-12-17 13:29:13 -07:00
parent f3365fc40b
commit c353ffd705
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
18 changed files with 111 additions and 37 deletions

View file

@ -96,7 +96,11 @@ class AlbumDetailFragment :
override fun onDestroyBinding(binding: FragmentDetailBinding) { override fun onDestroyBinding(binding: FragmentDetailBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
binding.detailToolbar.setOnMenuItemClickListener(null) binding.detailToolbar.apply {
setNavigationOnClickListener(null)
setOnMenuItemClickListener(null)
}
binding.detailRecycler.adapter = null binding.detailRecycler.adapter = null
} }

View file

@ -91,7 +91,11 @@ class ArtistDetailFragment :
override fun onDestroyBinding(binding: FragmentDetailBinding) { override fun onDestroyBinding(binding: FragmentDetailBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
binding.detailToolbar.setOnMenuItemClickListener(null) binding.detailToolbar.apply {
setNavigationOnClickListener(null)
setOnMenuItemClickListener(null)
}
binding.detailRecycler.adapter = null binding.detailRecycler.adapter = null
} }

View file

@ -92,7 +92,11 @@ class GenreDetailFragment :
override fun onDestroyBinding(binding: FragmentDetailBinding) { override fun onDestroyBinding(binding: FragmentDetailBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
binding.detailToolbar.setOnMenuItemClickListener(null) binding.detailToolbar.apply {
setNavigationOnClickListener(null)
setOnMenuItemClickListener(null)
}
binding.detailRecycler.adapter = null binding.detailRecycler.adapter = null
} }

View file

@ -29,6 +29,7 @@ import androidx.core.view.iterator
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
@ -58,7 +59,7 @@ import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.SelectionViewModel import org.oxycblt.auxio.ui.selection.SelectionViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
@ -69,10 +70,11 @@ import org.oxycblt.auxio.util.*
*/ */
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener { class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener {
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
private val homeModel: HomeViewModel by androidActivityViewModels() private val homeModel: HomeViewModel by androidActivityViewModels()
private val selectionModel: SelectionViewModel by activityViewModels()
private val musicModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
// Makes no sense to share selections across screens
private val selectionModel: SelectionViewModel by activityViewModels()
// lifecycleObject builds this in the creation step, so doing this is okay. // lifecycleObject builds this in the creation step, so doing this is okay.
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject { private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject {
@ -237,15 +239,12 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private fun updateCurrentTab(tab: MusicMode) { private fun updateCurrentTab(tab: MusicMode) {
// Make sure that we update the scrolling view and allowed menu items whenever // Make sure that we update the scrolling view and allowed menu items whenever
// the tab changes. // the tab changes.
val binding = requireBinding()
when (tab) { when (tab) {
MusicMode.SONGS -> { MusicMode.SONGS -> {
updateSortMenu(tab) { id -> id != R.id.option_sort_count } updateSortMenu(tab) { id -> id != R.id.option_sort_count }
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_song_list
} }
MusicMode.ALBUMS -> { MusicMode.ALBUMS -> {
updateSortMenu(tab) { id -> id != R.id.option_sort_album } updateSortMenu(tab) { id -> id != R.id.option_sort_album }
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_album_list
} }
MusicMode.ARTISTS -> { MusicMode.ARTISTS -> {
updateSortMenu(tab) { id -> updateSortMenu(tab) { id ->
@ -254,7 +253,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
id == R.id.option_sort_count || id == R.id.option_sort_count ||
id == R.id.option_sort_duration id == R.id.option_sort_duration
} }
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_artist_list
} }
MusicMode.GENRES -> { MusicMode.GENRES -> {
updateSortMenu(tab) { id -> updateSortMenu(tab) { id ->
@ -263,9 +261,10 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
id == R.id.option_sort_count || id == R.id.option_sort_count ||
id == R.id.option_sort_duration id == R.id.option_sort_duration
} }
binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_genre_list
} }
} }
requireBinding().homeAppbar.liftOnScrollTargetViewId = getRecyclerId(tab)
} }
private fun updateSortMenu(mode: MusicMode, isVisible: (Int) -> Boolean) { private fun updateSortMenu(mode: MusicMode, isVisible: (Int) -> Boolean) {
@ -402,7 +401,15 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
private fun updateSelection(selected: List<Music>) { private fun updateSelection(selected: List<Music>) {
requireBinding().homeToolbarOverlay.updateSelectionAmount(selected.size) val binding = requireBinding()
if (binding.homeToolbarOverlay.updateSelectionAmount(selected.size) && selected.isNotEmpty()) {
logD("Significant selection occurred, expanding AppBar")
// Significant enough change where we want to expand the RecyclerView
binding.homeAppbar.expandWithRecycler(
binding.homePager.findViewById(
getRecyclerId(homeModel.currentTab.value))
)
}
} }
private fun handleNavigation(item: Music?) { private fun handleNavigation(item: Music?) {
@ -449,6 +456,14 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
adapter = HomePagerAdapter() adapter = HomePagerAdapter()
} }
private fun getRecyclerId(tab: MusicMode) =
when (tab) {
MusicMode.SONGS -> R.id.home_song_recycler
MusicMode.ALBUMS -> R.id.home_album_recycler
MusicMode.ARTISTS -> R.id.home_artist_recycler
MusicMode.GENRES -> R.id.home_genre_recycler
}
private inner class HomePagerAdapter : private inner class HomePagerAdapter :
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) { FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {

View file

@ -51,7 +51,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply { binding.homeRecycler.apply {
id = R.id.home_album_list id = R.id.home_album_recycler
adapter = homeAdapter adapter = homeAdapter
} }

View file

@ -47,7 +47,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply { binding.homeRecycler.apply {
id = R.id.home_artist_list id = R.id.home_artist_recycler
adapter = homeAdapter adapter = homeAdapter
} }

View file

@ -46,7 +46,7 @@ class GenreListFragment : HomeListFragment<Genre>() {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply { binding.homeRecycler.apply {
id = R.id.home_genre_list id = R.id.home_genre_recycler
adapter = homeAdapter adapter = homeAdapter
} }

View file

@ -24,7 +24,7 @@ import androidx.fragment.app.activityViewModels
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.Music
import org.oxycblt.auxio.ui.SelectionViewModel import org.oxycblt.auxio.ui.selection.SelectionViewModel
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

View file

@ -54,7 +54,7 @@ class SongListFragment : HomeListFragment<Song>() {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply { binding.homeRecycler.apply {
id = R.id.home_song_list id = R.id.home_song_recycler
adapter = homeAdapter adapter = homeAdapter
} }

View file

@ -65,6 +65,9 @@ class PlaybackStateManager private constructor() {
private set private set
private var _queue = mutableListOf<Song>() private var _queue = mutableListOf<Song>()
private val orderedQueue = listOf<Song>()
private val shuffledQueue = listOf<Song>()
/** The current queue determined by [parent] */ /** The current queue determined by [parent] */
val queue val queue
get() = _queue get() = _queue

View file

@ -22,15 +22,18 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.view.animation.AnimationUtils
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import org.oxycblt.auxio.util.coordinatorLayoutBehavior import org.oxycblt.auxio.util.coordinatorLayoutBehavior
import org.oxycblt.auxio.util.logD
/** /**
* An [AppBarLayout] that fixes a bug with the default implementation where the lifted state will * An [AppBarLayout] that fixes several bugs with the default implementation where the lifted
* not properly respond to RecyclerView events. * state will not properly respond to RecyclerView events.
* *
* **Note:** This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what * **Note:** This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what
* scrolling view to use. Failure to specify this will result in the layout not working. * scrolling view to use. Failure to specify this will result in the layout not working.
@ -60,6 +63,17 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
viewTreeObserver.addOnPreDrawListener(onPreDraw) viewTreeObserver.addOnPreDrawListener(onPreDraw)
} }
/**
* Expand this app bar layout with the given recyclerview, preventing it from
* jumping around.
*/
fun expandWithRecycler(recycler: RecyclerView?) {
setExpanded(true)
recycler?.let {
addOnOffsetChangedListener(ExpansionHackListener(it))
}
}
override fun onDetachedFromWindow() { override fun onDetachedFromWindow() {
super.onDetachedFromWindow() super.onDetachedFromWindow()
viewTreeObserver.removeOnPreDrawListener(onPreDraw) viewTreeObserver.removeOnPreDrawListener(onPreDraw)
@ -88,4 +102,30 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
return scrollingChild return scrollingChild
} }
/**
* Hack to prevent RecyclerView jumping when the appbar expands.
* Adapted from Material Files:
* https://github.com/zhanghai/MaterialFiles/blob/master/app/src/main/java/me/zhanghai/android/files/ui/AppBarLayoutExpandHackListener.kt
*/
private class ExpansionHackListener(private val recycler: RecyclerView) : OnOffsetChangedListener {
private val offsetAnimationMaxEndTime = (AnimationUtils.currentAnimationTimeMillis()
+ 600)
private var lastVerticalOffset: Int? = null
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
if (verticalOffset == 0
|| AnimationUtils.currentAnimationTimeMillis() > offsetAnimationMaxEndTime) {
// AppBarLayout crashes with IndexOutOfBoundsException when a non-last listener removes
// itself, so we have to do the removal asynchronously.
appBarLayout.postOnAnimation { appBarLayout.removeOnOffsetChangedListener(this) }
}
val lastVerticalOffset = lastVerticalOffset
this.lastVerticalOffset = verticalOffset
if (lastVerticalOffset != null) {
recycler.scrollBy(0, verticalOffset - lastVerticalOffset)
}
}
}
} }

View file

@ -463,7 +463,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
private fun animateViewIn(view: View) { private fun animateViewIn(view: View) {
logD(view.translationX)
view view
.animate() .animate()
.alpha(1f) .alpha(1f)

View file

@ -1,20 +1,15 @@
package org.oxycblt.auxio.ui package org.oxycblt.auxio.ui.selection
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.transition.TransitionManager
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.transition.MaterialFadeThrough
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import kotlin.math.max
import kotlin.math.min
/** /**
* A wrapper around a Toolbar that enables an overlaid toolbar showing information about * A wrapper around a Toolbar that enables an overlaid toolbar showing information about
@ -31,8 +26,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
setNavigationIcon(R.drawable.ic_close_24) setNavigationIcon(R.drawable.ic_close_24)
} }
private val selectionMenu = selectionToolbar.menu
private var fadeThroughAnimator: ValueAnimator? = null private var fadeThroughAnimator: ValueAnimator? = null
override fun onFinishInflate() { override fun onFinishInflate() {
@ -103,8 +96,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
} }
if (!isLaidOut) { if (!isLaidOut) {
// Not laid out, just change it immediately while are not shown to the user.
// This is an initialization, so we return false despite changing.
changeToolbarAlpha(targetInnerAlpha) changeToolbarAlpha(targetInnerAlpha)
return true return false
} }
if (fadeThroughAnimator != null) { if (fadeThroughAnimator != null) {

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.ui package org.oxycblt.auxio.ui.selection
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -23,6 +23,10 @@ import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/**
* ViewModel that manages the current selection.
* @author OxygenCobalt
*/
class SelectionViewModel : ViewModel() { class SelectionViewModel : ViewModel() {
private val _selected = MutableStateFlow(listOf<Music>()) private val _selected = MutableStateFlow(listOf<Music>())
val selected: StateFlow<List<Music>> val selected: StateFlow<List<Music>>
@ -47,4 +51,9 @@ class SelectionViewModel : ViewModel() {
_selected.value = listOf() _selected.value = listOf()
} }
} }
override fun onCleared() {
super.onCleared()
logD("Cleared")
}
} }

View file

@ -13,7 +13,7 @@
style="@style/Widget.Auxio.AppBarLayout" style="@style/Widget.Auxio.AppBarLayout"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<org.oxycblt.auxio.ui.SelectionToolbarOverlay <org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
android:id="@+id/home_toolbar_overlay" android:id="@+id/home_toolbar_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -26,7 +26,7 @@
app:menu="@menu/menu_home" app:menu="@menu/menu_home"
app:title="@string/info_app_name" /> app:title="@string/info_app_name" />
</org.oxycblt.auxio.ui.SelectionToolbarOverlay> </org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay>
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/home_tabs" android:id="@+id/home_tabs"

View file

@ -4,10 +4,11 @@
<item <item
android:id="@+id/action_play_next_selection" android:id="@+id/action_play_next_selection"
android:title="@string/lbl_play_next" android:title="@string/lbl_play_next"
android:icon="@drawable/ic_play_next_24" android:icon="@drawable/ic_play_next"
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/action_queue_add_selection" android:id="@+id/action_queue_add_selection"
android:icon="@drawable/ic_play_next"
android:title="@string/lbl_queue_add" android:title="@string/lbl_queue_add"
app:showAsAction="never" /> app:showAsAction="never" />
<!-- TOOD: Disabled until able to get queue system into shape --> <!-- TOOD: Disabled until able to get queue system into shape -->

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- This is for HomeFragment's AppBarLayout. Explanations for these can be found there. --> <!-- This is for HomeFragment's AppBarLayout. Explanations for these can be found there. -->
<item name="home_song_list" type="id" /> <item name="home_song_recycler" type="id" />
<item name="home_album_list" type="id" /> <item name="home_album_recycler" type="id" />
<item name="home_artist_list" type="id" /> <item name="home_artist_recycler" type="id" />
<item name="home_genre_list" type="id" /> <item name="home_genre_recycler" type="id" />
<integer name="anim_fade_enter_duration">200</integer> <integer name="anim_fade_enter_duration">200</integer>
<integer name="anim_fade_exit_duration">100</integer> <integer name="anim_fade_exit_duration">100</integer>