2

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?

0 Answers0