I have a case where a view within RecyclerView items needs to be animated using the Lottie library. Each recycler view item is clickable and contains a liking Lottie animation.
I defined a custom RecyclerView.ItemAnimator like this:
class SampleItemAnimator : DefaultItemAnimator() {
override fun animateChange(
oldHolder: RecyclerView.ViewHolder,
newHolder: RecyclerView.ViewHolder,
preInfo: ItemHolderInfo,
postInfo: ItemHolderInfo
): Boolean {
val holder = newHolder as BindingViewHolder<ItemSampleBinding>
val animator = lottieAnimatorListener {
dispatchAnimationFinished(holder)
holder.binding.sampleAnimation.removeAllAnimatorListeners()
}
holder.binding.sampleAnimation.addAnimatorListener(animator)
if (preInfo is SampleItemHolderInfo) {
if (preInfo.isItemLicked) {
holder.binding.sampleAnimation.playAnimation()
} else {
resetAnimation(holder.binding.sampleAnimation)
}
return true
}
return super.animateChange(oldHolder, newHolder, preInfo, postInfo)
}
private fun resetAnimation(lottieAnimationView: LottieAnimationView) {
lottieAnimationView.progress = 0f
lottieAnimationView.cancelAnimation()
}
override fun recordPreLayoutInformation(
state: RecyclerView.State,
viewHolder: RecyclerView.ViewHolder,
changeFlags: Int,
payloads: MutableList<Any>
): ItemHolderInfo {
if (changeFlags == FLAG_CHANGED) {
return produceItemHolderInfoOrElse(payloads.firstOrNull() as? Int) {
super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads)
}
}
return super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads)
}
private fun produceItemHolderInfoOrElse(value: Int?, action: () -> ItemHolderInfo) =
when (value) {
LIKE_ITEM -> SampleItemHolderInfo(true)
UNLIKE_ITEM -> SampleItemHolderInfo(false)
else -> action()
}
override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder) = true
override fun canReuseUpdatedViewHolder(
viewHolder: RecyclerView.ViewHolder,
payloads: MutableList<Any>
) = true
}
lottieAnimatorListener is just a function that creates Animator.AnimatorListener to tell RecyclerView when the animation is canceled or ended by calling dispatchAnimationFinished(holder).
Everything works except that sometimes the liking animation can randomly play on items with no likes, especially while scrolling RecyclerView too fast.
As far as I understand, it happens because the ItemAnimator re-uses the same view holders and either uses outdated ItemHolderInfo or does not notify the RecyclerView about the end of the animation correctly.
That is how I pass a payload to the adapter to tell what has changed using DiffUtil.Callback.
class SampleListDiffCallback : DiffCallback<SampleItem> {
override fun areContentsTheSame(oldItem: SampleItem, newItem: SampleItem) =
oldItem.markableItem == newItem.markableItem
override fun areItemsTheSame(oldItem: SampleItem, newItem: SampleItem) =
oldItem.identifier == newItem.identifier
override fun getChangePayload(
oldItem: SampleItem,
oldItemPosition: Int,
newItem: SampleItem,
newItemPosition: Int
): Any? = createPayload(oldItem.markableItem, newItem.markableItem)
private fun createPayload(
oldItem: MarkableItem,
newItem: MarkableItem
) = when {
! oldItem.isLiked && newItem.isLiked -> LIKE_ITEM
oldItem.isLiked && ! newItem.isLiked -> UNLIKE_ITEM
else -> null
}
}
That is how I define a ViewHolder using the FastAdapter library:
class SampleItem(
val markableItem: MarkableItem,
private val onClickItem: (Item) -> Unit
) : AbstractBindingItem<ItemSampleBinding>() {
override val type: Int = R.layout.item
override var identifier: Long = markableItem.item.hashCode().toLong()
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?) =
ItemSampleBinding.inflate(inflater, parent, false)
override fun bindView(binding: ItemSampleBinding, payloads: List<Any>) {
super.bindView(binding, payloads)
with(binding) {
itemName.text = markableItem.item.name
itemImage.setContent(markableItem.item, IMAGE_SIZE)
likeAnimation.progress = if(markableItem.isClicked) 1f else 0f
root.setThrottleClickListener { onClickItem(markableItem.item) }
}
}
}
UPD: The liking animation's duration is 2 seconds.
Does anybody know if there is any way to fix it?