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:
Hunter Nickel 2021-08-27 06:44:09 -06:00 committed by Jobobby04
parent e36957f00b
commit cfa6c180e7
11 changed files with 84 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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