Can You smooth scroll to new position within RecyclerView (before it is shown) and then show insert animation?

Viewed 557

Hello fellow programmers! I am currently facing problem with RecyclerView's add animation with smooth scroll simulataneously.

Problem

I have implemented RecyclerView with ListAdapter and DiffUtil for cool animations (insert, remove). The problem is that I can't get it to smooth scroll to added position without cancelling insert animation.


User is supposed to type in 'exercise name' and then push 'add' button to add it to current 'exercise list'. Then this exercise should be added to RecyclerView list. Everything works fine here, but I can't achieve smooth scroll to new item position without cancelling (or accelerating) insert animation.

viewModel.exerciseListLiveData.observe(viewLifecycleOwner, {

    adapter.submitList(it) {
        // Cancels insert animation (or accelerates it rapidly) - bad UX...    
        layoutManager.smoothScrollToPosition(recyclerView, RecyclerView.State(), 0) 
    }

})

On the other hand, substituting smoothScrollToPosition with scrollToPosition works fine but when there's more items (recycler's view is filled from top to bottom) it blinks on scroll. I know it's rather jump than smooth scroll but then, why is it working with add animation so it doesn't cancel or accelerate?

viewModel.exerciseListLiveData.observe(viewLifecycleOwner, {

    adapter.submitList(it) {
        // Works fine (recyclerView is scrolling to added position) 
        // but when there's no room to display more items - it scrolls to it with blink...
        // which I simply can't stand!
        layoutManager.smoothScrollToPosition(recyclerView, RecyclerView.State(), 0) 
    }

})

Possible solution?

I'm supposed to first smoothScrollToPosition(0) to top-most position, then call submitList(it) and finally call scrollToPosition(0) but I can't achieve that (it is doing so fast, simultaneously, that it's layering each other). Maybe I should use coroutines? Don't know...

How am I supposed to add proper delay? Maybe I am doing something wrong because i read that some programmers had this autoscroll when added item.


Code sample

Adapter

class ExercisesListAdapter :
    ListAdapter<Exercise, ExercisesListAdapter.ExerciseItemViewHolder>(ExercisesListDiffUtil()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExerciseItemViewHolder {

        val inflater = LayoutInflater.from(parent.context)
        val binding: ItemRecyclerviewExerciseBinding =
            DataBindingUtil.inflate(inflater, R.layout.item_recyclerview_exercise, parent, false)

        return ExerciseItemViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ExerciseItemViewHolder, position: Int) {

        val currentExercise = getItem(position)

        holder.bind(currentExercise)
    }

    override fun getItemId(position: Int): Long {
        return getItem(position).id
    }

    // Item ViewHolder class
    class ExerciseItemViewHolder(private val binding: ItemRecyclerviewExerciseBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(itemExercise: Exercise) {
            binding.textCardExerciseName.text = itemExercise.name
        }
    }
}

DiffUtil

class ExercisesListDiffUtil: DiffUtil.ItemCallback<Exercise>() {
    override fun areItemsTheSame(oldItem: Exercise, newItem: Exercise): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: Exercise, newItem: Exercise): Boolean {
        return oldItem == newItem
    }
}

ViewModel

class CreateWorkoutViewModel(private val repository: WorkoutRepository) : ViewModel() {

    private var exerciseId = 0L
    private var exerciseList = mutableListOf<Exercise>()

    val exerciseName = MutableLiveData<String?>()

    private val _exerciseListLiveData = MutableLiveData<List<Exercise>>()
    val exerciseListLiveData: LiveData<List<Exercise>>
        get() = _exerciseListLiveData

    fun onAddExercise() {
        if (canValidateExerciseName())
            addExerciseToList()
    }

    fun onAddWorkout() {
        exerciseList.removeAt(2)
        _exerciseListLiveData.value = exerciseList.toList()
    }

    private fun canValidateExerciseName(): Boolean {
        return !exerciseName.value.isNullOrBlank()
    }

    private fun addExerciseToList() {
        exerciseList.add(0, Exercise(exerciseId++, exerciseName.value!!))
        _exerciseListLiveData.value = exerciseList.toList()
    }
}

Fragment

class CreateWorkoutFragment : Fragment() {

    lateinit var binding: FragmentCreateWorkoutBinding
    lateinit var viewModel: CreateWorkoutViewModel
    lateinit var recyclerView: RecyclerView
    lateinit var adapter: ExercisesListAdapter
    lateinit var layoutManager: LinearLayoutManager

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        binding =
            DataBindingUtil.inflate(inflater, R.layout.fragment_create_workout, container, false)

        setViewModel()
        setRecyclerView()

        // Observers:
        // Update RecyclerView list on change
        viewModel.exerciseListLiveData.observe(viewLifecycleOwner, {

            adapter.submitList(it) {
                layoutManager.smoothScrollToPosition(recyclerView, RecyclerView.State(), 0)
            }
        })

        return binding.root
    }

    private fun setRecyclerView() {
        recyclerView = binding.recyclerviewExercises
        setLayoutManager()
        setAdapter()
    }

    private fun setLayoutManager() {
        layoutManager = LinearLayoutManager(context)
        recyclerView.layoutManager = layoutManager
    }

    private fun setAdapter() {
        adapter = ExercisesListAdapter()
        adapter.setHasStableIds(true)
        recyclerView.adapter = adapter
    }

    private fun setViewModel() {
        val dao = WorkoutDatabase.getInstance(requireContext()).workoutDAO
        val repository = WorkoutRepository(dao)
        val factory = CreateWorkoutViewModelFactory(repository)

        viewModel = ViewModelProvider(this, factory).get(CreateWorkoutViewModel::class.java)

        binding.viewModel = viewModel
        binding.lifecycleOwner = this
    }
}

EDIT

I got it working but it's not 'elegant' way and insert animation is canceled through process :/

    adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
        override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
            if(layoutManager.findFirstCompletelyVisibleItemPosition() == 0)
                layoutManager.scrollToPosition(0)
            else {
                recyclerView.clearAnimation()
                layoutManager.smoothScrollToPosition(recyclerView, RecyclerView.State(), 0)
            }
        }
    })
0 Answers
Related