Apply system animation scale to parts of Tachiyomi that don't respect it by default (#5794)
* Add initial code for scaling animations, apply scale to reader nav overlay * Rename extension function, apply system animator scale to ActionToolbar * Apply system animator scale to expanding manga cover animation * Apply system animator scale to image crossfade (also disables animated covers when browsing) * Add documentation, make MotionScene Transition comment a bit more clear * Disable animated covers in MangaInfoHeaderAdapter if animator duration scale is 0 * Disable animated covers in Library if animator duration scale is 0 * Convert loadAny listener to extension function (cherry picked from commit df683375b1d7a15c03d315e85d4a0327b49f8ceb) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt
This commit is contained in:
parent
e36957f00b
commit
cfa6c180e7
@ -45,6 +45,7 @@ import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
|||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import exh.debug.DebugToggles
|
import exh.debug.DebugToggles
|
||||||
import exh.log.CrashlyticsPrinter
|
import exh.log.CrashlyticsPrinter
|
||||||
@ -156,7 +157,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
add(MangaCoverFetcher())
|
add(MangaCoverFetcher())
|
||||||
}
|
}
|
||||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
||||||
crossfade(300)
|
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,12 @@ import android.view.View
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.clear
|
import coil.clear
|
||||||
import coil.loadAny
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
|
import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
|
||||||
import eu.kanade.tachiyomi.util.isLocal
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
|
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
@ -86,7 +86,7 @@ class LibraryComfortableGridHolder(
|
|||||||
|
|
||||||
// Update the cover.
|
// Update the cover.
|
||||||
binding.thumbnail.clear()
|
binding.thumbnail.clear()
|
||||||
binding.thumbnail.loadAny(item.manga)
|
binding.thumbnail.loadAnyAutoPause(item.manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
@ -4,12 +4,12 @@ import android.view.View
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.clear
|
import coil.clear
|
||||||
import coil.loadAny
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
|
import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
|
||||||
import eu.kanade.tachiyomi.util.isLocal
|
import eu.kanade.tachiyomi.util.isLocal
|
||||||
|
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import reactivecircus.flowbinding.android.view.clicks
|
import reactivecircus.flowbinding.android.view.clicks
|
||||||
@ -82,7 +82,7 @@ class LibraryCompactGridHolder(
|
|||||||
|
|
||||||
// Update the cover.
|
// Update the cover.
|
||||||
binding.thumbnail.clear()
|
binding.thumbnail.clear()
|
||||||
binding.thumbnail.loadAny(item.manga)
|
binding.thumbnail.loadAnyAutoPause(item.manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
@ -8,7 +8,6 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.loadAny
|
|
||||||
import coil.target.ImageViewTarget
|
import coil.target.ImageViewTarget
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
@ -24,7 +23,9 @@ import eu.kanade.tachiyomi.source.online.MetadataSource
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
|
import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
|
||||||
import eu.kanade.tachiyomi.util.view.setChips
|
import eu.kanade.tachiyomi.util.view.setChips
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
@ -135,6 +136,12 @@ class MangaInfoHeaderAdapter(
|
|||||||
|
|
||||||
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
fun bind() {
|
fun bind() {
|
||||||
|
val headerTransition = binding.root.getTransition(R.id.manga_info_header_transition)
|
||||||
|
headerTransition.applySystemAnimatorScale(view.context)
|
||||||
|
|
||||||
|
val summaryTransition = binding.mangaSummarySection.getTransition(R.id.manga_summary_section_transition)
|
||||||
|
summaryTransition.applySystemAnimatorScale(view.context)
|
||||||
|
|
||||||
// For rounded corners
|
// For rounded corners
|
||||||
binding.mangaCover.clipToOutline = true
|
binding.mangaCover.clipToOutline = true
|
||||||
|
|
||||||
@ -362,8 +369,8 @@ class MangaInfoHeaderAdapter(
|
|||||||
setFavoriteButtonState(manga.favorite)
|
setFavoriteButtonState(manga.favorite)
|
||||||
|
|
||||||
// Set cover if changed.
|
// Set cover if changed.
|
||||||
binding.backdrop.loadAny(manga)
|
binding.backdrop.loadAnyAutoPause(manga)
|
||||||
binding.mangaCover.loadAny(manga) {
|
binding.mangaCover.loadAnyAutoPause(manga) {
|
||||||
listener(
|
listener(
|
||||||
onSuccess = { request, _ ->
|
onSuccess = { request, _ ->
|
||||||
(request.target as? ImageViewTarget)?.drawable?.let { drawable ->
|
(request.target as? ImageViewTarget)?.drawable?.let { drawable ->
|
||||||
|
@ -80,6 +80,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
|||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||||
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
||||||
import eu.kanade.tachiyomi.util.system.getThemeColor
|
import eu.kanade.tachiyomi.util.system.getThemeColor
|
||||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
@ -949,6 +950,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
|
|
||||||
if (animate) {
|
if (animate) {
|
||||||
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
|
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
|
||||||
|
toolbarAnimation.applySystemAnimatorScale(this)
|
||||||
toolbarAnimation.setAnimationListener(
|
toolbarAnimation.setAnimationListener(
|
||||||
object : SimpleAnimationListener() {
|
object : SimpleAnimationListener() {
|
||||||
override fun onAnimationStart(animation: Animation) {
|
override fun onAnimationStart(animation: Animation) {
|
||||||
@ -970,6 +972,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
}
|
}
|
||||||
|
|
||||||
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
|
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
|
||||||
|
bottomAnimation.applySystemAnimatorScale(this)
|
||||||
binding.readerMenuBottom.startAnimation(bottomAnimation)
|
binding.readerMenuBottom.startAnimation(bottomAnimation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -984,6 +987,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
|
|
||||||
if (animate) {
|
if (animate) {
|
||||||
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
|
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
|
||||||
|
toolbarAnimation.applySystemAnimatorScale(this)
|
||||||
toolbarAnimation.setAnimationListener(
|
toolbarAnimation.setAnimationListener(
|
||||||
object : SimpleAnimationListener() {
|
object : SimpleAnimationListener() {
|
||||||
override fun onAnimationEnd(animation: Animation) {
|
override fun onAnimationEnd(animation: Animation) {
|
||||||
@ -1004,6 +1008,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
|
|||||||
}
|
}
|
||||||
|
|
||||||
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom)
|
val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom)
|
||||||
|
bottomAnimation.applySystemAnimatorScale(this)
|
||||||
binding.readerMenuBottom.startAnimation(bottomAnimation)
|
binding.readerMenuBottom.startAnimation(bottomAnimation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package eu.kanade.tachiyomi.util.system
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import androidx.constraintlayout.motion.widget.MotionScene.Transition
|
||||||
|
|
||||||
|
/** Scale the duration of this [Animation] by [Context.animatorDurationScale] */
|
||||||
|
fun Animation.applySystemAnimatorScale(context: Context) {
|
||||||
|
this.duration = (this.duration * context.animatorDurationScale).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Scale the duration of this [Transition] by [Context.animatorDurationScale] */
|
||||||
|
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)
|
||||||
|
}
|
@ -18,6 +18,7 @@ import android.net.ConnectivityManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Display
|
import android.view.Display
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -203,6 +204,12 @@ val Context.displayCompat: Display?
|
|||||||
getSystemService<WindowManager>()?.defaultDisplay
|
getSystemService<WindowManager>()?.defaultDisplay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets the duration multiplier for general animations on the device
|
||||||
|
* @see Settings.Global.ANIMATOR_DURATION_SCALE
|
||||||
|
*/
|
||||||
|
val Context.animatorDurationScale: Float
|
||||||
|
get() = Settings.Global.getFloat(this.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience method to acquire a partial wake lock.
|
* Convenience method to acquire a partial wake lock.
|
||||||
*/
|
*/
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
package eu.kanade.tachiyomi.util.view
|
package eu.kanade.tachiyomi.util.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.imageLoader
|
||||||
|
import coil.loadAny
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.target.ImageViewTarget
|
||||||
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,3 +27,30 @@ fun ImageView.setVectorCompat(@DrawableRes drawable: Int, @AttrRes tint: Int? =
|
|||||||
}
|
}
|
||||||
setImageDrawable(vector)
|
setImageDrawable(vector)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the image referenced by [data] and set it on this [ImageView],
|
||||||
|
* and if the image is animated, this will also disable that animation
|
||||||
|
* if [Context.animatorDurationScale] is 0
|
||||||
|
*/
|
||||||
|
fun ImageView.loadAnyAutoPause(
|
||||||
|
data: Any?,
|
||||||
|
loader: ImageLoader = context.imageLoader,
|
||||||
|
builder: ImageRequest.Builder.() -> Unit = {}
|
||||||
|
) {
|
||||||
|
this.loadAny(data, loader) {
|
||||||
|
// Build the original request so we can add on our success listener
|
||||||
|
val originalBuild = apply(builder).build()
|
||||||
|
listener(
|
||||||
|
onSuccess = { request, metadata ->
|
||||||
|
(request.target as? ImageViewTarget)?.drawable.let {
|
||||||
|
if (it is Animatable && context.animatorDurationScale == 0f) it.stop()
|
||||||
|
}
|
||||||
|
originalBuild.listener?.onSuccess(request, metadata)
|
||||||
|
},
|
||||||
|
onStart = { request -> originalBuild.listener?.onStart(request) },
|
||||||
|
onCancel = { request -> originalBuild.listener?.onCancel(request) },
|
||||||
|
onError = { request, throwable -> originalBuild.listener?.onError(request, throwable) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ import androidx.appcompat.view.ActionMode
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.databinding.ActionToolbarBinding
|
import eu.kanade.tachiyomi.databinding.ActionToolbarBinding
|
||||||
|
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||||
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,6 +51,7 @@ class ActionToolbar @JvmOverloads constructor(context: Context, attrs: Attribute
|
|||||||
|
|
||||||
binding.actionToolbar.isVisible = true
|
binding.actionToolbar.isVisible = true
|
||||||
val bottomAnimation = AnimationUtils.loadAnimation(context, R.anim.enter_from_bottom)
|
val bottomAnimation = AnimationUtils.loadAnimation(context, R.anim.enter_from_bottom)
|
||||||
|
bottomAnimation.applySystemAnimatorScale(context)
|
||||||
binding.actionToolbar.startAnimation(bottomAnimation)
|
binding.actionToolbar.startAnimation(bottomAnimation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +60,7 @@ class ActionToolbar @JvmOverloads constructor(context: Context, attrs: Attribute
|
|||||||
*/
|
*/
|
||||||
fun hide() {
|
fun hide() {
|
||||||
val bottomAnimation = AnimationUtils.loadAnimation(context, R.anim.exit_to_bottom)
|
val bottomAnimation = AnimationUtils.loadAnimation(context, R.anim.exit_to_bottom)
|
||||||
|
bottomAnimation.applySystemAnimatorScale(context)
|
||||||
bottomAnimation.setAnimationListener(
|
bottomAnimation.setAnimationListener(
|
||||||
object : SimpleAnimationListener() {
|
object : SimpleAnimationListener() {
|
||||||
override fun onAnimationEnd(animation: Animation) {
|
override fun onAnimationEnd(animation: Animation) {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<Transition
|
<Transition
|
||||||
motion:constraintSetEnd="@+id/end"
|
motion:constraintSetEnd="@+id/end"
|
||||||
motion:constraintSetStart="@id/start"
|
motion:constraintSetStart="@id/start"
|
||||||
|
android:id="@+id/manga_info_header_transition"
|
||||||
motion:duration="@android:integer/config_mediumAnimTime">
|
motion:duration="@android:integer/config_mediumAnimTime">
|
||||||
<KeyFrameSet></KeyFrameSet>
|
<KeyFrameSet></KeyFrameSet>
|
||||||
<OnClick motion:targetId="@+id/manga_cover" />
|
<OnClick motion:targetId="@+id/manga_cover" />
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<Transition
|
<Transition
|
||||||
motion:constraintSetEnd="@+id/end"
|
motion:constraintSetEnd="@+id/end"
|
||||||
motion:constraintSetStart="@id/start"
|
motion:constraintSetStart="@id/start"
|
||||||
|
android:id="@+id/manga_summary_section_transition"
|
||||||
motion:duration="1">
|
motion:duration="1">
|
||||||
<KeyFrameSet></KeyFrameSet>
|
<KeyFrameSet></KeyFrameSet>
|
||||||
<OnClick motion:clickAction="toggle" />
|
<OnClick motion:clickAction="toggle" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user