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
This commit is contained in:
Jays2Kings 2023-10-22 01:29:05 -07:00
parent 4a34efdd7d
commit 24e8eeea59
5 changed files with 139 additions and 29 deletions

View file

@ -3,10 +3,18 @@ package eu.kanade.tachiyomi.ui.base.controller
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorSet import android.animation.AnimatorSet
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup 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.ControllerChangeHandler
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
import eu.kanade.tachiyomi.ui.main.MainActivity
import kotlin.math.max
import kotlin.math.roundToLong
class CrossFadeChangeHandler : AnimatorChangeHandler { class CrossFadeChangeHandler : AnimatorChangeHandler {
constructor() : super() constructor() : super()
@ -34,22 +42,68 @@ class CrossFadeChangeHandler : AnimatorChangeHandler {
} }
if (isPush) { if (isPush) {
if (from != null) { 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) { 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 { } else {
if (from != null) { 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) { if (to != null) {
// Allow this to have a nice transition when coming off an aborted push animation or // Allow this to have a nice transition when coming off an aborted push animation or
// from back gesture // from back gesture
val fromLeft = from?.translationX ?: 0F val fromLeft = from?.translationX ?: 0f
animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.width * 0.2f, 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 return animatorSet
} }

View file

@ -16,12 +16,14 @@ import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock
import android.provider.Settings import android.provider.Settings
import android.view.GestureDetector import android.view.GestureDetector
import android.view.Gravity import android.view.Gravity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window 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.navigation.NavigationBarView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import com.google.common.primitives.Floats.max
import com.google.common.primitives.Ints.max import com.google.common.primitives.Ints.max
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.Migrations
@ -172,6 +175,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
var hingeGapSize = 0 var hingeGapSize = 0
private set private set
val velocityTracker: VelocityTracker by lazy { VelocityTracker.obtain() }
private val actionButtonSize: Pair<Int, Int> by lazy { private val actionButtonSize: Pair<Int, Int> by lazy {
val attrs = intArrayOf(android.R.attr.minWidth, android.R.attr.minHeight) val attrs = intArrayOf(android.R.attr.minWidth, android.R.attr.minHeight)
val ta = obtainStyledAttributes(androidx.appcompat.R.style.Widget_AppCompat_ActionButton, attrs) val ta = obtainStyledAttributes(androidx.appcompat.R.style.Widget_AppCompat_ActionButton, attrs)
@ -254,8 +258,30 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
backPressedCallback = object : OnBackPressedCallback(enabled = true) { backPressedCallback = object : OnBackPressedCallback(enabled = true) {
var startTime: Long = 0
var lastX: Float = 0f
var lastY: Float = 0f
var controllerHandlesBackPress = false var controllerHandlesBackPress = false
override fun handleOnBackPressed() { 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() backCallback()
} }
@ -276,12 +302,22 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
controllerHandlesBackPress = true controllerHandlesBackPress = true
} }
if (controllerHandlesBackPress) { 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) (controller as? BackHandlerControllerInterface)?.handleOnBackStarted(backEvent)
} }
} }
override fun handleOnBackProgressed(backEvent: BackEventCompat) { override fun handleOnBackProgressed(backEvent: BackEventCompat) {
if (controllerHandlesBackPress) { 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 val controller = router.backstack.lastOrNull()?.controller as? BackHandlerControllerInterface
controller?.handleOnBackProgressed(backEvent) controller?.handleOnBackProgressed(backEvent)
} }
@ -1517,6 +1553,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
const val INTENT_SEARCH_FILTER = "filter" const val INTENT_SEARCH_FILTER = "filter"
var chapterIdToExitTo = 0L var chapterIdToExitTo = 0L
var backVelocity = 0f
} }
} }

View file

