Edge-to-edge manga details view (#5613)

* Prepare for edge-to-edge MangaController

* Fix derpy liftToScroll with our own implementation

* Edge-to-edge MangaController

Except when legacy blue theme is used.

* Save app bar lift state for controller backstack

* Fix expanded cover position after the view recycled

* Handle overlap changes when incognito mode disabled

* Tablet fixes

* Revert "Handle overlap changes when incognito mode disabled"

This reverts commit 1f492449

Breaks on rotation changes.

* Fix MangaController's swipe refresh position

* All controllers are now doing lift app bar on scroll by default

They are already doing that before so this pretty much just a cleanups.

* TachiyomiCoordinatorLayout: Support ViewPager for app bar lift state check

I'm willing to revert this if this minute detail solution is deemed too hacky xD

* Fix app bar not lifted when scrolled without fling

* Save app bar lift state across configuration changes

* Fix MangaController's swipe refresh position after configuration change

* TachiyomiCoordinatorLayout: Update ViewPager reference when controller is changed
This commit is contained in:
Ivan Iskandar 2021-08-19 20:12:52 +07:00 committed by GitHub
parent 914b686c8e
commit da16110e1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 490 additions and 90 deletions

View File

@ -7,6 +7,7 @@ import androidx.core.net.toUri
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.system.toast
fun Router.popControllerWithTag(tag: String): Boolean {
@ -41,3 +42,10 @@ fun Controller.openInBrowser(url: String) {
activity?.toast(e.message)
}
}
/**
* Returns [MainActivity]'s app bar height
*/
fun Controller.getMainAppBarHeight(): Int {
return (activity as? MainActivity)?.binding?.appbar?.measuredHeight ?: 0
}

View File

@ -1,3 +1,3 @@
package eu.kanade.tachiyomi.ui.base.controller
interface NoToolbarElevationController
interface NoAppBarElevationController

View File

@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.ui.base.controller
interface ToolbarLiftOnScrollController

View File

@ -34,7 +34,6 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.preference.DSL
@ -49,8 +48,7 @@ import uy.kohesive.injekt.injectLazy
@SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) :
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
ToolbarLiftOnScrollController {
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle) {
private val preferences: PreferencesHelper by injectLazy()

View File

@ -245,12 +245,7 @@ class LibraryController(
}
tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible ->
val tabAnimator = (activity as? MainActivity)?.tabAnimator
if (visible) {
tabAnimator?.expand()
} else {
tabAnimator?.collapse()
}
tabs.isVisible = visible
}
mangaCountVisibilitySubscription?.unsubscribe()
mangaCountVisibilitySubscription = mangaCountVisibilityRelay.subscribe {

View File

@ -34,6 +34,7 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
@ -42,10 +43,9 @@ import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.ui.base.activity.BaseViewBindingActivity
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NoAppBarElevationController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
@ -61,6 +61,7 @@ import eu.kanade.tachiyomi.ui.setting.SettingsMainController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
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
@ -85,7 +86,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
}
}
lateinit var tabAnimator: ViewHeightAnimator
private var bottomNavAnimator: ViewHeightAnimator? = null
private var isConfirmingExit: Boolean = false
@ -93,6 +93,11 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
private var fixedViewsToBottom = mutableMapOf<View, AppBarLayout.OnOffsetChangedListener>()
/**
* App bar lift state for backstack
*/
private val backstackLiftState = mutableMapOf<String, Boolean>()
// To be checked by splash screen. If true then splash screen will be removed.
var ready = false
@ -117,11 +122,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
// Draw edge-to-edge
WindowCompat.setDecorFitsSystemWindows(window, false)
binding.appbar.applyInsetter {
type(navigationBars = true, statusBars = true) {
padding(left = true, top = true, right = true)
}
}
binding.fabLayout.rootFab.applyInsetter {
type(navigationBars = true) {
margin()
@ -140,8 +140,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
}
setSplashScreenExitAnimation(splashScreen)
tabAnimator = ViewHeightAnimator(binding.tabs, 0L)
if (binding.bottomNav != null) {
bottomNavAnimator = ViewHeightAnimator(binding.bottomNav!!)
@ -218,7 +216,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
container: ViewGroup,
handler: ControllerChangeHandler
) {
syncActivityViewWithController(to, from)
syncActivityViewWithController(to, from, isPush)
}
override fun onChangeCompleted(
@ -504,7 +502,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
router.setRoot(controller.withFadeTransaction().tag(id.toString()))
}
private fun syncActivityViewWithController(to: Controller?, from: Controller? = null) {
private fun syncActivityViewWithController(to: Controller?, from: Controller? = null, isPush: Boolean = true) {
if (from is DialogController || to is DialogController) {
return
}
@ -529,12 +527,11 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
from.cleanupTabs(binding.tabs)
}
if (to is TabbedController) {
tabAnimator.expand()
to.configureTabs(binding.tabs)
} else {
tabAnimator.collapse()
binding.tabs.setupWithViewPager(null)
}
binding.tabs.isVisible = to is TabbedController
if (from is FabController) {
binding.fabLayout.rootFab.isVisible = false
@ -545,16 +542,32 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
to.configureFab(binding.fabLayout.rootFab)
}
when (to) {
is NoToolbarElevationController -> {
binding.appbar.disableElevation()
if (!isTablet()) {
// Save lift state
if (isPush) {
if (router.backstackSize > 1) {
// Save lift state
from?.let {
backstackLiftState[it.instanceId] = binding.appbar.isLifted
}
is ToolbarLiftOnScrollController -> {
binding.appbar.enableElevation(true)
} else {
backstackLiftState.clear()
}
else -> {
binding.appbar.enableElevation(false)
binding.appbar.isLifted = false
} else {
to?.let {
binding.appbar.isLifted = backstackLiftState.getOrElse(it.instanceId) { false }
}
from?.let {
backstackLiftState.remove(it.instanceId)
}
}
binding.root.isLiftAppBarOnScroll = to !is NoAppBarElevationController
binding.appbar.isTransparentWhenNotLifted = to is MangaController &&
preferences.appTheme().get() != PreferenceValues.AppTheme.BLUE
binding.controllerContainer.overlapHeader = to is MangaController
}
}

View File

@ -13,16 +13,21 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.FloatRange
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.os.bundleOf
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.children
import androidx.core.view.doOnLayout
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import coil.imageLoader
import coil.request.ImageRequest
import com.bluelinelabs.conductor.ControllerChangeHandler
@ -51,7 +56,7 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
@ -89,6 +94,7 @@ import eu.kanade.tachiyomi.util.view.snack
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.recyclerview.scrollEvents
import reactivecircus.flowbinding.recyclerview.scrollStateChanges
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
import timber.log.Timber
import uy.kohesive.injekt.Injekt
@ -99,7 +105,6 @@ import kotlin.math.min
class MangaController :
NucleusController<MangaControllerBinding, MangaPresenter>,
ToolbarLiftOnScrollController,
FabController,
ActionMode.Callback,
FlexibleAdapter.OnItemClickListener,
@ -254,6 +259,37 @@ class MangaController :
updateToolbarTitleAlpha()
}
}
it.scrollStateChanges()
.onEach { _ ->
// Disable swipe refresh when view is not at the top
val firstPos = (it.layoutManager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition()
binding.swipeRefresh.isEnabled = firstPos <= 0
}
.launchIn(viewScope)
binding.fastScroller.doOnLayout { scroller ->
scroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = getMainAppBarHeight()
}
scroller.applyInsetter {
type(navigationBars = true) {
margin()
}
}
}
binding.swipeRefresh.doOnLayout { swipeRefresh ->
swipeRefresh as SwipeRefreshLayout
swipeRefresh.setOnApplyWindowInsetsListener { _, windowInsets ->
val topStatusBarInset = WindowInsetsCompat.toWindowInsetsCompat(windowInsets)
.getInsets(WindowInsetsCompat.Type.statusBars())
.top
swipeRefresh.setProgressViewEndTarget(false, getMainAppBarHeight() + topStatusBarInset)
windowInsets
}
}
}
// Tablet layout
binding.infoRecycler?.let {

View File

@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.ui.manga.info
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import coil.loadAny
import coil.target.ImageViewTarget
@ -16,6 +18,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.view.setChips
@ -47,6 +50,7 @@ class MangaInfoHeaderAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
binding = MangaInfoHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
updateCoverPosition()
return HeaderViewHolder(binding.root)
}
@ -75,6 +79,15 @@ class MangaInfoHeaderAdapter(
notifyDataSetChanged()
}
private fun updateCoverPosition() {
val appBarHeight = controller.getMainAppBarHeight()
binding.mangaCover.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin += appBarHeight
}
binding.root.getConstraintSet(R.id.end)
?.setMargin(R.id.manga_cover, ConstraintLayout.LayoutParams.TOP, appBarHeight)
}
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
fun bind() {
// For rounded corners

View File

@ -5,7 +5,7 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NoAppBarElevationController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.more.licenses.LicensesController
@ -25,7 +25,7 @@ import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class AboutController : SettingsController(), NoToolbarElevationController {
class AboutController : SettingsController(), NoAppBarElevationController {
private val updateChecker by lazy { AppUpdateChecker() }

View File

@ -9,7 +9,7 @@ import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NoAppBarElevationController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.category.CategoryController
@ -41,7 +41,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class MoreController :
SettingsController(),
RootController,
NoToolbarElevationController {
NoAppBarElevationController {
private val downloadManager: DownloadManager by injectLazy()
private var isDownloading: Boolean = false

View File

@ -9,6 +9,7 @@ import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.MenuRes
import androidx.annotation.StringRes
@ -16,8 +17,11 @@ import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.TooltipCompat
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.core.view.descendants
import androidx.core.view.forEach
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
@ -214,3 +218,40 @@ fun RecyclerView.onAnimationsFinished(callback: (RecyclerView) -> Unit) = post(
}
}
)
/**
* Returns this ViewGroup's first child of specified class
*/
inline fun <reified T> ViewGroup.findChild(): T? {
return children.find { it is T } as? T
}
/**
* Returns this ViewGroup's first descendant of specified class
*/
inline fun <reified T> ViewGroup.findDescendant(): T? {
return descendants.find { it is T } as? T
}
/**
* Returns the active child view of a ViewPager according to the LayoutParams
*/
fun ViewPager.getActivePageView(): View? {
if (null == adapter || adapter?.count == 0 || childCount == 0) {
return null
}
val positionField = ViewPager.LayoutParams::class.java.getDeclaredField("position")
positionField.isAccessible = true
return children.find { child ->
val layoutParams = child.layoutParams as ViewPager.LayoutParams
try {
if (!layoutParams.isDecor && positionField.getInt(layoutParams) == currentItem) {
return@find true
}
} catch (e: NoSuchFieldException) {
} catch (e: IllegalAccessException) {
}
false
}
}

View File

@ -1,47 +1,87 @@
package eu.kanade.tachiyomi.widget
import android.animation.ObjectAnimator
import android.animation.StateListAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.R
import com.google.android.material.animation.AnimationUtils
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import eu.kanade.tachiyomi.R
class ElevationAppBarLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : AppBarLayout(context, attrs) {
private var origStateAnimator: StateListAnimator? = null
private var lifted = true
private var transparent = false
init {
origStateAnimator = stateListAnimator
private val toolbar by lazy { findViewById<MaterialToolbar>(R.id.toolbar) }
private var elevationAnimator: ValueAnimator? = null
private var backgroundAlphaAnimator: ValueAnimator? = null
var isTransparentWhenNotLifted = false
set(value) {
if (field != value) {
field = value
updateBackgroundAlpha()
}
}
fun enableElevation(liftOnScroll: Boolean) {
setElevation(liftOnScroll)
/**
* Disabled. Lift on scroll is handled manually with [TachiyomiCoordinatorLayout]
*/
override fun isLiftOnScroll(): Boolean = false
override fun isLifted(): Boolean = lifted
override fun setLifted(lifted: Boolean): Boolean {
return if (this.lifted != lifted) {
this.lifted = lifted
val from = elevation
val to = if (lifted) {
resources.getDimension(R.dimen.design_appbar_elevation)
} else {
0F
}
private fun setElevation(liftOnScroll: Boolean) {
stateListAnimator = origStateAnimator
isLiftOnScroll = liftOnScroll
elevationAnimator?.cancel()
elevationAnimator = ValueAnimator.ofFloat(from, to).apply {
duration = resources.getInteger(R.integer.app_bar_elevation_anim_duration).toLong()
interpolator = AnimationUtils.LINEAR_INTERPOLATOR
addUpdateListener {
elevation = it.animatedValue as Float
}
start()
}
fun disableElevation() {
stateListAnimator = StateListAnimator().apply {
val objAnimator = ObjectAnimator.ofFloat(this, "elevation", 0f)
updateBackgroundAlpha()
true
} else {
false
}
}
// Enabled and collapsible, but not collapsed means not elevated
addState(
intArrayOf(android.R.attr.enabled, R.attr.state_collapsible, -R.attr.state_collapsed),
objAnimator
)
private fun updateBackgroundAlpha() {
val newTransparent = if (lifted) false else isTransparentWhenNotLifted
if (transparent != newTransparent) {
transparent = newTransparent
val fromAlpha = if (transparent) 255 else 0
val toAlpha = if (transparent) 0 else 255
// Default enabled state
addState(intArrayOf(android.R.attr.enabled), objAnimator)
// Disabled state
addState(IntArray(0), objAnimator)
backgroundAlphaAnimator?.cancel()
backgroundAlphaAnimator = ValueAnimator.ofInt(fromAlpha, toAlpha).apply {
duration = resources.getInteger(R.integer.app_bar_elevation_anim_duration).toLong()
interpolator = AnimationUtils.LINEAR_INTERPOLATOR
addUpdateListener {
val alpha = it.animatedValue as Int
background.alpha = alpha
toolbar?.background?.alpha = alpha
statusBarForeground?.alpha = alpha
}
start()
}
}
}
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.widget
import android.view.View
import android.view.ViewGroup
import androidx.viewpager.widget.ViewPager
import com.nightlynexus.viewstatepageradapter.ViewStatePagerAdapter
import java.util.Stack
@ -22,7 +23,11 @@ abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() {
protected open fun recycleView(view: View, position: Int) {}
override fun createView(container: ViewGroup, position: Int): View {
val view = if (pool.isNotEmpty()) pool.pop() else createView(container)
val view = if (pool.isNotEmpty()) {
pool.pop().setViewPagerPositionParam(position)
} else {
createView(container)
}
bindView(view, position)
return view
}
@ -31,4 +36,25 @@ abstract class RecyclerViewPagerAdapter : ViewStatePagerAdapter() {
recycleView(view, position)
if (recycle) pool.push(view)
}
/**
* Making sure that this ViewPager child view has the correct "position" layout param
* after being recycled.
*/
private fun View.setViewPagerPositionParam(position: Int): View {
val params = layoutParams
if (params is ViewPager.LayoutParams) {
if (!params.isDecor) {
try {
val positionField = ViewPager.LayoutParams::class.java.getDeclaredField("position")
positionField.isAccessible = true
positionField.setInt(params, position)
layoutParams = params
} catch (e: NoSuchFieldException) {
} catch (e: IllegalAccessException) {
}
}
}
return this
}
}

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.widget
import android.content.Context
import android.util.AttributeSet
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.bluelinelabs.conductor.ChangeHandlerFrameLayout
/**
* [ChangeHandlerFrameLayout] with the ability to draw behind the header sibling in [CoordinatorLayout].
* The layout behavior of this view is set to [TachiyomiScrollingViewBehavior] and should not be changed.
*/
class TachiyomiChangeHandlerFrameLayout(
context: Context,
attrs: AttributeSet
) : ChangeHandlerFrameLayout(context, attrs), CoordinatorLayout.AttachedBehavior {
/**
* If true, this view will draw behind the header sibling.
*
* @see TachiyomiScrollingViewBehavior.shouldHeaderOverlap
*/
var overlapHeader = false
set(value) {
if (field != value) {
field = value
(layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = behavior.apply {
shouldHeaderOverlap = value
}
if (!value) {
// The behavior doesn't reset translationY when shouldHeaderOverlap is false
translationY = 0F
}
forceLayout()
}
}
override fun getBehavior() = TachiyomiScrollingViewBehavior()
}

View File

@ -0,0 +1,177 @@
package eu.kanade.tachiyomi.widget
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.coordinatorlayout.R
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.doOnLayout
import androidx.customview.view.AbsSavedState
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.bluelinelabs.conductor.ChangeHandlerFrameLayout
import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.util.system.isTablet
import eu.kanade.tachiyomi.util.view.findChild
import eu.kanade.tachiyomi.util.view.findDescendant
import eu.kanade.tachiyomi.util.view.getActivePageView
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.HierarchyChangeEvent
import reactivecircus.flowbinding.android.view.hierarchyChangeEvents
/**
* [CoordinatorLayout] with its own app bar lift state handler.
* This parent view checks for the app bar lift state from the following:
*
* 1. When nested scroll detected, lift state will be decided from the nested
* scroll target. (See [onNestedScroll])
*
* 2. When a descendant ViewPager active page is changed and the page contains RecyclerView,
* lift state will be decided from the said RecyclerView. (See [pageChangeListener])
*
*
* With those conditions, this view expects the following direct child:
*
* 1. An [AppBarLayout].
*
* 2. A [ChangeHandlerFrameLayout] that contains an optional [ViewPager].
*/
class TachiyomiCoordinatorLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.coordinatorLayoutStyle
) : CoordinatorLayout(context, attrs, defStyleAttr) {
/**
* Keep lifted state and do nothing on tablet UI
*/
private val isTablet = context.isTablet()
private var appBarLayout: AppBarLayout? = null
private var viewPager: ViewPager? = null
set(value) {
field?.removeOnPageChangeListener(pageChangeListener)
field = value
field?.addOnPageChangeListener(pageChangeListener)
}
private val pageChangeListener = object : ViewPager.SimpleOnPageChangeListener() {
override fun onPageScrollStateChanged(state: Int) {
// Wait until idle to make sure all the views laid out properly before checked
if (canLiftAppBarOnScroll && state == ViewPager.SCROLL_STATE_IDLE) {
appBarLayout?.isLifted = (viewPager?.getActivePageView() as? ViewGroup)
?.findDescendant<RecyclerView>()
?.canScrollVertically(-1) ?: false
}
}
}
/**
* If true, [AppBarLayout] child will be lifted on nested scroll.
*/
var isLiftAppBarOnScroll = true
/**
* Internal check
*/
private val canLiftAppBarOnScroll
get() = !isTablet && isLiftAppBarOnScroll
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed)
if (canLiftAppBarOnScroll) {
appBarLayout?.isLifted = dyConsumed != 0 || dyUnconsumed >= 0
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
appBarLayout = findChild()
viewPager = findChild<ChangeHandlerFrameLayout>()?.findDescendant()
// Updates ViewPager reference when controller is changed
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.let { scope ->
findChild<ChangeHandlerFrameLayout>()?.hierarchyChangeEvents()
?.onEach {
if (it is HierarchyChangeEvent.ChildRemoved) {
viewPager = (it.parent as? ViewGroup)?.findDescendant()
}
}
?.launchIn(scope)
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
appBarLayout = null
viewPager = null
}
override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState()
return if (superState != null) {
SavedState(superState).also {
it.appBarLifted = appBarLayout?.isLifted ?: false
}
} else {
superState
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is SavedState) {
super.onRestoreInstanceState(state.superState)
doOnLayout {
appBarLayout?.isLifted = state.appBarLifted
}
} else {
super.onRestoreInstanceState(state)
}
}
internal class SavedState : AbsSavedState {
var appBarLifted = false
constructor(superState: Parcelable) : super(superState)
constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
appBarLifted = source.readByte().toInt() == 1
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeByte((if (appBarLifted) 1 else 0).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)
}
}
}
}
}

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.widget
import com.google.android.material.appbar.AppBarLayout
/**
* [AppBarLayout.ScrollingViewBehavior] that lets the app bar overlaps the scrolling child.
*/
class TachiyomiScrollingViewBehavior : AppBarLayout.ScrollingViewBehavior() {
var shouldHeaderOverlap = false
override fun shouldHeaderOverlapScrollingChild(): Boolean {
return shouldHeaderOverlap
}
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_coordinator"
@ -15,6 +15,7 @@
android:id="@+id/appbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@ -88,7 +89,7 @@
app:layout_constraintStart_toEndOf="@+id/side_nav"
app:layout_constraintTop_toBottomOf="@+id/incognito_mode" />
<com.bluelinelabs.conductor.ChangeHandlerFrameLayout
<eu.kanade.tachiyomi.widget.TachiyomiChangeHandlerFrameLayout
android:id="@+id/controller_container"
android:layout_width="0dp"
android:layout_height="0dp"
@ -103,4 +104,4 @@
android:id="@+id/fab_layout"
layout="@layout/main_activity_fab" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_coordinator"
@ -7,11 +7,18 @@
android:layout_height="match_parent"
android:orientation="vertical">
<eu.kanade.tachiyomi.widget.TachiyomiChangeHandlerFrameLayout
android:id="@+id/controller_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<eu.kanade.tachiyomi.widget.ElevationAppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
android:fitsSystemWindows="true"
app:elevation="0dp"
app:statusBarForeground="?attr/colorToolbar">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
@ -23,7 +30,8 @@
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:visibility="gone" />
<FrameLayout
android:id="@+id/downloaded_only"
@ -63,12 +71,6 @@
</eu.kanade.tachiyomi.widget.ElevationAppBarLayout>
<com.bluelinelabs.conductor.ChangeHandlerFrameLayout
android:id="@+id/controller_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<include
android:id="@+id/fab_layout"
layout="@layout/main_activity_fab" />
@ -83,4 +85,4 @@
app:menu="@menu/main_nav"
tools:ignore="KeyboardInaccessibleWidget" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout>

View File

@ -25,17 +25,18 @@
<View
android:id="@+id/backdrop_overlay"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_height="0dp"
android:background="@drawable/manga_info_gradient"
android:backgroundTint="?android:attr/colorBackground"
app:layout_constraintBottom_toBottomOf="@+id/backdrop" />
app:layout_constraintBottom_toBottomOf="@+id/backdrop"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/manga_cover"
android:layout_width="100dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/rounded_rectangle"
android:contentDescription="@string/description_cover"
android:maxWidth="100dp"

View File

@ -36,7 +36,7 @@
<!-- Themes -->
<item name="android:windowLightStatusBar">@bool/lightStatusBar</item>
<item name="android:statusBarColor">?attr/colorSurface</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@color/surface_amoled</item>
<item name="android:navigationBarDividerColor" tools:targetApi="o_mr1">@null</item>
<item name="android:enforceNavigationBarContrast" tools:targetApi="Q">false</item>
@ -186,7 +186,6 @@
<!-- Status/Navigation bar -->
<item name="android:windowLightStatusBar" tools:targetApi="m">?attr/lightSystemBarsOnPrimary</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">?attr/lightSystemBarsOnPrimary</item>
<item name="android:statusBarColor">?attr/colorPrimary</item>
<item name="android:navigationBarColor">?attr/colorPrimary</item>
</style>