Эх сурвалжийг харах

Toolbar and bottom nav scroll snap (#5915)

Ivan Iskandar 3 жил өмнө
parent
commit
a2d007f2a9

+ 102 - 0
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<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()
+    }
+}

+ 4 - 0
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<AppBarLayout> = HideToolbarOnScrollBehavior()
+
     /**
      * Disabled. Lift on scroll is handled manually with [TachiyomiCoordinatorLayout]
      */

+ 67 - 2
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<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(
         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()
     }
 }