Explorar o código

Change how the bottom navigation is hidden (#5823)

* Change how the bottom navigation is hidden

Modifies the translationY instead of the height.

* Cleanups
Ivan Iskandar %!s(int64=3) %!d(string=hai) anos
pai
achega
f125ab01ee

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -383,7 +383,7 @@ class LibraryController(
                 actionMode!!,
                 R.menu.library_selection
             ) { onActionItemClicked(it!!) }
-            (activity as? MainActivity)?.showBottomNav(visible = false, expand = true)
+            (activity as? MainActivity)?.showBottomNav(false)
         }
     }
 
@@ -492,7 +492,7 @@ class LibraryController(
         selectionRelay.call(LibrarySelectionEvent.Cleared())
 
         binding.actionToolbar.hide()
-        (activity as? MainActivity)?.showBottomNav(visible = true, expand = true)
+        (activity as? MainActivity)?.showBottomNav(true)
 
         actionMode = null
     }

+ 8 - 38
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -10,7 +10,6 @@ import android.view.Gravity
 import android.view.ViewGroup
 import android.widget.Toast
 import androidx.appcompat.view.ActionMode
-import androidx.coordinatorlayout.widget.CoordinatorLayout
 import androidx.core.animation.doOnEnd
 import androidx.core.splashscreen.SplashScreen
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -64,7 +63,6 @@ import eu.kanade.tachiyomi.util.system.dpToPx
 import eu.kanade.tachiyomi.util.system.isTablet
 import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
-import eu.kanade.tachiyomi.widget.HideBottomNavigationOnScrollBehavior
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.launchIn
@@ -86,8 +84,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
         }
     }
 
-    private var bottomNavAnimator: ViewHeightAnimator? = null
-
     private var isConfirmingExit: Boolean = false
     private var isHandlingShortcut: Boolean = false
 
@@ -138,15 +134,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
         }
         setSplashScreenExitAnimation(splashScreen)
 
