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.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 * this@App.animatorDurationScale).toInt())
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
}.build()
}

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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