Browse Source

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
Hunter Nickel 3 years ago
parent
commit
df683375b1

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/App.kt

@@ -31,6 +31,7 @@ import eu.kanade.tachiyomi.data.preference.asImmediateFlow
 import eu.kanade.tachiyomi.network.NetworkHelper
 import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
 import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
+import eu.kanade.tachiyomi.util.system.animatorDurationScale
 import eu.kanade.tachiyomi.util.system.notification
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -125,7 +126,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
                 add(MangaCoverFetcher())
             }
             okHttpClient(Injekt.get<NetworkHelper>().coilClient)
-            crossfade(300)
+            crossfade((300 * [email protected]).toInt())
             allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
         }.build()
     }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt

@@ -4,11 +4,11 @@ import android.view.View
 import androidx.core.view.isVisible
 import androidx.recyclerview.widget.RecyclerView
 import coil.clear
-import coil.loadAny
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.IFlexible
 import eu.kanade.tachiyomi.databinding.SourceComfortableGridItemBinding
 import eu.kanade.tachiyomi.util.isLocal
+import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
 
 /**
  * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@@ -57,6 +57,6 @@ class LibraryComfortableGridHolder(
 
         // Update the cover.
         binding.thumbnail.clear()
-        binding.thumbnail.loadAny(item.manga)
+        binding.thumbnail.loadAnyAutoPause(item.manga)
     }
 }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt

@@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.ui.library
 import android.view.View
 import androidx.core.view.isVisible
 import coil.clear
-import coil.loadAny
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.kanade.tachiyomi.databinding.SourceCompactGridItemBinding
 import eu.kanade.tachiyomi.util.isLocal
+import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
 
 /**
  * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@@ -55,6 +55,6 @@ open class LibraryCompactGridHolder(
 
         // Update the cover.
         binding.thumbnail.clear()
-        binding.thumbnail.loadAny(item.manga)
+        binding.thumbnail.loadAnyAutoPause(item.manga)
     }
 }

+ 10 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt

@@ -7,7 +7,6 @@ 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
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import eu.kanade.tachiyomi.R
@@ -20,7 +19,9 @@ 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.applySystemAnimatorScale
 import eu.kanade.tachiyomi.util.system.copyToClipboard
+import eu.kanade.tachiyomi.util.view.loadAnyAutoPause
 import eu.kanade.tachiyomi.util.view.setChips
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.merge
@@ -92,6 +93,12 @@ class MangaInfoHeaderAdapter(
 
     inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
         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
             binding.mangaCover.clipToOutline = true
 
@@ -278,8 +285,8 @@ class MangaInfoHeaderAdapter(
             setFavoriteButtonState(manga.favorite)
 
             // Set cover if changed.
-            binding.backdrop.loadAny(manga)
-            binding.mangaCover.loadAny(manga) {
+            binding.backdrop.loadAnyAutoPause(manga)
+            binding.mangaCover.loadAnyAutoPause(manga) {
                 listener(
                     onSuccess = { request, _ ->
                         (request.target as? ImageViewTarget)?.drawable?.let { drawable ->

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

@@ -66,6 +66,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
 import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
 import eu.kanade.tachiyomi.util.storage.getUriCompat
 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.getThemeColor
 import eu.kanade.tachiyomi.util.system.hasDisplayCutout
@@ -528,6 +529,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
 
             if (animate) {
                 val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
+                toolbarAnimation.applySystemAnimatorScale(this)
                 toolbarAnimation.setAnimationListener(
                     object : SimpleAnimationListener() {
                         override fun onAnimationStart(animation: Animation) {
@@ -539,6 +541,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
                 binding.toolbar.startAnimation(toolbarAnimation)
 
                 val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_bottom)
+                bottomAnimation.applySystemAnimatorScale(this)
                 binding.readerMenuBottom.startAnimation(bottomAnimation)
             }
 
@@ -553,6 +556,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
 
             if (animate) {
                 val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
+                toolbarAnimation.applySystemAnimatorScale(this)
                 toolbarAnimation.setAnimationListener(
                     object : SimpleAnimationListener() {
                         override fun onAnimationEnd(animation: Animation) {
@@ -563,6 +567,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
                 binding.toolbar.startAnimation(toolbarAnimation)
 
                 val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom)
+                bottomAnimation.applySystemAnimatorScale(this)
                 binding.readerMenuBottom.startAnimation(bottomAnimation)
             }
 

+ 16 - 0
app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt

@@ -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)
+}

+ 7 - 0
app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt

@@ -18,6 +18,7 @@ import android.net.ConnectivityManager
 import android.net.Uri
 import android.os.Build
 import android.os.PowerManager
+import android.provider.Settings
 import android.util.TypedValue
 import android.view.Display
 import android.view.View
@@ -203,6 +204,12 @@ val Context.displayCompat: Display?
         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.
  */

+ 35 - 0
app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt

@@ -1,9 +1,17 @@
 package eu.kanade.tachiyomi.util.view
 
+import android.content.Context
+import android.graphics.drawable.Animatable
 import android.widget.ImageView
 import androidx.annotation.AttrRes
 import androidx.annotation.DrawableRes
 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
 
 /**
@@ -19,3 +27,30 @@ fun ImageView.setVectorCompat(@DrawableRes drawable: Int, @AttrRes tint: Int? =
     }
     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) }
+        )
+    }
+}

+ 3 - 0
app/src/main/java/eu/kanade/tachiyomi/widget/ActionToolbar.kt

@@ -13,6 +13,7 @@ import androidx.appcompat.view.ActionMode
 import androidx.core.view.isVisible
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.databinding.ActionToolbarBinding
+import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
 import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
 
 /**
@@ -50,6 +51,7 @@ class ActionToolbar @JvmOverloads constructor(context: Context, attrs: Attribute
 
         binding.actionToolbar.isVisible = true
         val bottomAnimation = AnimationUtils.loadAnimation(context, R.anim.enter_from_bottom)
+        bottomAnimation.applySystemAnimatorScale(context)
         binding.actionToolbar.startAnimation(bottomAnimation)
     }
 
@@ -58,6 +60,7 @@ class ActionToolbar @JvmOverloads constructor(context: Context, attrs: Attribute
      */
     fun hide() {
         val bottomAnimation = AnimationUtils.loadAnimation(context, R.anim.exit_to_bottom)
+        bottomAnimation.applySystemAnimatorScale(context)
         bottomAnimation.setAnimationListener(
             object : SimpleAnimationListener() {
                 override fun onAnimationEnd(animation: Animation) {

+ 1 - 0
app/src/main/res/xml/manga_info_header_scene.xml

@@ -5,6 +5,7 @@
     <Transition
         motion:constraintSetEnd="@+id/end"
         motion:constraintSetStart="@id/start"
+        android:id="@+id/manga_info_header_transition"
         motion:duration="@android:integer/config_mediumAnimTime">
         <KeyFrameSet></KeyFrameSet>
         <OnClick motion:targetId="@+id/manga_cover" />

+ 1 - 0
app/src/main/res/xml/manga_summary_section_scene.xml

@@ -5,6 +5,7 @@
     <Transition
         motion:constraintSetEnd="@+id/end"
         motion:constraintSetStart="@id/start"
+        android:id="@+id/manga_summary_section_transition"
         motion:duration="1">
         <KeyFrameSet></KeyFrameSet>
         <OnClick motion:clickAction="toggle" />