From df683375b1d7a15c03d315e85d4a0327b49f8ceb Mon Sep 17 00:00:00 2001 From: Hunter Nickel Date: Fri, 27 Aug 2021 06:44:09 -0600 Subject: [PATCH] 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 --- app/src/main/java/eu/kanade/tachiyomi/App.kt | 3 +- .../library/LibraryComfortableGridHolder.kt | 4 +-- .../ui/library/LibraryCompactGridHolder.kt | 4 +-- .../ui/manga/info/MangaInfoHeaderAdapter.kt | 13 +++++-- .../tachiyomi/ui/reader/ReaderActivity.kt | 5 +++ .../util/system/AnimationExtensions.kt | 16 +++++++++ .../util/system/ContextExtensions.kt | 7 ++++ .../util/view/ImageViewExtensions.kt | 35 +++++++++++++++++++ .../kanade/tachiyomi/widget/ActionToolbar.kt | 3 ++ .../main/res/xml/manga_info_header_scene.xml | 1 + .../res/xml/manga_summary_section_scene.xml | 1 + 11 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 0cd7734aa2..341b617ace 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/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().coilClient) - crossfade(300) + crossfade((300 * this@App.animatorDurationScale).toInt()) allowRgb565(getSystemService()!!.isLowRamDevice) }.build() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt index c994ea99f0..b587d6c779 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt +++ b/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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt index c9d2fcb53a..1fd2a63044 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt +++ b/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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt index 70d9496ef5..2d06383665 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt +++ b/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 -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 6d08494bf0..b568f7caa9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/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() 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() 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() 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() binding.toolbar.startAnimation(toolbarAnimation) val bottomAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_bottom) + bottomAnimation.applySystemAnimatorScale(this) binding.readerMenuBottom.startAnimation(bottomAnimation) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/AnimationExtensions.kt new file mode 100644 index 0000000000..1946c11796 --- /dev/null +++ b/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) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 6f948a0cfb..130e290740 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/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()?.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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt index dfeb7281e6..3bca8603ce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt +++ b/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) } + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ActionToolbar.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ActionToolbar.kt index 6b4778ea95..3bcab8a5f5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/ActionToolbar.kt +++ b/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) { diff --git a/app/src/main/res/xml/manga_info_header_scene.xml b/app/src/main/res/xml/manga_info_header_scene.xml index 53bb9d14f0..9f4e27d0c4 100644 --- a/app/src/main/res/xml/manga_info_header_scene.xml +++ b/app/src/main/res/xml/manga_info_header_scene.xml @@ -5,6 +5,7 @@ diff --git a/app/src/main/res/xml/manga_summary_section_scene.xml b/app/src/main/res/xml/manga_summary_section_scene.xml index 28724fbd05..fc1304bbe8 100644 --- a/app/src/main/res/xml/manga_summary_section_scene.xml +++ b/app/src/main/res/xml/manga_summary_section_scene.xml @@ -5,6 +5,7 @@