skip to Main Content

How I can handle items animation made with MotionLayout inside RecyclerView Because when I start scrolling like here everything becomes very messy.

I Know how RecyclerView works, but I try a lot to reset or control the MotionLayout Animate but It didn’t work all the time and is still messy

adapter code

class StoryViewHolder(private val mainView: View) : RecyclerView.ViewHolder(mainView){

    private var root: MotionLayout = mainView.findViewById(R.id.motion_root)
    private var deleteBtn: RelativeLayout = mainView.findViewById(R.id.delete_btn)
    private var editBtn: RelativeLayout = mainView.findViewById(R.id.edit_btn)
    private var storyImage: RoundedImageView = mainView.findViewById(R.id.story_image)
    private var storyTitleTv: TextView = mainView.findViewById(R.id.story_title_tv)
    private var storyAuthorTv: TextView = mainView.findViewById(R.id.story_author_tv)
    private var tagsLL: LinearLayout = mainView.findViewById(R.id.tags_ll)
    private var favouriteImg: ImageView = mainView.findViewById(R.id.favourite_img)

    lateinit var storyListener: StoryViewListener

    private lateinit var story: Story

    interface StoryViewListener {
        fun onDelete(story: Story, position: Int)
    }

    init {
        deleteBtn.setOnClickListener {
            storyListener.onDelete(story, adapterPosition)
        }

        editBtn.setOnClickListener {
        }

        storyImage.setOnClickListener {

        }
    }

    fun set(story: Story) {
        this.story = story
        Glide
            .with(mainView.context)
            .load(story.image)
            .centerCrop()
            .into(storyImage)

        storyTitleTv.text = story.title
        storyAuthorTv.text = "" 
        addTags(story.tags)

    }

    private fun addTags(tags: String) {
        tagsLL.removeAllViews()
        tags.split(",").forEach {
            val view =
                LayoutInflater.from(mainView.context).inflate(R.layout.view_tag, null, false)
            view.findViewById<TextView>(R.id.tag).text = it
            tagsLL.addView(view)
        }
    }

    fun setStart() {
        root.transitionToStart()
    }

    fun setEnd() {
        root.transitionToEnd()
    }
}

ViewHolder code

class StoryViewHolder(private val mainView: View) : RecyclerView.ViewHolder(mainView){

    private var root: MotionLayout = mainView.findViewById(R.id.motion_root)
    private var deleteBtn: RelativeLayout = mainView.findViewById(R.id.delete_btn)
    private var editBtn: RelativeLayout = mainView.findViewById(R.id.edit_btn)
    private var storyImage: RoundedImageView = mainView.findViewById(R.id.story_image)
    private var storyTitleTv: TextView = mainView.findViewById(R.id.story_title_tv)
    private var storyAuthorTv: TextView = mainView.findViewById(R.id.story_author_tv)
    private var tagsLL: LinearLayout = mainView.findViewById(R.id.tags_ll)
    private var favouriteImg: ImageView = mainView.findViewById(R.id.favourite_img)

    lateinit var storyListener: StoryViewListener

    private lateinit var story: Story

    interface StoryViewListener {
        fun onDelete(story: Story, position: Int)
    }

    init {
        deleteBtn.setOnClickListener {
            storyListener.onDelete(story, adapterPosition)
        }

        editBtn.setOnClickListener {
        }

        storyImage.setOnClickListener {

        }
    }

    fun set(story: Story) {
        this.story = story
        Glide
            .with(mainView.context)
            .load(story.image)
            .centerCrop()
            .into(storyImage)

        storyTitleTv.text = story.title
        storyAuthorTv.text = ""
        addTags(story.tags)

    }

    private fun addTags(tags: String) {
        tagsLL.removeAllViews()
        tags.split(",").forEach {
            val view =
                LayoutInflater.from(mainView.context).inflate(R.layout.view_tag, null, false)
            view.findViewById<TextView>(R.id.tag).text = it
            tagsLL.addView(view)
        }
    }

    fun setStart() {
        root.transitionToStart()
    }

    fun setEnd() {
        root.transitionToEnd()
    }
}

