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
|
||||
android:id="@+id/detail_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior"
|
||||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
||||
android:id="@+id/detail_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior"
|
||||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
||||
android:id="@+id/detail_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior"
|
||||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<org.oxycblt.auxio.ui.CoordinatorAppBarLayout
|
||||
android:id="@+id/detail_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
app:layout_behavior="org.oxycblt.auxio.detail.ContinuousAppBarLayoutBehavior"
|
||||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
|
|
Loading…
Reference in a new issue