From 1ee5645780eab3f80a2188d3e8a1df0a9cfa0a2d Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 17 Oct 2024 09:44:51 -0600 Subject: [PATCH] detail: continue scrolling even after toolbar collapses --- .../detail/ContinuousAppBarLayoutBehavior.kt | 116 ++++++++++++ .../auxio/detail/DetailAppBarLayout.kt | 169 ------------------ .../res/layout-h360dp/fragment_detail.xml | 1 + .../res/layout-h480dp/fragment_detail.xml | 1 + .../res/layout-sw600dp/fragment_detail.xml | 1 + .../res/layout-w600dp/fragment_detail.xml | 1 + app/src/main/res/layout/fragment_detail.xml | 1 + 7 files changed, 121 insertions(+), 169 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/detail/ContinuousAppBarLayoutBehavior.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt 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">