MotionLayout Animation code

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="300">
        <KeyFrameSet>
            <KeyAttribute
                android:alpha="0"
                motion:framePosition="40"
                motion:motionTarget="@+id/horizontalScrollView" />
            <KeyAttribute
                android:alpha="0"
                motion:framePosition="100"
                motion:motionTarget="@+id/horizontalScrollView" />

            <KeyAttribute
                android:alpha="0"
                motion:framePosition="50"
                motion:motionTarget="@+id/edit_btn" />
            <KeyAttribute
                android:alpha="1"
                motion:framePosition="100"
                motion:motionTarget="@+id/edit_btn" />
            <KeyAttribute
                android:alpha="0"
                motion:framePosition="50"
                motion:motionTarget="@+id/delete_btn" />
            <KeyAttribute
                android:alpha="1"
                motion:framePosition="100"
                motion:motionTarget="@+id/delete_btn" />
        </KeyFrameSet>
        <OnClick motion:targetId="@+id/story_image" />
    </Transition>

    <ConstraintSet android:id="@+id/start"></ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/frameLayout"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:alpha="1"
            motion:layout_constraintBottom_toBottomOf="@+id/story_image"
            motion:layout_constraintEnd_toEndOf="@+id/story_image"
            motion:layout_constraintStart_toStartOf="@+id/story_image"
            motion:layout_constraintTop_toTopOf="@+id/story_image"
            motion:layout_constraintVertical_bias="0.04000002" />

        <Constraint
            android:id="@+id/favourite_img"
            android:layout_width="1dp"
            android:layout_height="1dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="18dp"
            android:alpha="0"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="@+id/story_image" />


        <Constraint
            android:id="@+id/story_title_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_constraintBottom_toTopOf="@+id/story_author_tv"
            motion:layout_constraintEnd_toEndOf="@+id/story_image"
            motion:layout_constraintStart_toStartOf="@+id/story_image"
            motion:layout_constraintTop_toBottomOf="@+id/story_image" />
        <Constraint
            android:id="@+id/story_author_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_constraintBottom_toTopOf="@+id/story_image"
            motion:layout_constraintEnd_toEndOf="@+id/story_image"
            motion:layout_constraintStart_toStartOf="@+id/story_image"
            motion:layout_constraintTop_toBottomOf="@+id/story_title_tv" />

        <Constraint
            android:id="@+id/story_image"
            android:layout_width="0dp"
            android:layout_height="100dp"
            android:layout_marginStart="10dp"
            android:layout_marginTop="10dp"
            android:layout_marginEnd="10dp"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent"
            motion:riv_corner_radius="10dp" />

        <Constraint
            android:id="@+id/horizontalScrollView"
            android:layout_width="wrap_content"
            android:layout_height="1dp"
            android:layout_marginBottom="10dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintHorizontal_bias="1.0"
            motion:layout_constraintStart_toEndOf="@+id/story_image" />

        <Constraint
            android:id="@+id/delete_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="4dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toStartOf="@+id/edit_btn"
            motion:layout_constraintHorizontal_chainStyle="packed"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/story_image" />

        <Constraint
            android:id="@+id/edit_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="4dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintHorizontal_bias="0.5"
            motion:layout_constraintStart_toEndOf="@+id/delete_btn"
            motion:layout_constraintTop_toBottomOf="@id/story_image" />


    </ConstraintSet>

</MotionScene>

2

Answers


  1. Chosen as BEST ANSWER

    I found the problem: I had to control the MotionLayout animation completely from the Adapter inside onBindViewHolder(..) not from the MotionScene , So I deleted <OnClick motion:targetId="@+id/story_image" /> from the scene file xmlmotion_scene ,and I changed the adapter code.

    here's the short code:

    val START_MODE = 0
    val END_MODE = 1
    
    override fun onBindViewHolder(holder: StoryViewHolder, position: Int) {
       val model = modelsList[position]
    
       if (model.animeMode == START_MODE)
          holder.motionLayout.transitionToStart()
       else
          holder.motionLayout.transitionToEnd()
    
       holder.storyImage.setOnClickListener {
           if (models.get(holder.adapterPosition).animeMode == StoryModel.START_MODE) {
               models.get(holder.adapterPosition).animeMode = StoryModel.END_MODE
               holder.motionLayout.transitionToEnd()
           } else {
               models.get(holder.adapterPosition).animeMode = StoryModel.START_MODE
               holder.motionLayoutRoot.transitionToStart()
           }
       }
    
    }
    
    

  2. You should use RecyclerView.ItemAnimator for recyclerView animations. See recycerView.setItemAnimator method. It’s wrong to animate ViewHolder views directly into adapter because recyclerView reuse views. While you scroll recyclerView bind old views to new items, but recyclerview doesn’t know that old views are in process of animation, that’s why it becomes messy.

    Here is good video which explains how recyclerView and ItemAnimator works.
    Here is nice tutorial with custom ItemAnimator implementation.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search