From c353ffd705339a7f37a55bcc48b94062b8fb5ac6 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 17 Dec 2022 13:29:13 -0700 Subject: [PATCH] home: expand appbar when selection starts Expand the home AppBarLayout when a selection begins (Excluding initialization) --- .../auxio/detail/AlbumDetailFragment.kt | 6 ++- .../auxio/detail/ArtistDetailFragment.kt | 6 ++- .../auxio/detail/GenreDetailFragment.kt | 6 ++- .../org/oxycblt/auxio/home/HomeFragment.kt | 33 ++++++++++---- .../auxio/home/list/AlbumListFragment.kt | 2 +- .../auxio/home/list/ArtistListFragment.kt | 2 +- .../auxio/home/list/GenreListFragment.kt | 2 +- .../auxio/home/list/HomeListFragment.kt | 2 +- .../auxio/home/list/SongListFragment.kt | 2 +- .../playback/state/PlaybackStateManager.kt | 3 ++ .../org/oxycblt/auxio/ui/AuxioAppBarLayout.kt | 44 ++++++++++++++++++- .../ui/fastscroll/FastScrollRecyclerView.kt | 1 - .../SelectionToolbarOverlay.kt | 13 ++---- .../ui/{ => selection}/SelectionViewModel.kt | 11 ++++- .../{ic_play_next_24.xml => ic_play_next.xml} | 0 app/src/main/res/layout/fragment_home.xml | 4 +- .../main/res/menu/menu_selection_actions.xml | 3 +- app/src/main/res/values/attrs.xml | 8 ++-- 18 files changed, 111 insertions(+), 37 deletions(-) rename app/src/main/java/org/oxycblt/auxio/ui/{ => selection}/SelectionToolbarOverlay.kt (93%) rename app/src/main/java/org/oxycblt/auxio/ui/{ => selection}/SelectionViewModel.kt (88%) rename app/src/main/res/drawable/{ic_play_next_24.xml => ic_play_next.xml} (100%) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 2dba17668..b851b697b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -96,7 +96,11 @@ class AlbumDetailFragment : override fun onDestroyBinding(binding: FragmentDetailBinding) { super.onDestroyBinding(binding) - binding.detailToolbar.setOnMenuItemClickListener(null) + binding.detailToolbar.apply { + setNavigationOnClickListener(null) + setOnMenuItemClickListener(null) + } + binding.detailRecycler.adapter = null } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index b40b69ab1..3b20d74f6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -91,7 +91,11 @@ class ArtistDetailFragment : override fun onDestroyBinding(binding: FragmentDetailBinding) { super.onDestroyBinding(binding) - binding.detailToolbar.setOnMenuItemClickListener(null) + binding.detailToolbar.apply { + setNavigationOnClickListener(null) + setOnMenuItemClickListener(null) + } + binding.detailRecycler.adapter = null } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index c3018be04..04166535d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -92,7 +92,11 @@ class GenreDetailFragment : override fun onDestroyBinding(binding: FragmentDetailBinding) { super.onDestroyBinding(binding) - binding.detailToolbar.setOnMenuItemClickListener(null) + binding.detailToolbar.apply { + setNavigationOnClickListener(null) + setOnMenuItemClickListener(null) + } + binding.detailRecycler.adapter = null } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 44e8604a7..99ade8dba 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -29,6 +29,7 @@ import androidx.core.view.iterator import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.RecyclerView 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.ui.MainNavigationAction 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.util.* @@ -69,10 +70,11 @@ import org.oxycblt.auxio.util.* */ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuItemClickListener { private val playbackModel: PlaybackViewModel by androidActivityViewModels() - private val navModel: NavigationViewModel by activityViewModels() private val homeModel: HomeViewModel by androidActivityViewModels() - private val selectionModel: SelectionViewModel 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. private val storagePermissionLauncher: ActivityResultLauncher by lifecycleObject { @@ -237,15 +239,12 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI private fun updateCurrentTab(tab: MusicMode) { // Make sure that we update the scrolling view and allowed menu items whenever // the tab changes. - val binding = requireBinding() when (tab) { MusicMode.SONGS -> { updateSortMenu(tab) { id -> id != R.id.option_sort_count } - binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_song_list } MusicMode.ALBUMS -> { updateSortMenu(tab) { id -> id != R.id.option_sort_album } - binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_album_list } MusicMode.ARTISTS -> { updateSortMenu(tab) { id -> @@ -254,7 +253,6 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI id == R.id.option_sort_count || id == R.id.option_sort_duration } - binding.homeAppbar.liftOnScrollTargetViewId = R.id.home_artist_list } MusicMode.GENRES -> { updateSortMenu(tab) { id -> @@ -263,9 +261,10 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI id == R.id.option_sort_count || 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) { @@ -402,7 +401,15 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI } private fun updateSelection(selected: List) { - 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?) { @@ -449,6 +456,14 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI 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 : FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index cf5808cb3..304b3f4b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -51,7 +51,7 @@ class AlbumListFragment : HomeListFragment() { super.onBindingCreated(binding, savedInstanceState) binding.homeRecycler.apply { - id = R.id.home_album_list + id = R.id.home_album_recycler adapter = homeAdapter } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index 8381fc7f8..0e68f83ae 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -47,7 +47,7 @@ class ArtistListFragment : HomeListFragment() { super.onBindingCreated(binding, savedInstanceState) binding.homeRecycler.apply { - id = R.id.home_artist_list + id = R.id.home_artist_recycler adapter = homeAdapter } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 9812ddf45..28d323dfb 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -46,7 +46,7 @@ class GenreListFragment : HomeListFragment() { super.onBindingCreated(binding, savedInstanceState) binding.homeRecycler.apply { - id = R.id.home_genre_list + id = R.id.home_genre_recycler adapter = homeAdapter } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt index 9f2892eb0..ef796ab89 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt @@ -24,7 +24,7 @@ import androidx.fragment.app.activityViewModels import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeViewModel 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.fragment.MenuFragment import org.oxycblt.auxio.ui.recycler.Item diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 4ebb2102b..39248e546 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -54,7 +54,7 @@ class SongListFragment : HomeListFragment() { super.onBindingCreated(binding, savedInstanceState) binding.homeRecycler.apply { - id = R.id.home_song_list + id = R.id.home_song_recycler adapter = homeAdapter } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 716894e50..d11b67525 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -65,6 +65,9 @@ class PlaybackStateManager private constructor() { private set private var _queue = mutableListOf() + private val orderedQueue = listOf() + private val shuffledQueue = listOf() + /** The current queue determined by [parent] */ val queue get() = _queue diff --git a/app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt index 9916197c1..4cb65e05b 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt @@ -22,15 +22,18 @@ import android.util.AttributeSet import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver +import android.view.animation.AnimationUtils import androidx.annotation.AttrRes import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout 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 - * not properly respond to RecyclerView events. + * An [AppBarLayout] that fixes several bugs with the default implementation where the lifted + * state will not properly respond to RecyclerView events. * * **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. @@ -60,6 +63,17 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr 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() { super.onDetachedFromWindow() viewTreeObserver.removeOnPreDrawListener(onPreDraw) @@ -88,4 +102,30 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr 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) + } + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollRecyclerView.kt index b26f16296..511431d3e 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/fastscroll/FastScrollRecyclerView.kt @@ -463,7 +463,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } private fun animateViewIn(view: View) { - logD(view.translationX) view .animate() .alpha(1f) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/SelectionToolbarOverlay.kt b/app/src/main/java/org/oxycblt/auxio/ui/selection/SelectionToolbarOverlay.kt similarity index 93% rename from app/src/main/java/org/oxycblt/auxio/ui/SelectionToolbarOverlay.kt rename to app/src/main/java/org/oxycblt/auxio/ui/selection/SelectionToolbarOverlay.kt index b650ec4bb..0a04bc8c8 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/SelectionToolbarOverlay.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/selection/SelectionToolbarOverlay.kt @@ -1,20 +1,15 @@ -package org.oxycblt.auxio.ui +package org.oxycblt.auxio.ui.selection import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet -import android.view.View import android.widget.FrameLayout import androidx.annotation.AttrRes import androidx.appcompat.widget.Toolbar import androidx.core.view.isInvisible -import androidx.transition.TransitionManager import com.google.android.material.appbar.MaterialToolbar -import com.google.android.material.transition.MaterialFadeThrough import org.oxycblt.auxio.R 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 @@ -31,8 +26,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr setNavigationIcon(R.drawable.ic_close_24) } - private val selectionMenu = selectionToolbar.menu - private var fadeThroughAnimator: ValueAnimator? = null override fun onFinishInflate() { @@ -103,8 +96,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } 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) - return true + return false } if (fadeThroughAnimator != null) { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/ui/selection/SelectionViewModel.kt similarity index 88% rename from app/src/main/java/org/oxycblt/auxio/ui/SelectionViewModel.kt rename to app/src/main/java/org/oxycblt/auxio/ui/selection/SelectionViewModel.kt index e276a2411..772755db8 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/selection/SelectionViewModel.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.ui +package org.oxycblt.auxio.ui.selection import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -23,6 +23,10 @@ import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.util.logD +/** + * ViewModel that manages the current selection. + * @author OxygenCobalt + */ class SelectionViewModel : ViewModel() { private val _selected = MutableStateFlow(listOf()) val selected: StateFlow> @@ -47,4 +51,9 @@ class SelectionViewModel : ViewModel() { _selected.value = listOf() } } + + override fun onCleared() { + super.onCleared() + logD("Cleared") + } } diff --git a/app/src/main/res/drawable/ic_play_next_24.xml b/app/src/main/res/drawable/ic_play_next.xml similarity index 100% rename from app/src/main/res/drawable/ic_play_next_24.xml rename to app/src/main/res/drawable/ic_play_next.xml diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 2b2ca753b..5c8d40436 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -13,7 +13,7 @@ style="@style/Widget.Auxio.AppBarLayout" android:fitsSystemWindows="true"> - @@ -26,7 +26,7 @@ app:menu="@menu/menu_home" app:title="@string/info_app_name" /> - + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 27000bdf6..3b43e79b2 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,10 +1,10 @@ - - - - + + + + 200 100