diff --git a/core/src/main/java/io/plaidapp/core/ui/recyclerview/SlideInItemAnimator.kt b/core/src/main/java/io/plaidapp/core/ui/recyclerview/SlideInItemAnimator.kt index d5a05fe78..cd0078f44 100644 --- a/core/src/main/java/io/plaidapp/core/ui/recyclerview/SlideInItemAnimator.kt +++ b/core/src/main/java/io/plaidapp/core/ui/recyclerview/SlideInItemAnimator.kt @@ -17,15 +17,16 @@ package io.plaidapp.core.ui.recyclerview -import android.animation.Animator -import android.animation.AnimatorListenerAdapter import android.annotation.SuppressLint import android.view.Gravity import android.view.View +import androidx.dynamicanimation.animation.SpringAnimation.ALPHA +import androidx.dynamicanimation.animation.SpringAnimation.TRANSLATION_X +import androidx.dynamicanimation.animation.SpringAnimation.TRANSLATION_Y import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.RecyclerView -import io.plaidapp.core.util.AnimUtils -import java.util.ArrayList +import io.plaidapp.core.util.listenForAllSpringsEnd +import io.plaidapp.core.util.spring /** * A [RecyclerView.ItemAnimator] that fades & slides newly added items in from a given @@ -36,7 +37,7 @@ open class SlideInItemAnimator @JvmOverloads constructor( layoutDirection: Int = -1 ) : DefaultItemAnimator() { - private val pendingAdds = ArrayList() + private val pendingAdds = mutableListOf() private val slideFromEdge: Int = Gravity.getAbsoluteGravity(slideFromEdge, layoutDirection) init { @@ -47,11 +48,11 @@ open class SlideInItemAnimator @JvmOverloads constructor( override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean { holder.itemView.alpha = 0f when (slideFromEdge) { - Gravity.LEFT -> holder.itemView.translationX = (-holder.itemView.width / 3).toFloat() - Gravity.TOP -> holder.itemView.translationY = (-holder.itemView.height / 3).toFloat() - Gravity.RIGHT -> holder.itemView.translationX = (holder.itemView.width / 3).toFloat() + Gravity.LEFT -> holder.itemView.translationX = -holder.itemView.width / 3f + Gravity.TOP -> holder.itemView.translationY = -holder.itemView.height / 3f + Gravity.RIGHT -> holder.itemView.translationX = holder.itemView.width / 3f else // Gravity.BOTTOM - -> holder.itemView.translationY = (holder.itemView.height / 3).toFloat() + -> holder.itemView.translationY = holder.itemView.height / 3f } pendingAdds.add(holder) return true @@ -62,35 +63,31 @@ open class SlideInItemAnimator @JvmOverloads constructor( if (pendingAdds.isNotEmpty()) { for (i in pendingAdds.indices.reversed()) { val holder = pendingAdds[i] - holder.itemView.animate() - .alpha(1f) - .translationX(0f) - .translationY(0f) - .setDuration(addDuration) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - dispatchAddStarting(holder) - } + val springAlpha = holder.itemView.spring(ALPHA) + val springTranslationX = holder.itemView.spring(TRANSLATION_X) + val springTranslationY = holder.itemView.spring(TRANSLATION_Y) + dispatchAddStarting(holder) + springAlpha.animateToFinalPosition(1f) + springTranslationX.animateToFinalPosition(0f) + springTranslationY.animateToFinalPosition(0f) - override fun onAnimationEnd(animation: Animator) { - animation.listeners.remove(this) - dispatchAddFinished(holder) - dispatchFinishedWhenDone() - } + listenForAllSpringsEnd({ cancelled -> + if (cancelled) { + clearAnimatedValues(holder.itemView) + } + dispatchAddFinished(holder) + dispatchFinishedWhenDone() - override fun onAnimationCancel(animation: Animator) { - clearAnimatedValues(holder.itemView) - } - }).interpolator = AnimUtils.getLinearOutSlowInInterpolator( - holder.itemView.context - ) + }, springAlpha, springTranslationX, springTranslationY) pendingAdds.removeAt(i) } } } override fun endAnimation(holder: RecyclerView.ViewHolder) { - holder.itemView.animate().cancel() + holder.itemView.spring(ALPHA).cancel() + holder.itemView.spring(TRANSLATION_X).cancel() + holder.itemView.spring(TRANSLATION_Y).cancel() if (pendingAdds.remove(holder)) { dispatchAddFinished(holder) clearAnimatedValues(holder.itemView) diff --git a/core/src/main/java/io/plaidapp/core/util/SpringUtils.kt b/core/src/main/java/io/plaidapp/core/util/SpringUtils.kt new file mode 100644 index 000000000..cfb4c7e3c --- /dev/null +++ b/core/src/main/java/io/plaidapp/core/util/SpringUtils.kt @@ -0,0 +1,98 @@ +package io.plaidapp.core.util + +import android.view.View +import androidx.annotation.IdRes +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.DynamicAnimation.ViewProperty +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce +import io.plaidapp.core.R + +/** + * An extension function which creates/retrieves a [SpringAnimation] and stores it in the [View]s + * tag. + */ +fun View.spring( + property: ViewProperty, + stiffness: Float = 500f, + damping: Float = SpringForce.DAMPING_RATIO_NO_BOUNCY, + startVelocity: Float? = null +): SpringAnimation { + val key = getKey(property) + var springAnim = getTag(key) as? SpringAnimation? + if (springAnim == null) { + springAnim = SpringAnimation(this, property).apply { + spring = SpringForce().apply { + this.dampingRatio = damping + this.stiffness = stiffness + startVelocity?.let { setStartVelocity(it) } + } + } + setTag(key, springAnim) + } + return springAnim +} + +/** + * Map from a [ViewProperty] to an `id` suitable to use as a [View] tag. + */ +@IdRes +private fun getKey(property: ViewProperty): Int { + return when (property) { + SpringAnimation.TRANSLATION_X -> R.id.translation_x + SpringAnimation.TRANSLATION_Y -> R.id.translation_y + SpringAnimation.TRANSLATION_Z -> R.id.translation_z + SpringAnimation.SCALE_X -> R.id.scale_x + SpringAnimation.SCALE_Y -> R.id.scale_y + SpringAnimation.ROTATION -> R.id.rotation + SpringAnimation.ROTATION_X -> R.id.rotation_x + SpringAnimation.ROTATION_Y -> R.id.rotation_y + SpringAnimation.X -> R.id.x + SpringAnimation.Y -> R.id.y + SpringAnimation.Z -> R.id.z + SpringAnimation.ALPHA -> R.id.alpha + SpringAnimation.SCROLL_X -> R.id.scroll_x + SpringAnimation.SCROLL_Y -> R.id.scroll_y + else -> throw IllegalAccessException("Unknown ViewProperty: $property") + } +} + +/** + * A class which adds [DynamicAnimation.OnAnimationEndListener]s to the given `springs` and invokes + * `onEnd` when all have finished. + */ +class MultiSpringEndListener( + onEnd: (Boolean) -> Unit, + vararg springs: SpringAnimation +) { + private val listeners = ArrayList(springs.size) + + private var wasCancelled = false + + init { + springs.forEach { + val listener = object : DynamicAnimation.OnAnimationEndListener { + override fun onAnimationEnd( + animation: DynamicAnimation>?, + canceled: Boolean, + value: Float, + velocity: Float + ) { + animation?.removeEndListener(this) + wasCancelled = wasCancelled or canceled + listeners.remove(this) + if (listeners.isEmpty()) { + onEnd(wasCancelled) + } + } + } + it.addEndListener(listener) + listeners.add(listener) + } + } +} + +fun listenForAllSpringsEnd( + onEnd: (Boolean) -> Unit, + vararg springs: SpringAnimation +) = MultiSpringEndListener(onEnd, *springs) diff --git a/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml index 3a0ed0180..1043de54a 100644 --- a/core/src/main/res/values/ids.xml +++ b/core/src/main/res/values/ids.xml @@ -18,4 +18,18 @@ + + + + + + + + + + + + + +