Adding Lottie animation to Android recyclerview leads to unstable results including views displaying in the wrong place and scrollToPosition bugging

Viewed 1014

Im creating a simple chat style recyclerview like messenger only super simplified. It all works perfectly, however when I add a lottie animation to show the other person is typing it causes a lot of instability in the recyclerview.

I used a scrollToPosition function when a new message is inserted so it will auto scroll down to the bottom and make the message visible. This bugs out when i add the lottie file, it wont scroll to the bottom properly maybe only half way down instead.

In addition I noticed when I move down the list the message from index 0 of the list will appear as the most recent index so its now duplicated. All these anomolies fix themselves when I remove the lottie animation from the xml. But I would like to have the ability to show a typing animation like every other chat application on the planet. If someone could help me understand what is going wrong I would be greatly appreciative.

Video snippet of scrollToPosition failing with lottie animation

To clarify when I remove the lottie animation from the xml the scrollToPosition works as intended and their is no unusual behaviour.

ViewHolder XML

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="@dimen/_6sdp"
    android:layout_marginTop="@dimen/_12sdp">

    <ImageView
        android:id="@+id/icon"
        android:layout_width="@dimen/_24sdp"
        android:layout_height="@dimen/_24sdp"
        android:layout_marginStart="@dimen/_6sdp"
        android:layout_marginBottom="4dp"
        android:contentDescription="User Icon"
        app:layout_constraintBottom_toBottomOf="@+id/messageCv"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/messageCv"
        app:layout_constraintVertical_bias="1.0"
        app:srcCompat="@drawable/feelingfit_app_icon_legacy_circular_v1" />

    <!--<com.airbnb.lottie.LottieAnimationView
        android:id="@+id/typingAnimation"
        android:layout_width="@dimen/_30sdp"
        android:layout_height="@dimen/_30sdp"
        android:layout_marginStart="@dimen/_12sdp"
        app:lottie_autoPlay="true"
        app:lottie_loop="true"
        app:lottie_repeatCount="3"
        android:visibility="visible"
        app:lottie_rawRes="@raw/typing"
        app:lottie_cacheStrategy="strong"
        app:layout_constraintBottom_toBottomOf="@+id/icon"
        app:layout_constraintStart_toEndOf="@+id/icon"
        app:layout_constraintTop_toTopOf="@+id/icon"
        />-->

    <com.google.android.material.card.MaterialCardView
        android:id="@+id/messageCv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="@dimen/_40sdp"
        android:visibility="invisible"
        app:cardBackgroundColor="@color/light_blue_50"
        app:cardCornerRadius="@dimen/_10sdp"
        app:cardElevation="0dp"
        app:cardPreventCornerOverlap="false"
        app:cardUseCompatPadding="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toEndOf="@+id/icon"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:id="@+id/layout_gchat_container_other"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <com.google.android.material.textview.MaterialTextView
                android:id="@+id/message"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="@dimen/_6sdp"
                android:maxWidth="@dimen/_230sdp"
                android:paddingLeft="@dimen/_10sdp"
                android:paddingTop="@dimen/_6sdp"
                android:paddingRight="@dimen/_10sdp"
                android:text="Message"
                android:textColor="#000000"
                android:textSize="@dimen/text_size_normal" />
        </LinearLayout>

    </com.google.android.material.card.MaterialCardView>

    <com.google.android.material.textview.MaterialTextView
        android:id="@+id/timestamp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/_10sdp"
        android:layout_marginTop="@dimen/_4sdp"
        android:layout_marginBottom="@dimen/_4sdp"
        android:text="Today at 110:00pm"
        android:textColor="#C0C0C0"
        android:textSize="@dimen/_8sdp"
        android:visibility="invisible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="@+id/messageCv"
        app:layout_constraintTop_toBottomOf="@+id/messageCv" />

</androidx.constraintlayout.widget.ConstraintLayout>

ADAPTER

class ReplaceAndEmbraceAdapter
constructor(
    private val dateUtil: DateUtil,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private var messageList: MutableList<ReplaceAppModel> = mutableListOf()
    private val appMessage: Int = 1
    private val userMessage: Int = 2
    
    inner class ChatDiffCallback(
        private val oldList: List<ReplaceAppModel>,
        private val newList: List<ReplaceAppModel>
    ) : DiffUtil.Callback() {
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return oldList[oldItemPosition] == newList[newItemPosition]
        }
        override fun getOldListSize(): Int = oldList.size
        override fun getNewListSize(): Int = newList.size
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            return oldList[oldItemPosition] == newList[newItemPosition]
        }
    }

    fun addChat(list: List<ReplaceAppModel>) = apply {
        val diffCallback = ChatDiffCallback(messageList, list)
        val diffResult = DiffUtil.calculateDiff(diffCallback)
        messageList.clear()
        messageList.addAll(list)
        diffResult.dispatchUpdatesTo(this)
    }

    /* fun addChat(list: List<ReplaceAppModel>) {
         messageList = list
         notifyItemInserted(messageList.size)
     }*/

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            appMessage -> {
                MessageAppViewHolder(
                    RecyclerViewReplaceAndEmbraceAppBinding.inflate(
                        LayoutInflater.from(parent.context),
                        parent,
                        false
                    ),
                    dateUtil
                )
            }

            else -> MessageUserViewHolder(
                RecyclerViewReplaceAndEmbraceUserBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent,
                    false
                )
            )
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is MessageAppViewHolder -> {
                holder.bind(messageList?.get(position))
            }
            is MessageUserViewHolder -> {
                holder.bind(messageList?.get(position))
            }
        }
    }

    override fun getItemCount(): Int {
        return if (!messageList.isNullOrEmpty()) {
            messageList!!.size
        } else {
            0
        }
    }

    override fun getItemViewType(position: Int): Int = messageList[position].id

    class MessageAppViewHolder
    constructor(
        private val binding: RecyclerViewReplaceAndEmbraceAppBinding,
        private val dateUtil: DateUtil
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: ReplaceAppModel?) = with(itemView) {
            binding?.apply {


                 typingAnimation.addAnimatorListener(object : Animator.AnimatorListener {
                     override fun onAnimationRepeat(animation: Animator?) {
                     }

                     override fun onAnimationEnd(animation: Animator?) {
                         typingAnimation.visibility = View.INVISIBLE
                         message.text =
                             HtmlCompat.fromHtml(item?.message!!, HtmlCompat.FROM_HTML_MODE_LEGACY)
                         icon.setImageResource(R.drawable.feelingfit_app_icon_legacy_circular_v1)
                         timestamp.text = dateUtil.getCurrentTimestampEDDMMMYYYYHHMM()
                         timestamp.visibility = View.VISIBLE
                         messageCv.visibility = View.VISIBLE
                         messageCv.fadeIn()
                         timestamp.fadeIn()
                     }

                     override fun onAnimationCancel(animation: Animator?) {
                     }

                     override fun onAnimationStart(animation: Animator?) {
                     }

                 })

            }
        }
    }
1 Answers

After looking at the available code in my opinion, the problem is with constraints so first try to change constraints of typingAnimation and icon w.r.t parent instead of messageCv.

But after looking the video, I think you don’t have to put animation in recyclerView/adapter. Just put it below the recyclerView. I mean in between recyclerView and EditText and change visibility when you want to show/hide animation.

If you still want to show animation in recycler view then you should create a separate view for animation view. Which means you will have three viewTypes and viewHolders. This is a better way to do this If you are showing it in recyclerView.

Related