From 24e8eeea598a9cb3874a007c4eccf3cf77c0450d Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sun, 22 Oct 2023 01:29:05 -0700 Subject: [PATCH] Improvements to the pop animations On Android 14's the pop interpolation now uses the speed at which the back gesture happens Same "magic" applies to the back gesture for the full cover --- .../base/controller/CrossFadeChangeHandler.kt | 64 +++++++++++++++++-- .../kanade/tachiyomi/ui/main/MainActivity.kt | 37 +++++++++++ .../tachiyomi/ui/manga/FullCoverDialog.kt | 61 ++++++++++++------ .../util/view/ControllerExtensions.kt | 4 +- .../tachiyomi/util/view/ViewExtensions.kt | 2 +- 5 files changed, 139 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/CrossFadeChangeHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/CrossFadeChangeHandler.kt index 9daa83eba3..84601e164f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/CrossFadeChangeHandler.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/CrossFadeChangeHandler.kt @@ -3,10 +3,18 @@ package eu.kanade.tachiyomi.ui.base.controller import android.animation.Animator import android.animation.AnimatorSet import android.animation.ObjectAnimator +import android.os.Build import android.view.View import android.view.ViewGroup +import android.view.animation.DecelerateInterpolator +import androidx.core.animation.doOnCancel +import androidx.core.animation.doOnEnd +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler +import eu.kanade.tachiyomi.ui.main.MainActivity +import kotlin.math.max +import kotlin.math.roundToLong class CrossFadeChangeHandler : AnimatorChangeHandler { constructor() : super() @@ -34,22 +42,68 @@ class CrossFadeChangeHandler : AnimatorChangeHandler { } if (isPush) { if (from != null) { - animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, -from.width.toFloat() * 0.2f)) + animatorSet.play( + ObjectAnimator.ofFloat( + from, + View.TRANSLATION_X, + -from.width.toFloat() * 0.2f, + ), + ) } if (to != null) { - animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, to.width.toFloat() * 0.2f, 0f)) + animatorSet.play( + ObjectAnimator.ofFloat( + to, + View.TRANSLATION_X, + to.width.toFloat() * 0.2f, + 0f, + ), + ) } } else { if (from != null) { - animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.width.toFloat() * 0.2f)) + animatorSet.play( + ObjectAnimator.ofFloat( + from, + View.TRANSLATION_X, + from.width.toFloat() * 0.2f, + ), + ) } if (to != null) { // Allow this to have a nice transition when coming off an aborted push animation or // from back gesture - val fromLeft = from?.translationX ?: 0F - animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.width * 0.2f, 0f)) + val fromLeft = from?.translationX ?: 0f + animatorSet.play( + ObjectAnimator.ofFloat( + to, + View.TRANSLATION_X, + fromLeft - to.width * 0.2f, + 0f, + ), + ) } } + animatorSet.duration = if (isPush) { + 200 + } else { + from?.let { + val startX = from.width.toFloat() * 0.2f + ((startX - it.x) / startX) * 150f + }?.roundToLong() ?: 150 + } + animatorSet.doOnCancel { to?.x = 0f } + animatorSet.doOnEnd { to?.x = 0f } + if (!isPush && from?.x != null && from.x != 0f && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + ) { + animatorSet.interpolator = if (MainActivity.backVelocity != 0f) { + DecelerateInterpolator(max(1f, MainActivity.backVelocity)) + } else { + LinearOutSlowInInterpolator() + } + MainActivity.backVelocity = 0f + } return animatorSet } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 246699902c..ae6817fb48 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -16,12 +16,14 @@ import android.graphics.Rect import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.SystemClock import android.provider.Settings import android.view.GestureDetector import android.view.Gravity import android.view.Menu import android.view.MenuItem import android.view.MotionEvent +import android.view.VelocityTracker import android.view.View import android.view.ViewGroup import android.view.Window @@ -65,6 +67,7 @@ import com.getkeepsafe.taptargetview.TapTargetView import com.google.android.material.navigation.NavigationBarView import com.google.android.material.snackbar.Snackbar import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback +import com.google.common.primitives.Floats.max import com.google.common.primitives.Ints.max import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.Migrations @@ -172,6 +175,7 @@ open class MainActivity : BaseActivity() { var hingeGapSize = 0 private set + val velocityTracker: VelocityTracker by lazy { VelocityTracker.obtain() } private val actionButtonSize: Pair by lazy { val attrs = intArrayOf(android.R.attr.minWidth, android.R.attr.minHeight) val ta = obtainStyledAttributes(androidx.appcompat.R.style.Widget_AppCompat_ActionButton, attrs) @@ -254,8 +258,30 @@ open class MainActivity : BaseActivity() { super.onCreate(savedInstanceState) backPressedCallback = object : OnBackPressedCallback(enabled = true) { + var startTime: Long = 0 + var lastX: Float = 0f + var lastY: Float = 0f var controllerHandlesBackPress = false override fun handleOnBackPressed() { + if (controllerHandlesBackPress && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && + lastX != 0f && lastY != 0f + ) { + val motionEvent = MotionEvent.obtain( + startTime, + SystemClock.uptimeMillis(), + MotionEvent.ACTION_UP, + lastX, + lastY, + 0, + ) + velocityTracker.addMovement(motionEvent) + motionEvent.recycle() + velocityTracker.computeCurrentVelocity(2, 5f) + backVelocity = max(1f, velocityTracker.getAxisVelocity(MotionEvent.AXIS_X)) + } + lastX = 0f + lastY = 0f backCallback() } @@ -276,12 +302,22 @@ open class MainActivity : BaseActivity() { controllerHandlesBackPress = true } if (controllerHandlesBackPress) { + startTime = SystemClock.uptimeMillis() + velocityTracker.clear() + val motionEvent = MotionEvent.obtain(startTime, startTime, MotionEvent.ACTION_DOWN, backEvent.touchX, backEvent.touchY, 0) + velocityTracker.addMovement(motionEvent) + motionEvent.recycle() (controller as? BackHandlerControllerInterface)?.handleOnBackStarted(backEvent) } } override fun handleOnBackProgressed(backEvent: BackEventCompat) { if (controllerHandlesBackPress) { + val motionEvent = MotionEvent.obtain(startTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_MOVE, backEvent.touchX, backEvent.touchY, 0) + lastX = backEvent.touchX + lastY = backEvent.touchY + velocityTracker.addMovement(motionEvent) + motionEvent.recycle() val controller = router.backstack.lastOrNull()?.controller as? BackHandlerControllerInterface controller?.handleOnBackProgressed(backEvent) } @@ -1517,6 +1553,7 @@ open class MainActivity : BaseActivity() { const val INTENT_SEARCH_FILTER = "filter" var chapterIdToExitTo = 0L + var backVelocity = 0f } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/FullCoverDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/FullCoverDialog.kt index 0bb0554009..bc45b28217 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/FullCoverDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/FullCoverDialog.kt @@ -14,19 +14,22 @@ import android.graphics.Shader import android.graphics.drawable.Drawable import android.os.Build import android.os.PowerManager +import android.os.SystemClock import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.VelocityTracker import android.view.View import android.view.animation.DecelerateInterpolator import androidx.activity.BackEventCompat import androidx.activity.ComponentDialog import androidx.activity.OnBackPressedCallback -import androidx.activity.addCallback import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.animation.addListener import androidx.core.content.ContextCompat import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.updateLayoutParams +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import androidx.transition.ChangeBounds import androidx.transition.ChangeImageTransform import androidx.transition.TransitionManager @@ -40,6 +43,7 @@ import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat import eu.kanade.tachiyomi.util.view.animateBlur import uy.kohesive.injekt.injectLazy +import kotlin.math.max import kotlin.math.min class FullCoverDialog(val controller: MangaDetailsController, drawable: Drawable, private val thumbView: View) : @@ -49,6 +53,7 @@ class FullCoverDialog(val controller: MangaDetailsController, drawable: Drawable val binding = FullCoverDialogBinding.inflate(LayoutInflater.from(context), null, false) val preferences: PreferencesHelper by injectLazy() + val velocityTracker: VelocityTracker by lazy { VelocityTracker.obtain() } private val ratio = 5f.dpToPx private val fullRatio = 0f private val shortAnimationDuration = ( @@ -85,26 +90,38 @@ class FullCoverDialog(val controller: MangaDetailsController, drawable: Drawable } val backPressedCallback = object : OnBackPressedCallback(enabled = true) { - var startX = 0f - var startY = 0f + var startTime: Long = 0 + var lastX: Float = 0f + var lastY: Float = 0f override fun handleOnBackPressed() { if (binding.mangaCoverFull.isClickable) { + val motionEvent = MotionEvent.obtain(startTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, lastX, lastY, 0) + velocityTracker.addMovement(motionEvent) + motionEvent.recycle() animateBack() } } override fun handleOnBackStarted(backEvent: BackEventCompat) { - startX = backEvent.touchX - startY = backEvent.touchY + super.handleOnBackStarted(backEvent) + startTime = SystemClock.uptimeMillis() + velocityTracker.clear() + val motionEvent = MotionEvent.obtain(startTime, startTime, MotionEvent.ACTION_DOWN, backEvent.touchX, backEvent.touchY, 0) + velocityTracker.addMovement(motionEvent) + motionEvent.recycle() } override fun handleOnBackProgressed(backEvent: BackEventCompat) { val maxProgress = min(backEvent.progress, 0.4f) + val motionEvent = MotionEvent.obtain(startTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_MOVE, backEvent.touchX, backEvent.touchY, 0) + lastX = backEvent.touchX + lastY = backEvent.touchY + velocityTracker.addMovement(motionEvent) + motionEvent.recycle() binding.mangaCoverFull.scaleX = 1f - maxProgress * 0.6f binding.mangaCoverFull.translationX = maxProgress * 100f * (if (backEvent.swipeEdge == BackEventCompat.EDGE_LEFT) 1 else -1) - binding.mangaCoverFull.translationY = - -maxProgress * 150f + binding.mangaCoverFull.translationY = -maxProgress * 150f binding.mangaCoverFull.scaleY = 1f - maxProgress * 0.6f } @@ -264,21 +281,24 @@ class FullCoverDialog(val controller: MangaDetailsController, drawable: Drawable } // Zoom out back to tc thumbnail - val transitionSet2 = TransitionSet() - val bound2 = ChangeBounds() - transitionSet2.addTransition(bound2) - val changeImageTransform2 = ChangeImageTransform() - transitionSet2.addTransition(changeImageTransform2) - transitionSet2.duration = shortAnimationDuration - TransitionManager.beginDelayedTransition(binding.root, transitionSet2) + val transitionSet = TransitionSet() + transitionSet.addTransition(ChangeBounds()) + transitionSet.addTransition(ChangeImageTransform()) + transitionSet.duration = shortAnimationDuration + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + velocityTracker.computeCurrentVelocity(2, 40f) + transitionSet.interpolator = DecelerateInterpolator(max(1f, velocityTracker.getAxisVelocity(MotionEvent.AXIS_X))) + } + TransitionManager.beginDelayedTransition(binding.root, transitionSet) - if (Build.VERSION.SDK_INT >= 31) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { activity?.window?.decorView?.animateBlur(20f, 0.1f, 50, true)?.apply { startDelay = shortAnimationDuration - 100 }?.start() } val attrs = window?.attributes val ogDim = attrs?.dimAmount ?: 0.25f + velocityTracker.recycle() // AnimationSet for backdrop because idk how to use TransitionSet AnimatorSet().apply { @@ -303,13 +323,12 @@ class FullCoverDialog(val controller: MangaDetailsController, drawable: Drawable binding.btnSave.alpha = it.animatedValue as Float } } + playTogether(radiusAnimator, dimAnimator, saveAnimator) - val objectAnimator = ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, expandedImageView.scaleX, 1f) - val objectAnimator1 = ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, expandedImageView.scaleY, 1f) - val objectAnimator2 = ObjectAnimator.ofFloat(expandedImageView, View.TRANSLATION_X, expandedImageView.translationX, 0f) - val objectAnimator3 = ObjectAnimator.ofFloat(expandedImageView, View.TRANSLATION_Y, expandedImageView.translationY, 0f) - - playTogether(radiusAnimator, dimAnimator, saveAnimator, objectAnimator, objectAnimator1, objectAnimator2, objectAnimator3) + play(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, 1f)) + play(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, 1f)) + play(ObjectAnimator.ofFloat(expandedImageView, View.TRANSLATION_X, 0f)) + play(ObjectAnimator.ofFloat(expandedImageView, View.TRANSLATION_Y, 0f)) addListener( onEnd = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt index 2e98454f06..d5816d0c1d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt @@ -812,14 +812,14 @@ fun Controller.withFadeTransaction(): RouterTransaction { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { FadeChangeHandler() } else { - CrossFadeChangeHandler(duration = 200, removesFromViewOnPush = isLowRam) + CrossFadeChangeHandler(removesFromViewOnPush = isLowRam) }, ) .popChangeHandler( if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { FadeChangeHandler() } else { - CrossFadeChangeHandler(duration = 150, removesFromViewOnPush = isLowRam) + CrossFadeChangeHandler(removesFromViewOnPush = isLowRam) }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 280cc2c73c..3a414ac5f3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -540,7 +540,7 @@ fun View.updateGradiantBGRadius( } } -@RequiresApi(31) +@RequiresApi(Build.VERSION_CODES.S) fun View.animateBlur( @FloatRange(from = 0.1) from: Float, @FloatRange(from = 0.1) to: Float,