From bbe180ecd12d4faaa53dad7a35a0611044317ffa Mon Sep 17 00:00:00 2001 From: len Date: Sat, 20 May 2017 12:15:44 +0200 Subject: [PATCH] Improve tab layout animation. Fixes #800 and #801 --- .../tachiyomi/ui/library/LibraryController.kt | 38 ++++-- .../kanade/tachiyomi/ui/main/MainActivity.kt | 6 +- .../kanade/tachiyomi/ui/main/TabsAnimator.kt | 121 ++++++++++++++---- app/src/main/res/layout/activity_main.xml | 1 - 4 files changed, 133 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 4ce6cedd86..0a60296ca7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -31,12 +31,14 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.category.CategoryController +import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.library_controller.view.* +import rx.Subscription import timber.log.Timber import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -118,6 +120,10 @@ class LibraryController( */ private var drawerListener: DrawerLayout.DrawerListener? = null + private var tabsVisibilityRelay: BehaviorRelay = BehaviorRelay.create() + + private var tabsVisibilitySubscription: Subscription? = null + init { setHasOptionsMenu(true) } @@ -173,6 +179,8 @@ class LibraryController( super.onDestroyView(view) adapter = null actionMode = null + tabsVisibilitySubscription?.unsubscribe() + tabsVisibilitySubscription = null } override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup { @@ -204,6 +212,27 @@ class LibraryController( navView = null } + override fun configureTabs(tabs: TabLayout) { + with(tabs) { + tabGravity = TabLayout.GRAVITY_CENTER + tabMode = TabLayout.MODE_SCROLLABLE + } + tabsVisibilitySubscription?.unsubscribe() + tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible -> + val tabAnimator = (activity as? MainActivity)?.tabAnimator + if (visible) { + tabAnimator?.expand() + } else { + tabAnimator?.collapse() + } + } + } + + override fun cleanupTabs(tabs: TabLayout) { + tabsVisibilitySubscription?.unsubscribe() + tabsVisibilitySubscription = null + } + fun onNextLibraryUpdate(categories: List, mangaMap: Map>) { val view = view ?: return val adapter = adapter ?: return @@ -227,7 +256,7 @@ class LibraryController( // Restore active category. view.view_pager.setCurrentItem(activeCat, false) - tabs?.visibility = if (categories.size <= 1) View.GONE else View.VISIBLE + tabsVisibilityRelay.call(categories.size > 1) // Delay the scroll position to allow the view to be properly measured. view.post { @@ -282,13 +311,6 @@ class LibraryController( adapter.recycle = true } - override fun configureTabs(tabs: TabLayout) { - with(tabs) { - tabGravity = TabLayout.GRAVITY_CENTER - tabMode = TabLayout.MODE_SCROLLABLE - } - } - /** * Creates the action mode if it's not created already. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 5eac8dde2a..aa613d8c39 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -49,7 +49,7 @@ class MainActivity : BaseActivity() { } } - private val tabAnimator by lazy { TabsAnimator(tabs) } + lateinit var tabAnimator: TabsAnimator override fun onCreate(savedInstanceState: Bundle?) { setAppTheme() @@ -69,6 +69,8 @@ class MainActivity : BaseActivity() { drawerArrow?.color = Color.WHITE toolbar.navigationIcon = drawerArrow + tabAnimator = TabsAnimator(tabs) + // Set behavior of Navigation drawer nav_view.setNavigationItemSelectedListener { item -> val id = item.itemId @@ -190,8 +192,8 @@ class MainActivity : BaseActivity() { from.cleanupTabs(tabs) } if (to is TabbedController) { - to.configureTabs(tabs) tabAnimator.expand() + to.configureTabs(tabs) } else { tabAnimator.collapse() tabs.setupWithViewPager(null) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt index 82109cd326..498f979690 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/TabsAnimator.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.ui.main import android.support.design.widget.TabLayout +import android.view.View +import android.view.ViewTreeObserver import android.view.animation.Animation import android.view.animation.DecelerateInterpolator import android.view.animation.Transformation @@ -9,15 +11,32 @@ import eu.kanade.tachiyomi.util.visible class TabsAnimator(val tabs: TabLayout) { - private var height = 0 + /** + * The default height of the tab layout. It's unknown until the view is layout. + */ + private var tabsHeight = 0 + /** + * Whether the last state of the tab layout is [View.VISIBLE] or [View.GONE]. + */ + private var isLastStateShown = true + + /** + * Interpolator used to animate the tab layout. + */ private val interpolator = DecelerateInterpolator() + /** + * Duration of the animation. + */ private val duration = 300L + /** + * Animation used to expand the tab layout. + */ private val expandAnimation = object : Animation() { override fun applyTransformation(interpolatedTime: Float, t: Transformation) { - tabs.layoutParams.height = (height * interpolatedTime).toInt() + setHeight((tabsHeight * interpolatedTime).toInt()) tabs.requestLayout() } @@ -26,12 +45,24 @@ class TabsAnimator(val tabs: TabLayout) { } } + /** + * Animation used to collapse the tab layout. + */ private val collapseAnimation = object : Animation() { + + /** + * Property holding the height of the tabs at the moment the animation is started. Useful + * to provide a seamless animation. + */ + private var startHeight = 0 + override fun applyTransformation(interpolatedTime: Float, t: Transformation) { - if (interpolatedTime == 1f) { + if (interpolatedTime == 0f) { + startHeight = tabs.height + } else if (interpolatedTime == 1f) { tabs.gone() } else { - tabs.layoutParams.height = (height * (1 - interpolatedTime)).toInt() + setHeight((startHeight * (1 - interpolatedTime)).toInt()) tabs.requestLayout() } } @@ -46,29 +77,75 @@ class TabsAnimator(val tabs: TabLayout) { collapseAnimation.interpolator = interpolator expandAnimation.duration = duration expandAnimation.interpolator = interpolator - } - fun expand() { - tabs.visible() - if (measure() && tabs.measuredHeight != height) { - tabs.startAnimation(expandAnimation) - } - } + isLastStateShown = tabs.visibility == View.VISIBLE + tabs.viewTreeObserver.addOnGlobalLayoutListener( + object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + if (tabs.height > 0) { + tabs.viewTreeObserver.removeOnGlobalLayoutListener(this) - fun collapse() { - if (measure() && tabs.measuredHeight != 0) { - tabs.startAnimation(collapseAnimation) - } else { - tabs.gone() - } + // Save the tabs default height. + tabsHeight = tabs.height + + // Now that we know the height, set the initial height and visibility. + if (isLastStateShown) { + setHeight(tabsHeight) + tabs.visible() + } else { + setHeight(0) + tabs.gone() + } + } + } + } + ) } /** - * Returns true if the view is measured, otherwise query dimensions and check again. + * Sets the height of the tab layout. + * + * @param newHeight The new height of the tab layout. */ - private fun measure(): Boolean { - if (height > 0) return true - height = tabs.measuredHeight - return height > 0 + private fun setHeight(newHeight: Int) { + tabs.layoutParams.height = newHeight } + + /** + * Expands the tab layout with an animation. + */ + fun expand() { + cancelCurrentAnimations() + tabs.visible() + if (isMeasured && (!isLastStateShown || tabs.height != tabsHeight)) { + tabs.startAnimation(expandAnimation) + } + isLastStateShown = true + } + + /** + * Collapse the tab layout with an animation. + */ + fun collapse() { + cancelCurrentAnimations() + if (isMeasured && (isLastStateShown || tabs.height != 0)) { + tabs.startAnimation(collapseAnimation) + } + isLastStateShown = false + } + + /** + * Cancels all the currently running animations. + */ + private fun cancelCurrentAnimations() { + collapseAnimation.cancel() + expandAnimation.cancel() + } + + /** + * Returns whether the tab layout has a known height. + */ + val isMeasured: Boolean + get() = tabsHeight > 0 + } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 92da0a5fc2..f7f2c7e4b7 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -24,7 +24,6 @@ android:id="@+id/tabs" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="gone" android:theme="@style/Theme.ActionBar.Tab" app:tabIndicatorColor="@android:color/white" app:tabGravity="center"