detail: continue scrolling even after toolbar collapses
This commit is contained in:
parent
6c9f170afc
commit
1ee5645780
7 changed files with 121 additions and 169 deletions
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<RecyclerView>(child.liftOnScrollTargetViewId)
|
||||||
|
this.recycler = newRecycler
|
||||||
|
return newRecycler
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FLING_UNITS = 1000 // copied from base class
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<Toolbar>(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<RecyclerView>(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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,6 +11,7 @@
|
||||||
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
||||||
android:id="@+id/detail_appbar"
|
android:id="@+id/detail_appbar"
|
||||||
style="@style/Widget.Auxio.AppBarLayout"
|
style="@style/Widget.Auxio.AppBarLayout"
|
||||||
|
app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior"
|
||||||
app:liftOnScroll="true"
|
app:liftOnScroll="true"
|
||||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
||||||
android:id="@+id/detail_appbar"
|
android:id="@+id/detail_appbar"
|
||||||
style="@style/Widget.Auxio.AppBarLayout"
|
style="@style/Widget.Auxio.AppBarLayout"
|
||||||
|
app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior"
|
||||||
app:liftOnScroll="true"
|
app:liftOnScroll="true"
|
||||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
||||||
android:id="@+id/detail_appbar"
|
android:id="@+id/detail_appbar"
|
||||||
style="@style/Widget.Auxio.AppBarLayout"
|
style="@style/Widget.Auxio.AppBarLayout"
|
||||||
|
app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior"
|
||||||
app:liftOnScroll="true"
|
app:liftOnScroll="true"
|
||||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
||||||
android:id="@+id/detail_appbar"
|
android:id="@+id/detail_appbar"
|
||||||
style="@style/Widget.Auxio.AppBarLayout"
|
style="@style/Widget.Auxio.AppBarLayout"
|
||||||
|
app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior"
|
||||||
app:liftOnScroll="true"
|
app:liftOnScroll="true"
|
||||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
android:id="@+id/detail_appbar"
|
android:id="@+id/detail_appbar"
|
||||||
style="@style/Widget.Auxio.AppBarLayout"
|
style="@style/Widget.Auxio.AppBarLayout"
|
||||||
app:liftOnScroll="true"
|
app:liftOnScroll="true"
|
||||||
|
app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior"
|
||||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||||
|
|
||||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
|
Loading…
Reference in a new issue