mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	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:
		@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 ->
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
@@ -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.
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
@@ -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) }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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" />
 | 
			
		||||
 
 | 
			
		||||
@@ -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" />
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user