detail: show name when scrolling

Show the name of the currently shown detail item when scrolling. This
is just UI candy that I've always wanted to add but couldn't due to
CollapsingToolbarLayout being a mess. This addition circumvents that
by simply doing some reflection magic and hooking the alpha of the
toolbar title to the current scroll state, solving the issue.
This commit is contained in:
OxygenCobalt 2021-11-08 19:54:38 -07:00
parent 1b79eb11e0
commit c5fcc45ee9
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
9 changed files with 146 additions and 6 deletions

View file

@ -64,7 +64,7 @@ class AlbumDetailFragment : DetailFragment() {
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
setupToolbar(R.menu.menu_album_detail) { itemId -> setupToolbar(detailModel.curAlbum.value!!, R.menu.menu_album_detail) { itemId ->
if (itemId == R.id.action_queue_add) { if (itemId == R.id.action_queue_add) {
playbackModel.addToUserQueue(detailModel.curAlbum.value!!) playbackModel.addToUserQueue(detailModel.curAlbum.value!!)
requireContext().showToast(R.string.lbl_queue_added) requireContext().showToast(R.string.lbl_queue_added)

View file

@ -73,7 +73,7 @@ class ArtistDetailFragment : DetailFragment() {
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
setupToolbar() setupToolbar(detailModel.curArtist.value!!)
setupRecycler(detailAdapter) { pos -> setupRecycler(detailAdapter) { pos ->
// If the item is an ActionHeader we need to also make the item full-width // If the item is an ActionHeader we need to also make the item full-width
val item = detailAdapter.currentList[pos] val item = detailAdapter.currentList[pos]

View file

@ -0,0 +1,133 @@
package org.oxycblt.auxio.detail
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.EdgeAppBarLayout
/**
* An [EdgeAppBarLayout] variant that also shows the name of the toolbar whenever the detail
* recyclerview is scrolled beyond it's first item (a.k.a the header). This is used instead of
* CollapsingToolbarLayout since that thing is a mess with crippling bugs and state issues.
* This just works.
*/
class DetailAppBarLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@StyleRes defStyleAttr: Int = -1
) : EdgeAppBarLayout(context, attrs, defStyleAttr) {
private var mTitleView: AppCompatTextView? = null
private var mRecycler: RecyclerView? = null
private var titleShown: Boolean? = null
private var mTitleAnimator: ValueAnimator? = null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
(layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context)
}
private fun findTitleView(): AppCompatTextView {
val titleView = mTitleView
if (titleView != null) {
return titleView
}
val toolbar = findViewById<Toolbar>(R.id.detail_toolbar)
val newTitleView = Toolbar::class.java.getDeclaredField("mTitleTextView").run {
isAccessible = true
get(toolbar) as AppCompatTextView
}
newTitleView.alpha = 0f
mTitleView = newTitleView
return newTitleView
}
private fun findRecyclerView(): RecyclerView {
val recycler = mRecycler
if (recycler != null) {
return recycler
}
val newRecycler = (parent as ViewGroup).findViewById<RecyclerView>(R.id.detail_recycler)
mRecycler = newRecycler
return newRecycler
}
private fun setTitleVisibility(visible: Boolean) {
if (titleShown == visible) return
titleShown = visible
if (mTitleAnimator != null) {
mTitleAnimator!!.cancel()
mTitleAnimator = null
}
val titleView = findTitleView()
val from: Float
val to: Float
if (visible) {
from = 0f
to = 1f
} else {
from = 1f
to = 0f
}
if (titleView.alpha == to) return
mTitleAnimator = ValueAnimator.ofFloat(from, to).apply {
addUpdateListener {
titleView.alpha = it.animatedValue as Float
}
duration = resources.getInteger(R.integer.app_bar_elevation_anim_duration).toLong()
start()
}
}
class Behavior @JvmOverloads constructor(
context: Context? = null,
attrs: AttributeSet? = null
) : AppBarLayout.Behavior(context, attrs) {
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: AppBarLayout,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int
) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
val appBar = child as DetailAppBarLayout
val recycler = appBar.findRecyclerView()
val showTitle = (recycler.layoutManager as LinearLayoutManager)
.findFirstVisibleItemPosition() > 0
appBar.setTitleVisibility(showTitle)
}
}
}

View file

@ -31,6 +31,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.SortMode import org.oxycblt.auxio.ui.SortMode
import org.oxycblt.auxio.ui.memberBinding import org.oxycblt.auxio.ui.memberBinding
@ -70,14 +71,18 @@ abstract class DetailFragment : Fragment() {
/** /**
* Shortcut method for doing setup of the detail toolbar. * Shortcut method for doing setup of the detail toolbar.
* @param music Music data to use as the toolbar title
* @param menu Menu resource to use * @param menu Menu resource to use
* @param onMenuClick (Optional) a click listener for that menu * @param onMenuClick (Optional) a click listener for that menu
*/ */
protected fun setupToolbar( protected fun setupToolbar(
data: Music,
@MenuRes menu: Int = -1, @MenuRes menu: Int = -1,
onMenuClick: ((itemId: Int) -> Boolean)? = null onMenuClick: ((itemId: Int) -> Boolean)? = null
) { ) {
binding.detailToolbar.apply { binding.detailToolbar.apply {
title = data.name
if (menu != -1) { if (menu != -1) {
inflateMenu(menu) inflateMenu(menu)
} }

View file

@ -64,7 +64,7 @@ class GenreDetailFragment : DetailFragment() {
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
setupToolbar() setupToolbar(detailModel.curGenre.value!!)
setupRecycler(detailAdapter) { pos -> setupRecycler(detailAdapter) { pos ->
val item = detailAdapter.currentList[pos] val item = detailAdapter.currentList[pos]
item is Header || item is ActionHeader || item is Genre item is Header || item is ActionHeader || item is Genre

View file

@ -47,6 +47,7 @@ import kotlin.math.sqrt
* - Use modified Auxio resources instead of AFS resources * - Use modified Auxio resources instead of AFS resources
* - Variable names are no longer prefixed with m * - Variable names are no longer prefixed with m
* - Made path management compat-friendly * - Made path management compat-friendly
* - Converted to kotlin
*/ */
class FastScrollPopupDrawable(context: Context) : Drawable() { class FastScrollPopupDrawable(context: Context) : Drawable() {
private val paint: Paint = Paint().apply { private val paint: Paint = Paint().apply {

View file

@ -38,6 +38,7 @@ import org.oxycblt.auxio.util.systemBarsCompat
* this class was primarily written by me. * this class was primarily written by me.
* *
* TODO: Add a swipe-up behavior a la Phonograph. I think that would improve UX. * TODO: Add a swipe-up behavior a la Phonograph. I think that would improve UX.
* - We need to use a separate drag helper to prevent issues
* TODO: Leverage this layout to make more tablet-friendly UIs * TODO: Leverage this layout to make more tablet-friendly UIs
* *
* @author OxygenCobalt * @author OxygenCobalt

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.util.systemBarsCompat
* **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.
*/ */
class EdgeAppBarLayout @JvmOverloads constructor( open class EdgeAppBarLayout @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
@StyleRes defStyleAttr: Int = -1 @StyleRes defStyleAttr: Int = -1

View file

@ -13,7 +13,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.oxycblt.auxio.ui.EdgeAppBarLayout <org.oxycblt.auxio.detail.DetailAppBarLayout
android:id="@+id/detail_appbar" android:id="@+id/detail_appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -25,7 +25,7 @@
android:id="@+id/detail_toolbar" android:id="@+id/detail_toolbar"
style="@style/Widget.Auxio.Toolbar.Icon" /> style="@style/Widget.Auxio.Toolbar.Icon" />
</org.oxycblt.auxio.ui.EdgeAppBarLayout> </org.oxycblt.auxio.detail.DetailAppBarLayout>
<org.oxycblt.auxio.ui.EdgeRecyclerView <org.oxycblt.auxio.ui.EdgeRecyclerView
android:id="@+id/detail_recycler" android:id="@+id/detail_recycler"