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
This commit is contained in:
Hunter Nickel 2021-08-27 06:44:09 -06:00 committed by GitHub
parent cc3cbbc4bb
commit df683375b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 84 additions and 8 deletions

View file

@ -31,6 +31,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 kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -125,7 +126,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()
} }

View file

@ -4,11 +4,11 @@ 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.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
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * 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. // Update the cover.
binding.thumbnail.clear() binding.thumbnail.clear()
binding.thumbnail.loadAny(item.manga) binding.thumbnail.loadAnyAutoPause(item.manga)
} }
} }

View file

@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.clear import coil.clear
import coil.loadAny
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
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
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * 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. // Update the cover.
binding.thumbnail.clear() binding.thumbnail.clear()
binding.thumbnail.loadAny(item.manga) binding.thumbnail.loadAnyAutoPause(item.manga)
} }
} }

View file

@ -7,7 +7,6 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
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.kanade.tachiyomi.R 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.source.online.HttpSource
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.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 kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@ -92,6 +93,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
@ -278,8 +285,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 ->

View file

@ -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.ui.reader.viewer.pager.R2LPagerViewer
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
@ -528,6 +529,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) {
@ -539,6 +541,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.toolbar.startAnimation(toolbarAnimation) binding.toolbar.startAnimation(toolbarAnimation)
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)
} }
@ -553,6 +556,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) {
@ -563,6 +567,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.toolbar.startAnimation(toolbarAnimation) binding.toolbar.startAnimation(toolbarAnimation)
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)
} }

View file

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

View file

@ -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.
*/ */

View file

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

View file

@ -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) {

View file

@ -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" />

View file

@ -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" />