Toolbar and bottom nav scroll snap (#5915)
This commit is contained in:
parent
774f818bbb
commit
a2d007f2a9
@ -0,0 +1,102 @@
|
|||||||
|
package com.google.android.material.appbar
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.animation.doOnEnd
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.marginTop
|
||||||
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
|
import eu.kanade.tachiyomi.util.view.findChild
|
||||||
|
import eu.kanade.tachiyomi.widget.ElevationAppBarLayout
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide toolbar on scroll behavior for [AppBarLayout].
|
||||||
|
*
|
||||||
|
* Inside this package to access some package-private methods.
|
||||||
|
*/
|
||||||
|
class HideToolbarOnScrollBehavior : AppBarLayout.Behavior() {
|
||||||
|
|
||||||
|
@ViewCompat.NestedScrollType
|
||||||
|
private var lastStartedType: Int = 0
|
||||||
|
|
||||||
|
private var offsetAnimator: ValueAnimator? = null
|
||||||
|
|
||||||
|
private var toolbarHeight: Int = 0
|
||||||
|
|
||||||
|
override fun onStartNestedScroll(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
child: AppBarLayout,
|
||||||
|
directTargetChild: View,
|
||||||
|
target: View,
|
||||||
|
nestedScrollAxes: Int,
|
||||||
|
type: Int
|
||||||
|
): Boolean {
|
||||||
|
lastStartedType = type
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopNestedScroll(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
layout: AppBarLayout,
|
||||||
|
target: View,
|
||||||
|
type: Int
|
||||||
|
) {
|
||||||
|
super.onStopNestedScroll(parent, layout, target, type)
|
||||||
|
if (toolbarHeight == 0) {
|
||||||
|
toolbarHeight = layout.findChild<Toolbar>()?.height ?: 0
|
||||||
|
}
|
||||||
|
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
||||||
|
animateToolbarVisibility(
|
||||||
|
parent,
|
||||||
|
layout,
|
||||||
|
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFlingFinished(parent: CoordinatorLayout, layout: AppBarLayout) {
|
||||||
|
super.onFlingFinished(parent, layout)
|
||||||
|
animateToolbarVisibility(
|
||||||
|
parent,
|
||||||
|
layout,
|
||||||
|
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTopBottomOffsetForScrollingSibling(abl: AppBarLayout): Int {
|
||||||
|
return topBottomOffsetForScrollingSibling - abl.marginTop
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateToolbarVisibility(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: AppBarLayout,
|
||||||
|
isVisible: Boolean
|
||||||
|
) {
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
offsetAnimator = ValueAnimator().apply {
|
||||||
|
interpolator = DecelerateInterpolator()
|
||||||
|
duration = (150 * child.context.animatorDurationScale).roundToLong()
|
||||||
|
addUpdateListener {
|
||||||
|
setHeaderTopBottomOffset(coordinatorLayout, child, it.animatedValue as Int)
|
||||||
|
}
|
||||||
|
doOnEnd {
|
||||||
|
if (!isVisible &&
|
||||||
|
!child.isLifted &&
|
||||||
|
(child as? ElevationAppBarLayout)?.isTransparentWhenNotLifted == true
|
||||||
|
) {
|
||||||
|
child.isLifted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offsetAnimator?.setIntValues(
|
||||||
|
getTopBottomOffsetForScrollingSibling(child),
|
||||||
|
if (isVisible) 0 else -toolbarHeight
|
||||||
|
)
|
||||||
|
offsetAnimator?.start()
|
||||||
|
}
|
||||||
|
}
|
@ -5,10 +5,12 @@ import android.content.Context
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.FloatRange
|
import androidx.annotation.FloatRange
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
import com.google.android.material.animation.AnimationUtils
|
import com.google.android.material.animation.AnimationUtils
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.appbar.HideToolbarOnScrollBehavior
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.view.findChild
|
import eu.kanade.tachiyomi.util.view.findChild
|
||||||
@ -51,6 +53,8 @@ class ElevationAppBarLayout @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getBehavior(): CoordinatorLayout.Behavior<AppBarLayout> = HideToolbarOnScrollBehavior()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disabled. Lift on scroll is handled manually with [TachiyomiCoordinatorLayout]
|
* Disabled. Lift on scroll is handled manually with [TachiyomiCoordinatorLayout]
|
||||||
*/
|
*/
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.widget
|
package eu.kanade.tachiyomi.widget
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
|
import eu.kanade.tachiyomi.util.view.findChild
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide behavior similar to app bar for [BottomNavigationView]
|
* Hide behavior similar to app bar for [BottomNavigationView]
|
||||||
@ -15,6 +23,31 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
|||||||
attrs: AttributeSet? = null
|
attrs: AttributeSet? = null
|
||||||
) : CoordinatorLayout.Behavior<BottomNavigationView>(context, attrs) {
|
) : CoordinatorLayout.Behavior<BottomNavigationView>(context, attrs) {
|
||||||
|
|
||||||
|
@ViewCompat.NestedScrollType
|
||||||
|
private var lastStartedType: Int = 0
|
||||||
|
|
||||||
|
private var offsetAnimator: ValueAnimator? = null
|
||||||
|
|
||||||
|
private var dyRatio = 1F
|
||||||
|
|
||||||
|
override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean {
|
||||||
|
return dependency is AppBarLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDependentViewChanged(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
child: BottomNavigationView,
|
||||||
|
dependency: View
|
||||||
|
): Boolean {
|
||||||
|
val toolbarSize = (dependency as ViewGroup).findChild<Toolbar>()?.height ?: 0
|
||||||
|
dyRatio = if (toolbarSize > 0) {
|
||||||
|
child.height.toFloat() / toolbarSize
|
||||||
|
} else {
|
||||||
|
1F
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStartNestedScroll(
|
override fun onStartNestedScroll(
|
||||||
coordinatorLayout: CoordinatorLayout,
|
coordinatorLayout: CoordinatorLayout,
|
||||||
child: BottomNavigationView,
|
child: BottomNavigationView,
|
||||||
@ -23,7 +56,12 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
|||||||
axes: Int,
|
axes: Int,
|
||||||
type: Int
|
type: Int
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return axes == ViewCompat.SCROLL_AXIS_VERTICAL
|
if (axes != ViewCompat.SCROLL_AXIS_VERTICAL) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lastStartedType = type
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNestedPreScroll(
|
override fun onNestedPreScroll(
|
||||||
@ -36,6 +74,33 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
|||||||
type: Int
|
type: Int
|
||||||
) {
|
) {
|
||||||
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
|
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
|
||||||
child.translationY = (child.translationY + dy).coerceIn(0F, child.height.toFloat())
|
child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopNestedScroll(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: BottomNavigationView,
|
||||||
|
target: View,
|
||||||
|
type: Int
|
||||||
|
) {
|
||||||
|
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
||||||
|
animateBottomNavigationVisibility(child, child.translationY < child.height / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateBottomNavigationVisibility(child: BottomNavigationView, isVisible: Boolean) {
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
offsetAnimator = ValueAnimator().apply {
|
||||||
|
interpolator = DecelerateInterpolator()
|
||||||
|
duration = (150 * child.context.animatorDurationScale).roundToLong()
|
||||||
|
addUpdateListener {
|
||||||
|
child.translationY = it.animatedValue as Float
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offsetAnimator?.setFloatValues(
|
||||||
|
child.translationY,
|
||||||
|
if (isVisible) 0F else child.height.toFloat()
|
||||||
|
)
|
||||||
|
offsetAnimator?.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user