Improve tab layout animation. Fixes #800 and #801

This commit is contained in:
len 2017-05-20 12:15:44 +02:00
parent 67678cd49e
commit bbe180ecd1
4 changed files with 133 additions and 33 deletions

View file

@ -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.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.category.CategoryController 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.ui.manga.MangaController
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.library_controller.view.* import kotlinx.android.synthetic.main.library_controller.view.*
import rx.Subscription
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -118,6 +120,10 @@ class LibraryController(
*/ */
private var drawerListener: DrawerLayout.DrawerListener? = null private var drawerListener: DrawerLayout.DrawerListener? = null
private var tabsVisibilityRelay: BehaviorRelay<Boolean> = BehaviorRelay.create()
private var tabsVisibilitySubscription: Subscription? = null
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
@ -173,6 +179,8 @@ class LibraryController(
super.onDestroyView(view) super.onDestroyView(view)
adapter = null adapter = null
actionMode = null actionMode = null
tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = null
} }
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup { override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup {
@ -204,6 +212,27 @@ class LibraryController(
navView = null 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<Category>, mangaMap: Map<Int, List<LibraryItem>>) { fun onNextLibraryUpdate(categories: List<Category>, mangaMap: Map<Int, List<LibraryItem>>) {
val view = view ?: return val view = view ?: return
val adapter = adapter ?: return val adapter = adapter ?: return
@ -227,7 +256,7 @@ class LibraryController(
// Restore active category. // Restore active category.
view.view_pager.setCurrentItem(activeCat, false) 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. // Delay the scroll position to allow the view to be properly measured.
view.post { view.post {
@ -282,13 +311,6 @@ class LibraryController(
adapter.recycle = true 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. * Creates the action mode if it's not created already.
*/ */

View file

@ -49,7 +49,7 @@ class MainActivity : BaseActivity() {
} }
} }
private val tabAnimator by lazy { TabsAnimator(tabs) } lateinit var tabAnimator: TabsAnimator
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setAppTheme() setAppTheme()
@ -69,6 +69,8 @@ class MainActivity : BaseActivity() {
drawerArrow?.color = Color.WHITE drawerArrow?.color = Color.WHITE
toolbar.navigationIcon = drawerArrow toolbar.navigationIcon = drawerArrow
tabAnimator = TabsAnimator(tabs)
// Set behavior of Navigation drawer // Set behavior of Navigation drawer
nav_view.setNavigationItemSelectedListener { item -> nav_view.setNavigationItemSelectedListener { item ->
val id = item.itemId val id = item.itemId
@ -190,8 +192,8 @@ class MainActivity : BaseActivity() {
from.cleanupTabs(tabs) from.cleanupTabs(tabs)
} }
if (to is TabbedController) { if (to is TabbedController) {
to.configureTabs(tabs)
tabAnimator.expand() tabAnimator.expand()
to.configureTabs(tabs)
} else { } else {
tabAnimator.collapse() tabAnimator.collapse()
tabs.setupWithViewPager(null) tabs.setupWithViewPager(null)

View file

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.main package eu.kanade.tachiyomi.ui.main
import android.support.design.widget.TabLayout import android.support.design.widget.TabLayout
import android.view.View
import android.view.ViewTreeObserver
import android.view.animation.Animation import android.view.animation.Animation
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.view.animation.Transformation import android.view.animation.Transformation
@ -9,15 +11,32 @@ import eu.kanade.tachiyomi.util.visible
class TabsAnimator(val tabs: TabLayout) { 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() private val interpolator = DecelerateInterpolator()
/**
* Duration of the animation.
*/
private val duration = 300L private val duration = 300L
/**
* Animation used to expand the tab layout.
*/
private val expandAnimation = object : Animation() { private val expandAnimation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation) { override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
tabs.layoutParams.height = (height * interpolatedTime).toInt() setHeight((tabsHeight * interpolatedTime).toInt())
tabs.requestLayout() tabs.requestLayout()
} }
@ -26,12 +45,24 @@ class TabsAnimator(val tabs: TabLayout) {
} }
} }
/**
* Animation used to collapse the tab layout.
*/
private val collapseAnimation = object : Animation() { 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) { override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
if (interpolatedTime == 1f) { if (interpolatedTime == 0f) {
startHeight = tabs.height
} else if (interpolatedTime == 1f) {
tabs.gone() tabs.gone()
} else { } else {
tabs.layoutParams.height = (height * (1 - interpolatedTime)).toInt() setHeight((startHeight * (1 - interpolatedTime)).toInt())
tabs.requestLayout() tabs.requestLayout()
} }
} }
@ -46,29 +77,75 @@ class TabsAnimator(val tabs: TabLayout) {
collapseAnimation.interpolator = interpolator collapseAnimation.interpolator = interpolator
expandAnimation.duration = duration expandAnimation.duration = duration
expandAnimation.interpolator = interpolator expandAnimation.interpolator = interpolator
}
fun expand() { isLastStateShown = tabs.visibility == View.VISIBLE
tabs.viewTreeObserver.addOnGlobalLayoutListener(
object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (tabs.height > 0) {
tabs.viewTreeObserver.removeOnGlobalLayoutListener(this)
// 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() tabs.visible()
if (measure() && tabs.measuredHeight != height) {
tabs.startAnimation(expandAnimation)
}
}
fun collapse() {
if (measure() && tabs.measuredHeight != 0) {
tabs.startAnimation(collapseAnimation)
} else { } else {
setHeight(0)
tabs.gone() 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 { private fun setHeight(newHeight: Int) {
if (height > 0) return true tabs.layoutParams.height = newHeight
height = tabs.measuredHeight
return height > 0
} }
/**
* 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
} }

View file

@ -24,7 +24,6 @@
android:id="@+id/tabs" android:id="@+id/tabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
android:theme="@style/Theme.ActionBar.Tab" android:theme="@style/Theme.ActionBar.Tab"
app:tabIndicatorColor="@android:color/white" app:tabIndicatorColor="@android:color/white"
app:tabGravity="center" app:tabGravity="center"