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:
parent
1b79eb11e0
commit
c5fcc45ee9
9 changed files with 146 additions and 6 deletions
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
133
app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt
Normal file
133
app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue