Skip to content

Commit

Permalink
Convert SlideInItemAnimator to use Springs.
Browse files Browse the repository at this point in the history
  • Loading branch information
nickbutcher committed Jul 11, 2019
1 parent 0d49dd1 commit 5088419
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,7 +37,7 @@ open class SlideInItemAnimator @JvmOverloads constructor(
layoutDirection: Int = -1
) : DefaultItemAnimator() {

private val pendingAdds = ArrayList<RecyclerView.ViewHolder>()
private val pendingAdds = mutableListOf<RecyclerView.ViewHolder>()
private val slideFromEdge: Int = Gravity.getAbsoluteGravity(slideFromEdge, layoutDirection)

init {
Expand All @@ -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
Expand All @@ -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)
Expand Down
98 changes: 98 additions & 0 deletions core/src/main/java/io/plaidapp/core/util/SpringUtils.kt
Original file line number Diff line number Diff line change
@@ -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<DynamicAnimation.OnAnimationEndListener>(springs.size)

private var wasCancelled = false

init {
springs.forEach {
val listener = object : DynamicAnimation.OnAnimationEndListener {
override fun onAnimationEnd(
animation: DynamicAnimation<out 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)
14 changes: 14 additions & 0 deletions core/src/main/res/values/ids.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,18 @@
<resources>
<item type="id" name="tag_reflow_data" />
<item type="id" name="fab" />
<item name="translation_x" type="id"/>
<item name="translation_y" type="id"/>
<item name="translation_z" type="id"/>
<item name="scale_x" type="id"/>
<item name="scale_y" type="id"/>
<item name="rotation" type="id"/>
<item name="rotation_x" type="id"/>
<item name="rotation_y" type="id"/>
<item name="x" type="id"/>
<item name="y" type="id"/>
<item name="z" type="id"/>
<item name="alpha" type="id"/>
<item name="scroll_x" type="id"/>
<item name="scroll_y" type="id"/>
</resources>

0 comments on commit 5088419

Please sign in to comment.