mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-19 04:49:17 +01:00
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:
parent
914b686c8e
commit
da16110e1c
@ -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
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
interface NoToolbarElevationController
|
||||
interface NoAppBarElevationController
|
@ -1,3 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.base.controller
|
||||
|
||||
interface ToolbarLiftOnScrollController
|
@ -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()
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
is ToolbarLiftOnScrollController -> {
|
||||
binding.appbar.enableElevation(true)
|
||||
}
|
||||
else -> {
|
||||
binding.appbar.enableElevation(false)
|
||||
if (!isTablet()) {
|
||||
// Save lift state
|
||||
if (isPush) {
|
||||
if (router.backstackSize > 1) {
|
||||
// Save lift state
|
||||
from?.let {
|
||||
backstackLiftState[it.instanceId] = binding.appbar.isLifted
|
||||
}
|
||||
} else {
|
||||
backstackLiftState.clear()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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() }
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
updateBackgroundAlpha()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun enableElevation(liftOnScroll: Boolean) {
|
||||
setElevation(liftOnScroll)
|
||||
}
|
||||
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
|
||||
|
||||
private fun setElevation(liftOnScroll: Boolean) {
|
||||
stateListAnimator = origStateAnimator
|
||||
isLiftOnScroll = liftOnScroll
|
||||
}
|
||||
|
||||
fun disableElevation() {
|
||||
stateListAnimator = StateListAnimator().apply {
|
||||
val objAnimator = ObjectAnimator.ofFloat(this, "elevation", 0f)
|
||||
|
||||
// Enabled and collapsible, but not collapsed means not elevated
|
||||
addState(
|
||||
intArrayOf(android.R.attr.enabled, R.attr.state_collapsible, -R.attr.state_collapsed),
|
||||
objAnimator
|
||||
)
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user