From 74d55ba59e98679e0e1e17b1e1e2a812841d7a4f Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Fri, 3 Sep 2021 18:01:28 -0600 Subject: [PATCH] home: make appbar liftOnScroll Make HomeFragment's AppBarLayout lift when the data scrolls. This was something I wanted to do initially, but kept running into issues with. Turns out the addition of my custom AppBarLayout made this pretty trivial all things considered. The entire app now follows this idiom. --- .../org/oxycblt/auxio/home/HomeFragment.kt | 22 ++++++++- .../oxycblt/auxio/home/HomeListFragment.kt | 42 +++++++++++++---- .../org/oxycblt/auxio/ui/LiftAppBarLayout.kt | 45 ++++++++++++++----- app/src/main/res/layout/fragment_detail.xml | 3 +- app/src/main/res/layout/fragment_home.xml | 10 ++--- app/src/main/res/layout/fragment_queue.xml | 3 +- app/src/main/res/layout/fragment_search.xml | 3 +- app/src/main/res/values/ids.xml | 8 ++++ 8 files changed, 106 insertions(+), 30 deletions(-) create mode 100644 app/src/main/res/values/ids.xml 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 ac7db0274..2e52cfd39 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -116,6 +116,26 @@ class HomeFragment : Fragment() { logE("Unable to reduce ViewPager sensitivity") logE(e.stackTraceToString()) } + + // We know that there will only be a fixed amount of tabs, so we manually set this + // limit to that. This also prevents the appbar lift state from being confused during + // page transitions. + offscreenPageLimit = homeModel.tabs.value!!.size + + // ViewPager2 tends to garble any scrolling view events that occur within it's + // fragments, so we fix that by instructing our AppBarLayout to follow the specific + // view we have just selected. + registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + binding.homeAppbar.liftOnScrollTargetViewId = + when (homeModel.tabs.value!![position]) { + 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 + } + } + }) } TabLayoutMediator(binding.homeTabs, binding.homePager) { tab, pos -> @@ -132,7 +152,7 @@ class HomeFragment : Fragment() { // --- VIEWMODEL SETUP --- detailModel.navToItem.observe(viewLifecycleOwner) { item -> - // The AppBarLayout bugs out and collapses when we navigate too fast, wait for it + // The AppBarLayout gets confused and collapses when we navigate too fast, wait for it // to draw before we continue. binding.homeAppbar.post { when (item) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeListFragment.kt index ed43011a0..4218f499a 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeListFragment.kt @@ -22,13 +22,17 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.IdRes import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.LiveData import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.BuildConfig +import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel @@ -77,9 +81,38 @@ class HomeListFragment : Fragment() { ::newMenu ) + // --- ITEM SETUP --- + + // Get some tab-specific values before we go ahead. More specifically, the data to use + // and the unique ID that HomeFragment's AppBarLayout uses to determine lift state. + val pos = requireNotNull(arguments).getInt(ARG_POS) + + @IdRes val customId: Int + val toObserve: LiveData> + + when (requireNotNull(homeModel.tabs.value)[pos]) { + DisplayMode.SHOW_SONGS -> { + customId = R.id.home_song_list + toObserve = homeModel.songs + } + DisplayMode.SHOW_ALBUMS -> { + customId = R.id.home_album_list + toObserve = homeModel.albums + } + DisplayMode.SHOW_ARTISTS -> { + customId = R.id.home_artist_list + toObserve = homeModel.artists + } + DisplayMode.SHOW_GENRES -> { + customId = R.id.home_genre_list + toObserve = homeModel.genres + } + } + // --- UI SETUP --- binding.homeRecycler.apply { + id = customId adapter = homeAdapter setHasFixedSize(true) applySpans() @@ -87,15 +120,6 @@ class HomeListFragment : Fragment() { // --- VIEWMODEL SETUP --- - val pos = requireNotNull(arguments).getInt(ARG_POS) - - val toObserve = when (requireNotNull(homeModel.tabs.value)[pos]) { - DisplayMode.SHOW_SONGS -> homeModel.songs - DisplayMode.SHOW_ALBUMS -> homeModel.albums - DisplayMode.SHOW_ARTISTS -> homeModel.artists - DisplayMode.SHOW_GENRES -> homeModel.genres - } - // Make sure that this RecyclerView has data before startup homeAdapter.updateData(toObserve.value!!) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/LiftAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/LiftAppBarLayout.kt index c5a6c9890..6c737a545 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/LiftAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/LiftAppBarLayout.kt @@ -20,17 +20,22 @@ package org.oxycblt.auxio.ui import android.content.Context import android.util.AttributeSet +import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import androidx.annotation.StyleRes import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.res.ResourcesCompat import androidx.core.view.children import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout +import org.oxycblt.auxio.util.logE /** * An [AppBarLayout] that fixes a bug 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. * FIXME: Fix issue where elevation change will always animate * FIXME: Fix issue where expanded state does not work correctly when switching orientations */ @@ -39,15 +44,17 @@ class LiftAppBarLayout @JvmOverloads constructor( attrs: AttributeSet? = null, @StyleRes defStyleAttr: Int = -1 ) : AppBarLayout(context, attrs, defStyleAttr) { - private var recycler: RecyclerView? = null + private var scrollingChild: View? = null private val tConsumed = IntArray(2) private val onPreDraw = ViewTreeObserver.OnPreDrawListener { - recycler?.let { rec -> - val coordinator = (parent as CoordinatorLayout) + val child = findScrollingChild() + + if (child != null) { + val coordinator = parent as CoordinatorLayout (layoutParams as CoordinatorLayout.LayoutParams).behavior?.onNestedPreScroll( - coordinator, this, rec, 0, 0, tConsumed, 0 + coordinator, this, coordinator, 0, 0, tConsumed, 0 ) } @@ -58,17 +65,31 @@ class LiftAppBarLayout @JvmOverloads constructor( viewTreeObserver.addOnPreDrawListener(onPreDraw) } - override fun onAttachedToWindow() { - super.onAttachedToWindow() - - // Assume there is one RecyclerView [Because there is] - recycler = (parent as ViewGroup).children.firstOrNull { it is RecyclerView } - as RecyclerView? - } - override fun onDetachedFromWindow() { super.onDetachedFromWindow() viewTreeObserver.removeOnPreDrawListener(onPreDraw) } + + override fun setLiftOnScrollTargetViewId(liftOnScrollTargetViewId: Int) { + super.setLiftOnScrollTargetViewId(liftOnScrollTargetViewId) + + // Sometimes we dynamically set the scrolling child [such as in HomeFragment], so clear it + // and re-draw when that occurs. + scrollingChild = null + onPreDraw.onPreDraw() + } + + private fun findScrollingChild(): View? { + // Roll some custom code for finding our scrolling view. This can be anything as long as + // it updates this layout in it's onNestedPreScroll call. + if (scrollingChild == null) { + if (liftOnScrollTargetViewId != ResourcesCompat.ID_NULL) { + scrollingChild = (parent as ViewGroup).findViewById(liftOnScrollTargetViewId) + } else { + logE("liftOnScrollTargetViewId was not specified. ignoring scroll events.") + } + } + return scrollingChild + } } diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index 684e89073..0b109667b 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -13,7 +13,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/colorSurface" - app:liftOnScroll="true"> + app:liftOnScroll="true" + app:liftOnScrollTargetViewId="@id/detail_recycler"> - + android:focusable="true" + app:liftOnScroll="true"> - + + app:liftOnScroll="true" + app:liftOnScrollTargetViewId="@id/queue_recycler"> + app:liftOnScroll="true" + app:liftOnScrollTargetViewId="@id/search_recycler"> + + + + + + + \ No newline at end of file