-        if (binding.bottomNav != null) {
-            bottomNavAnimator = ViewHeightAnimator(binding.bottomNav!!)
-
-            // Set behavior of bottom nav
-            preferences.hideBottomBarOnScroll()
-                .asImmediateFlow { setBottomNavBehaviorOnScroll() }
-                .launchIn(lifecycleScope)
-        }
-
         if (binding.sideNav != null) {
             preferences.sideNavIconAlignment()
                 .asImmediateFlow {
@@ -532,11 +519,11 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
         binding.appbar.setExpanded(true)
 
         if ((from == null || from is RootController) && to !is RootController) {
-            showNav(visible = false, expand = true)
+            showNav(false)
         }
         if (to is RootController) {
             // Always show bottom nav again when returning to a RootController
-            showNav(visible = true, expand = from !is RootController)
+            showNav(true)
         }
 
         if (from is TabbedController) {
@@ -587,27 +574,22 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
         }
     }
 
-    private fun showNav(visible: Boolean, expand: Boolean = false) {
-        showBottomNav(visible, expand)
+    private fun showNav(visible: Boolean) {
+        showBottomNav(visible)
         showSideNav(visible)
     }
 
     // Also used from some controllers to swap bottom nav with action toolbar
-    fun showBottomNav(visible: Boolean, expand: Boolean = false) {
+    fun showBottomNav(visible: Boolean) {
         if (visible) {
-            binding.bottomNav?.translationY = 0F
-            if (expand) {
-                bottomNavAnimator?.expand()
-            }
+            binding.bottomNav?.slideUp()
         } else {
-            bottomNavAnimator?.collapse()
+            binding.bottomNav?.slideDown()
         }
     }
 
     private fun showSideNav(visible: Boolean) {
-        binding.sideNav?.let {
-            it.isVisible = visible
-        }
+        binding.sideNav?.isVisible = visible
     }
 
     /**
@@ -622,18 +604,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
         }
     }
 
-    private fun setBottomNavBehaviorOnScroll() {
-        showNav(visible = true)
-
-        binding.bottomNav?.updateLayoutParams<CoordinatorLayout.LayoutParams> {
-            behavior = when {
-                preferences.hideBottomBarOnScroll().get() -> HideBottomNavigationOnScrollBehavior()
-                else -> null
-            }
-        }
-        binding.bottomNav?.translationY = 0F
-    }
-
     private val nav: NavigationBarView
         get() = binding.bottomNav ?: binding.sideNav!!
 

+ 0 - 107
app/src/main/java/eu/kanade/tachiyomi/ui/main/ViewHeightAnimator.kt

@@ -1,107 +0,0 @@
-package eu.kanade.tachiyomi.ui.main
-
-import android.animation.ObjectAnimator
-import android.view.View
-import android.view.ViewTreeObserver
-import android.view.animation.DecelerateInterpolator
-import androidx.annotation.Keep
-
-class ViewHeightAnimator(val view: View, val duration: Long = 250L) {
-
-    /**
-     * The default height of the view. It's unknown until the view is layout.
-     */
-    private var height = 0
-
-    /**
-     * Whether the last state of the view is shown or hidden.
-     */
-    private var isLastStateShown = true
-
-    /**
-     * Animation used to expand and collapse the view.
-     */
-    private val animation by lazy {
-        ObjectAnimator.ofInt(this, "height", height).apply {
-            duration = [email protected]
-            interpolator = DecelerateInterpolator()
-        }
-    }
-
-    init {
-        view.viewTreeObserver.addOnGlobalLayoutListener(
-            object : ViewTreeObserver.OnGlobalLayoutListener {
-                override fun onGlobalLayout() {
-                    if (view.height > 0) {
-                        view.viewTreeObserver.removeOnGlobalLayoutListener(this)
-
-                        // Save the tabs default height.
-                        height = view.height
-
-                        // Now that we know the height, set the initial height.
-                        if (isLastStateShown) {
-                            setHeight(height)
-                        } else {
-                            setHeight(0)
-                        }
-                    }
-                }
-            }
-        )
-    }
-
-    /**
-     * Sets the height of the tab layout.
-     *
-     * @param newHeight The new height of the tab layout.
-     */
-    @Keep
-    fun setHeight(newHeight: Int) {
-        view.layoutParams.height = newHeight
-        view.requestLayout()
-    }
-
-    /**
-     * Returns the height of the tab layout. This method is also called from the animator through
-     * reflection.
-     */
-    fun getHeight(): Int {
-        return view.layoutParams.height
-    }
-
-    /**
-     * Expands the tab layout with an animation.
-     */
-    fun expand() {
-        if (isMeasured) {
-            if (getHeight() != height) {
-                animation.setIntValues(height)
-                animation.start()
-            } else {
-                animation.cancel()
-            }
-        }
-        isLastStateShown = true
-    }
-
-    /**
-     * Collapse the tab layout with an animation.
-     */
-    fun collapse() {
-        if (isMeasured) {
-            if (getHeight() != 0) {
-                animation.setIntValues(0)
-                animation.start()
-            } else {
-                animation.cancel()
-            }
-        }
-        isLastStateShown = false
-    }
-
-    /**
-     * Returns whether the tab layout has a known height.
-     */
-    private val isMeasured: Boolean
-        get() = height > 0
-}

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt

@@ -180,7 +180,7 @@ class UpdatesController :
                 actionMode!!,
                 R.menu.updates_chapter_selection
             ) { onActionItemClicked(it!!) }
-            (activity as? MainActivity)?.showBottomNav(visible = false, expand = true)
+            (activity as? MainActivity)?.showBottomNav(false)
         }
 
         toggleSelection(position)
@@ -386,7 +386,7 @@ class UpdatesController :
         adapter?.clearSelection()
 
         binding.actionToolbar.hide()
-        (activity as? MainActivity)?.showBottomNav(visible = true, expand = true)
+        (activity as? MainActivity)?.showBottomNav(true)
 
         actionMode = null
     }

+ 6 - 0
app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt

@@ -1,6 +1,7 @@
 package eu.kanade.tachiyomi.util.system
 
 import android.content.Context
+import android.view.ViewPropertyAnimator
 import android.view.animation.Animation
 import androidx.constraintlayout.motion.widget.MotionScene.Transition
 
@@ -14,3 +15,8 @@ fun Transition.applySystemAnimatorScale(context: Context) {
     // End layout of cover expanding animation tends to break when the transition is less than ~25ms
     this.duration = (this.duration * context.animatorDurationScale).toInt().coerceAtLeast(25)
 }
+
+/** Scale the duration of this [ViewPropertyAnimator] by [Context.animatorDurationScale] */
+fun ViewPropertyAnimator.applySystemAnimatorScale(context: Context): ViewPropertyAnimator = apply {
+    this.duration = (this.duration * context.animatorDurationScale).toLong()
+}

+ 174 - 0
app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt

