From a2d007f2a9cb9273c9f5af3c388fbcebd3642b5b Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sun, 19 Sep 2021 03:41:23 +0700 Subject: [PATCH] Toolbar and bottom nav scroll snap (#5915) --- .../appbar/HideToolbarOnScrollBehavior.kt | 102 ++++++++++++++++++ .../tachiyomi/widget/ElevationAppBarLayout.kt | 4 + .../HideBottomNavigationOnScrollBehavior.kt | 69 +++++++++++- 3 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/google/android/material/appbar/HideToolbarOnScrollBehavior.kt diff --git a/app/src/main/java/com/google/android/material/appbar/HideToolbarOnScrollBehavior.kt b/app/src/main/java/com/google/android/material/appbar/HideToolbarOnScrollBehavior.kt new file mode 100644 index 000000000..a6c931881 --- /dev/null +++ b/app/src/main/java/com/google/android/material/appbar/HideToolbarOnScrollBehavior.kt @@ -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()?.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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt index 3f68bb1c2..c1dd79bc9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ElevationAppBarLayout.kt @@ -5,10 +5,12 @@ import android.content.Context import android.util.AttributeSet import android.widget.TextView import androidx.annotation.FloatRange +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.lifecycle.coroutineScope import androidx.lifecycle.findViewTreeLifecycleOwner import com.google.android.material.animation.AnimationUtils import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.appbar.HideToolbarOnScrollBehavior import com.google.android.material.appbar.MaterialToolbar import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.view.findChild @@ -51,6 +53,8 @@ class ElevationAppBarLayout @JvmOverloads constructor( } } + override fun getBehavior(): CoordinatorLayout.Behavior = HideToolbarOnScrollBehavior() + /** * Disabled. Lift on scroll is handled manually with [TachiyomiCoordinatorLayout] */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/HideBottomNavigationOnScrollBehavior.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/HideBottomNavigationOnScrollBehavior.kt index 27a1d7b9d..a4b7a43dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/HideBottomNavigationOnScrollBehavior.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/HideBottomNavigationOnScrollBehavior.kt @@ -1,11 +1,19 @@ package eu.kanade.tachiyomi.widget +import android.animation.ValueAnimator import android.content.Context import android.util.AttributeSet 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.core.view.ViewCompat +import com.google.android.material.appbar.AppBarLayout 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] @@ -15,6 +23,31 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor( attrs: AttributeSet? = null ) : CoordinatorLayout.Behavior(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()?.height ?: 0 + dyRatio = if (toolbarSize > 0) { + child.height.toFloat() / toolbarSize + } else { + 1F + } + return false + } + override fun onStartNestedScroll( coordinatorLayout: CoordinatorLayout, child: BottomNavigationView, @@ -23,7 +56,12 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor( axes: Int, type: Int ): Boolean { - return axes == ViewCompat.SCROLL_AXIS_VERTICAL + if (axes != ViewCompat.SCROLL_AXIS_VERTICAL) { + return false + } + lastStartedType = type + offsetAnimator?.cancel() + return true } override fun onNestedPreScroll( @@ -36,6 +74,33 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor( type: Int ) { 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() } }