@ -14,19 +14,22 @@ import android.graphics.Shader
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.os.SystemClock
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.View import android.view.View
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import androidx.activity.BackEventCompat import androidx.activity.BackEventCompat
import androidx.activity.ComponentDialog import androidx.activity.ComponentDialog
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.animation.addListener import androidx.core.animation.addListener
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import androidx.transition.ChangeBounds import androidx.transition.ChangeBounds
import androidx.transition.ChangeImageTransform import androidx.transition.ChangeImageTransform
import androidx.transition.TransitionManager 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.system.rootWindowInsetsCompat
import eu.kanade.tachiyomi.util.view.animateBlur import eu.kanade.tachiyomi.util.view.animateBlur
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class FullCoverDialog(val controller: MangaDetailsController, drawable: Drawable, private val thumbView: View) : 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 binding = FullCoverDialogBinding.inflate(LayoutInflater.from(context), null, false)
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
val velocityTracker: VelocityTracker by lazy { VelocityTracker.obtain() }
private val ratio = 5f.dpToPx private val ratio = 5f.dpToPx
private val fullRatio = 0f private val fullRatio = 0f
private val shortAnimationDuration = ( private val shortAnimationDuration = (
@ -85,26 +90,38 @@ class FullCoverDialog(val controller: MangaDetailsController, drawable: Drawable
} }
val backPressedCallback = object : OnBackPressedCallback(enabled = true) { val backPressedCallback = object : OnBackPressedCallback(enabled = true) {
var startX = 0f var startTime: Long = 0
var startY = 0f var lastX: Float = 0f
var lastY: Float = 0f
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
if (binding.mangaCoverFull.isClickable) { if (binding.mangaCoverFull.isClickable) {
val motionEvent = MotionEvent.obtain(startTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, lastX, lastY, 0)
velocityTracker.addMovement(motionEvent)
motionEvent.recycle()
animateBack() animateBack()
} }
} }
override fun handleOnBackStarted(backEvent: BackEventCompat) { override fun handleOnBackStarted(backEvent: BackEventCompat) {
startX = backEvent.touchX super.handleOnBackStarted(backEvent)
startY = backEvent.touchY 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) { override fun handleOnBackProgressed(backEvent: BackEventCompat) {
val maxProgress = min(backEvent.progress, 0.4f) 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.scaleX = 1f - maxProgress * 0.6f
binding.mangaCoverFull.translationX = binding.mangaCoverFull.translationX =
maxProgress * 100f * (if (backEvent.swipeEdge == BackEventCompat.EDGE_LEFT) 1 else -1) maxProgress * 100f * (if (backEvent.swipeEdge == BackEventCompat.EDGE_LEFT) 1 else -1)
binding.mangaCoverFull.translationY = binding.mangaCoverFull.translationY = -maxProgress * 150f
-maxProgress * 150f
binding.mangaCoverFull.scaleY = 1f - maxProgress * 0.6f binding.mangaCoverFull.scaleY = 1f - maxProgress * 0.6f
} }
@ -264,21 +281,24 @@ class FullCoverDialog(val controller: MangaDetailsController, drawable: Drawable
} }
// Zoom out back to tc thumbnail // Zoom out back to tc thumbnail
val transitionSet2 = TransitionSet() val transitionSet = TransitionSet()
val bound2 = ChangeBounds() transitionSet.addTransition(ChangeBounds())
transitionSet2.addTransition(bound2) transitionSet.addTransition(ChangeImageTransform())
val changeImageTransform2 = ChangeImageTransform() transitionSet.duration = shortAnimationDuration
transitionSet2.addTransition(changeImageTransform2) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
transitionSet2.duration = shortAnimationDuration velocityTracker.computeCurrentVelocity(2, 40f)
TransitionManager.beginDelayedTransition(binding.root, transitionSet2) 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 { activity?.window?.decorView?.animateBlur(20f, 0.1f, 50, true)?.apply {
startDelay = shortAnimationDuration - 100 startDelay = shortAnimationDuration - 100
}?.start() }?.start()
} }
val attrs = window?.attributes val attrs = window?.attributes
val ogDim = attrs?.dimAmount ?: 0.25f val ogDim = attrs?.dimAmount ?: 0.25f
velocityTracker.recycle()
// AnimationSet for backdrop because idk how to use TransitionSet // AnimationSet for backdrop because idk how to use TransitionSet
AnimatorSet().apply { AnimatorSet().apply {
@ -303,13 +323,12 @@ class FullCoverDialog(val controller: MangaDetailsController, drawable: Drawable
binding.btnSave.alpha = it.animatedValue as Float binding.btnSave.alpha = it.animatedValue as Float
} }
} }
playTogether(radiusAnimator, dimAnimator, saveAnimator)
val objectAnimator = ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, expandedImageView.scaleX, 1f) play(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, 1f))
val objectAnimator1 = ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, expandedImageView.scaleY, 1f) play(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, 1f))
val objectAnimator2 = ObjectAnimator.ofFloat(expandedImageView, View.TRANSLATION_X, expandedImageView.translationX, 0f) play(ObjectAnimator.ofFloat(expandedImageView, View.TRANSLATION_X, 0f))
val objectAnimator3 = ObjectAnimator.ofFloat(expandedImageView, View.TRANSLATION_Y, expandedImageView.translationY, 0f) play(ObjectAnimator.ofFloat(expandedImageView, View.TRANSLATION_Y, 0f))
playTogether(radiusAnimator, dimAnimator, saveAnimator, objectAnimator, objectAnimator1, objectAnimator2, objectAnimator3)
addListener( addListener(
onEnd = { onEnd = {

View file

@ -812,14 +812,14 @@ fun Controller.withFadeTransaction(): RouterTransaction {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
FadeChangeHandler() FadeChangeHandler()
} else { } else {
CrossFadeChangeHandler(duration = 200, removesFromViewOnPush = isLowRam) CrossFadeChangeHandler(removesFromViewOnPush = isLowRam)
}, },
) )
.popChangeHandler( .popChangeHandler(
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
FadeChangeHandler() FadeChangeHandler()
} else { } else {
CrossFadeChangeHandler(duration = 150, removesFromViewOnPush = isLowRam) CrossFadeChangeHandler(removesFromViewOnPush = isLowRam)
}, },
) )
} }

View file

@ -540,7 +540,7 @@ fun View.updateGradiantBGRadius(
} }
} }
@RequiresApi(31) @RequiresApi(Build.VERSION_CODES.S)
fun View.animateBlur( fun View.animateBlur(
@FloatRange(from = 0.1) from: Float, @FloatRange(from = 0.1) from: Float,
@FloatRange(from = 0.1) to: Float, @FloatRange(from = 0.1) to: Float,