diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ContinuousAppBarLayoutBehavior.kt b/app/src/main/java/org/oxycblt/auxio/detail/ContinuousAppBarLayoutBehavior.kt
new file mode 100644
index 000000000..d2e074d8f
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/detail/ContinuousAppBarLayoutBehavior.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2022 Auxio Project
+ * ContinuousAppBarLayoutBehavior.kt is part of Auxio.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.oxycblt.auxio.detail
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.VelocityTracker
+import android.view.ViewGroup
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.appbar.AppBarLayout
+
+class ContinuousAppBarLayoutBehavior
+@JvmOverloads
+constructor(context: Context? = null, attrs: AttributeSet? = null) :
+ AppBarLayout.Behavior(context, attrs) {
+ private var recycler: RecyclerView? = null
+ private var pointerId = -1
+ private var velocityTracker: VelocityTracker? = null
+
+ override fun onInterceptTouchEvent(
+ parent: CoordinatorLayout,
+ child: AppBarLayout,
+ ev: MotionEvent
+ ): Boolean {
+ val consumed = super.onInterceptTouchEvent(parent, child, ev)
+ when (ev.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ ensureVelocityTracker()
+ findRecyclerView(child).stopScroll()
+ pointerId = ev.getPointerId(0)
+ }
+ MotionEvent.ACTION_CANCEL -> {
+ velocityTracker?.recycle()
+ velocityTracker = null
+ pointerId = -1
+ }
+ else -> {}
+ }
+ return consumed
+ }
+
+ override fun onTouchEvent(
+ parent: CoordinatorLayout,
+ child: AppBarLayout,
+ ev: MotionEvent
+ ): Boolean {
+ val consumed = super.onTouchEvent(parent, child, ev)
+ when (ev.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ ensureVelocityTracker()
+ pointerId = ev.getPointerId(0)
+ }
+ MotionEvent.ACTION_UP -> {
+ findRecyclerView(child).fling(0, getYVelocity(ev))
+ }
+ MotionEvent.ACTION_CANCEL -> {
+ velocityTracker?.recycle()
+ velocityTracker = null
+ pointerId = -1
+ }
+ else -> {}
+ }
+ velocityTracker?.addMovement(ev)
+ return consumed
+ }
+
+ private fun ensureVelocityTracker() {
+ if (velocityTracker == null) {
+ velocityTracker = VelocityTracker.obtain()
+ }
+ }
+
+ private fun getYVelocity(event: MotionEvent): Int {
+ velocityTracker?.let {
+ it.addMovement(event)
+ it.computeCurrentVelocity(FLING_UNITS)
+ return -it.getYVelocity(pointerId).toInt()
+ }
+ return 0
+ }
+
+ private fun findRecyclerView(child: AppBarLayout): RecyclerView {
+ val recycler = recycler
+ if (recycler != null) {
+ return recycler
+ }
+
+ // Use the scrolling view in order to find a RecyclerView to use.
+ val newRecycler =
+ (child.parent as ViewGroup).findViewById(child.liftOnScrollTargetViewId)
+ this.recycler = newRecycler
+ return newRecycler
+ }
+
+ companion object {
+ private const val FLING_UNITS = 1000 // copied from base class
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt
deleted file mode 100644
index 3c494cd96..000000000
--- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (c) 2022 Auxio Project
- * DetailAppBarLayout.kt is part of Auxio.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-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 android.widget.TextView
-import androidx.annotation.AttrRes
-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 java.lang.reflect.Field
-import org.oxycblt.auxio.R
-import org.oxycblt.auxio.ui.CoordinatorAppBarLayout
-import org.oxycblt.auxio.util.getInteger
-import org.oxycblt.auxio.util.lazyReflectedField
-import org.oxycblt.auxio.util.logD
-
-/**
- * An [CoordinatorAppBarLayout] that displays the title of a hidden [Toolbar] when the scrolling
- * view goes beyond it's first item.
- *
- * This is intended for the detail views, in which the first item is the album/artist/genre header,
- * and thus scrolling past them should make the toolbar show the name in order to give context on
- * where the user currently is.
- *
- * @author Alexander Capehart (OxygenCobalt)
- */
-class DetailAppBarLayout
-@JvmOverloads
-constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
- CoordinatorAppBarLayout(context, attrs, defStyleAttr) {
- private var titleView: TextView? = null
- private var recycler: RecyclerView? = null
-
- private var titleShown: Boolean? = null
- private var titleAnimator: ValueAnimator? = null
-
- override fun onAttachedToWindow() {
- super.onAttachedToWindow()
- if (!isInEditMode) {
- (layoutParams as CoordinatorLayout.LayoutParams).behavior = Behavior(context)
- }
- }
-
- private fun findTitleView(): TextView {
- val titleView = titleView
- if (titleView != null) {
- return titleView
- }
-
- // Assume that we have a Toolbar with a detail_toolbar ID, as this view is only
- // used within the detail layouts.
- val toolbar = findViewById(R.id.detail_normal_toolbar)
-
- // The Toolbar's title view is actually hidden. To avoid having to create our own
- // title view, we just reflect into Toolbar and grab the hidden field.
- val newTitleView =
- (TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply {
- // We can never properly initialize the title view's state before draw time,
- // so we just set it's alpha to 0f to produce a less jarring initialization
- // animation.
- alpha = 0f
- }
-
- this.titleView = newTitleView
- return newTitleView
- }
-
- private fun findRecyclerView(): RecyclerView {
- val recycler = recycler
- if (recycler != null) {
- return recycler
- }
-
- // Use the scrolling view in order to find a RecyclerView to use.
- val newRecycler = (parent as ViewGroup).findViewById(liftOnScrollTargetViewId)
- this.recycler = newRecycler
- return newRecycler
- }
-
- private fun setTitleVisibility(visible: Boolean) {
- if (titleShown == visible) return
- titleShown = visible
-
- // Emulate the AppBarLayout lift animation (Linear, alpha 0f -> 1f), but now with
- // the title view's alpha instead of the AppBarLayout's elevation.
- val titleView = findTitleView()
- val from: Float
- val to: Float
-
- if (visible) {
- from = 0f
- to = 1f
- } else {
- from = 1f
- to = 0f
- }
-
- if (titleView.alpha == to) {
- // Nothing to do
- return
- }
-
- logD("Changing title visibility [from: $from to: $to]")
- titleAnimator?.cancel()
- titleAnimator =
- ValueAnimator.ofFloat(from, to).apply {
- addUpdateListener { titleView.alpha = it.animatedValue as Float }
- duration =
- if (titleShown == true) {
- context.getInteger(R.integer.anim_fade_enter_duration).toLong()
- } else {
- context.getInteger(R.integer.anim_fade_exit_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 appBarLayout = child as DetailAppBarLayout
- val recycler = appBarLayout.findRecyclerView()
-
- // Title should be visible if we are no longer showing the top item
- // (i.e the header)
- appBarLayout.setTitleVisibility(
- (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 0)
- }
- }
-
- private companion object {
- val TOOLBAR_TITLE_TEXT_FIELD: Field by lazyReflectedField(Toolbar::class, "mTitleTextView")
- }
-}
diff --git a/app/src/main/res/layout-h360dp/fragment_detail.xml b/app/src/main/res/layout-h360dp/fragment_detail.xml
index ad56ee758..a04d23b7b 100644
--- a/app/src/main/res/layout-h360dp/fragment_detail.xml
+++ b/app/src/main/res/layout-h360dp/fragment_detail.xml
@@ -11,6 +11,7 @@
diff --git a/app/src/main/res/layout-h480dp/fragment_detail.xml b/app/src/main/res/layout-h480dp/fragment_detail.xml
index 71c041256..b219830c9 100644
--- a/app/src/main/res/layout-h480dp/fragment_detail.xml
+++ b/app/src/main/res/layout-h480dp/fragment_detail.xml
@@ -11,6 +11,7 @@
diff --git a/app/src/main/res/layout-sw600dp/fragment_detail.xml b/app/src/main/res/layout-sw600dp/fragment_detail.xml
index d44f004f5..df3e5dfeb 100644
--- a/app/src/main/res/layout-sw600dp/fragment_detail.xml
+++ b/app/src/main/res/layout-sw600dp/fragment_detail.xml
@@ -11,6 +11,7 @@
diff --git a/app/src/main/res/layout-w600dp/fragment_detail.xml b/app/src/main/res/layout-w600dp/fragment_detail.xml
index ad56ee758..a04d23b7b 100644
--- a/app/src/main/res/layout-w600dp/fragment_detail.xml
+++ b/app/src/main/res/layout-w600dp/fragment_detail.xml
@@ -11,6 +11,7 @@
diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml
index 327fe333d..82c95571a 100644
--- a/app/src/main/res/layout/fragment_detail.xml
+++ b/app/src/main/res/layout/fragment_detail.xml
@@ -12,6 +12,7 @@
android:id="@+id/detail_appbar"
style="@style/Widget.Auxio.AppBarLayout"
app:liftOnScroll="true"
+ app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior"
app:liftOnScrollTargetViewId="@id/detail_recycler">