@@ -0,0 +1,174 @@
+package eu.kanade.tachiyomi.widget
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.TimeInterpolator
+import android.content.Context
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.AttributeSet
+import android.view.ViewPropertyAnimator
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.view.doOnLayout
+import androidx.core.view.doOnNextLayout
+import androidx.core.view.updateLayoutParams
+import androidx.customview.view.AbsSavedState
+import androidx.interpolator.view.animation.FastOutLinearInInterpolator
+import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.bottomnavigation.BottomNavigationView
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.asImmediateFlow
+import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
+import kotlinx.coroutines.flow.launchIn
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class TachiyomiBottomNavigationView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = R.attr.bottomNavigationStyle,
+    defStyleRes: Int = R.style.Widget_Design_BottomNavigationView
+) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes) {
+
+    private var currentAnimator: ViewPropertyAnimator? = null
+
+    private var currentState = STATE_UP
+
+    init {
+        // Hide on scroll
+        doOnLayout {
+            findViewTreeLifecycleOwner()?.lifecycleScope?.let { scope ->
+                Injekt.get<PreferencesHelper>().hideBottomBarOnScroll()
+                    .asImmediateFlow {
+                        updateLayoutParams<CoordinatorLayout.LayoutParams> {
+                            behavior = if (it) {
+                                HideBottomNavigationOnScrollBehavior()
+                            } else {
+                                null
+                            }
+                        }
+                    }
+                    .launchIn(scope)
+            }
+        }
+    }
+
+    override fun onSaveInstanceState(): Parcelable {
+        val superState = super.onSaveInstanceState()
+        return SavedState(superState).also {
+            it.currentState = currentState
+        }
+    }
+
+    override fun onRestoreInstanceState(state: Parcelable?) {
+        if (state is SavedState) {
+            super.onRestoreInstanceState(state.superState)
+            doOnNextLayout {
+                if (state.currentState == STATE_UP) {
+                    slideUp(animate = false)
+                } else if (state.currentState == STATE_DOWN) {
+                    slideDown(animate = false)
+                }
+            }
+        } else {
+            super.onRestoreInstanceState(state)
+        }
+    }
+
+    override fun setTranslationY(translationY: Float) {
+        // Disallow translation change when state down
+        if (currentState == STATE_DOWN) return
+        super.setTranslationY(translationY)
+    }
+
+    /**
+     * Shows this view up.
+     *
+     * @param animate True if slide up should be animated
+     */
+    fun slideUp(animate: Boolean = true) {
+        currentAnimator?.cancel()
+        clearAnimation()
+
+        currentState = STATE_UP
+        animateTranslation(
+            0F,
+            if (animate) SLIDE_UP_ANIMATION_DURATION else 0,
+            LinearOutSlowInInterpolator()
+        )
+    }
+
+    /**
+     * Hides this view down. [setTranslationY] won't work until [slideUp] is called.
+     *
+     * @param animate True if slide down should be animated
+     */
+    fun slideDown(animate: Boolean = true) {
+        currentAnimator?.cancel()
+        clearAnimation()
+
+        currentState = STATE_DOWN
+        animateTranslation(
+            height.toFloat(),
+            if (animate) SLIDE_DOWN_ANIMATION_DURATION else 0,
+            FastOutLinearInInterpolator()
+        )
+    }
+
+    private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) {
+        currentAnimator = animate()
+            .translationY(targetY)
+            .setInterpolator(interpolator)
+            .setDuration(duration)
+            .applySystemAnimatorScale(context)
+            .setListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator?) {
+                    currentAnimator = null
+                    postInvalidate()
+                }
+            })
+    }
+
+    internal class SavedState : AbsSavedState {
+        var currentState = STATE_UP
+
+        constructor(superState: Parcelable) : super(superState)
+
+        constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
+            currentState = source.readByte().toInt()
+        }
+
+        override fun writeToParcel(out: Parcel, flags: Int) {
+            super.writeToParcel(out, flags)
+            out.writeByte(currentState.toByte())
+        }
+
+        companion object {
+            @JvmField
+            val CREATOR: Parcelable.ClassLoaderCreator<SavedState> = object : Parcelable.ClassLoaderCreator<SavedState> {
+                override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState {
+                    return SavedState(source, loader)
+                }
+
+                override fun createFromParcel(source: Parcel): SavedState {
+                    return SavedState(source, null)
+                }
+
+                override fun newArray(size: Int): Array<SavedState> {
+                    return newArray(size)
+                }
+            }
+        }
+    }
+
+    companion object {
+        private const val STATE_DOWN = 1
+        private const val STATE_UP = 2
+
+        private const val SLIDE_UP_ANIMATION_DURATION = 225L
+        private const val SLIDE_DOWN_ANIMATION_DURATION = 175L
+    }
+}

+ 1 - 1
app/src/main/res/layout/main_activity.xml

@@ -75,7 +75,7 @@
         android:id="@+id/fab_layout"
         layout="@layout/main_activity_fab" />
 
-    <com.google.android.material.bottomnavigation.BottomNavigationView
+    <eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
         android:id="@+id/bottom_nav"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"