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.
This commit is contained in:
OxygenCobalt 2021-09-03 18:01:28 -06:00
parent 624eb57e7a
commit 74d55ba59e
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 106 additions and 30 deletions

View file

@ -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) {

View file

@ -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<out List<BaseModel>>
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!!)

View file

@ -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
}
}

View file

@ -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">
<androidx.appcompat.widget.Toolbar
android:id="@+id/detail_toolbar"

View file

@ -11,15 +11,15 @@
android:animateLayoutChanges="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
<org.oxycblt.auxio.ui.LiftAppBarLayout
android:id="@+id/home_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:clickable="true"
android:clipChildren="true"
app:liftOnScroll="true"
android:focusable="true">
android:focusable="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/home_toolbar"
@ -32,16 +32,16 @@
android:id="@+id/home_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:tabContentStart="@dimen/spacing_medium"
app:tabIndicatorColor="?attr/colorAccent"
app:tabMode="scrollable"
app:tabRippleColor="?attr/colorControlHighlight"
android:background="@android:color/transparent"
app:tabTextAppearance="@style/TextAppearance.TabLayout.Label"
app:tabTextColor="?android:attr/textColorPrimary"
app:tabUnboundedRipple="true" />
</com.google.android.material.appbar.AppBarLayout>
</org.oxycblt.auxio.ui.LiftAppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/home_pager"

View file

@ -17,7 +17,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:liftOnScroll="true">
app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/queue_recycler">
<androidx.appcompat.widget.Toolbar
android:id="@+id/queue_toolbar"

View file

@ -12,7 +12,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:liftOnScroll="true">
app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/search_recycler">
<androidx.appcompat.widget.Toolbar
android:id="@+id/search_toolbar"

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- This is for HomeFragment's AppBarLayout. Explanations for these can be found there. -->
<item name="home_song_list" type="id" />
<item name="home_album_list" type="id" />
<item name="home_artist_list" type="id" />
<item name="home_genre_list" type="id" />
</resources>