mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Change how the bottom navigation is hidden (#5823)
* Change how the bottom navigation is hidden Modifies the translationY instead of the height. * Cleanups
This commit is contained in:
		| @@ -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 | ||||
|     } | ||||
|   | ||||
| @@ -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!! | ||||
|  | ||||
|   | ||||
| @@ -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 = this@ViewHeightAnimator.duration | ||||
|             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 | ||||
| } | ||||
| @@ -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 | ||||
|     } | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
| @@ -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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user