Adrian Pascu
Adrian Pascu

Reputation: 1039

Android: RecyclerView randomly changing height

In my fragment I have a ViewPager2 component, each page containing a fragment that only holds a RecyclerView to display lists. The elements inside a list are meant to be moved around the lists (from the RecyclerView of one page the the RecyclerView of another page). So I wrote some logic to update the adaptors of the RecyclerViews to be able to move items around.

Updating the datasets works as expected, but for some reason, after moving an item from a list to another, the height of the lists changes. This behaviour is not consistent. Sometimes all the lists will get shrinked to the same height, sometimes only some of them have their height changed, sometimes some lists get their height set to 0, and sometimes everything works normally. Setting a fixed height to the RecyclerView fixed the issue, although I want the list to take up the entire space of the display, so a fixed height is obviously not a solution.

Also, I am not sure if it's the RecyclerView that shrinks and the ViewPager updates it's height accordingly or if it's vice versa.

Looking at the Sunflower example project in the Android docs I couldn't see any relevant difference between my project and the example, so I have no idea what is causing this behavior. Does anyone have any idea?

Here are the relevant parts of my application: Note: Anything database related is using the Room API. Also, The adapter for the RecyclerView was originally RecyclerView.Adapter, not ListAdapter, but the behavior is the same. I am willing to use any of them if the problem is related to the adapter.

MainFragment:

class MainFragment : Fragment() {

    private lateinit var binding: FragmentMainBinding
    private lateinit var viewPagerAdapter: ViewPagerAdapter;
    private lateinit var viewPager2: ViewPager2

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = FragmentMainBinding.inflate(inflater, container, false);
        return binding.root;
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)


        //Init the view pager
        viewPagerAdapter = ViewPagerAdapter(this)
        viewPager2 = binding.viewPager
        viewPager2.adapter = viewPagerAdapter
        viewPager2.isUserInputEnabled = false

        //init the tab layout

        binding.tabLayout.apply {
            TabLayoutMediator(this, viewPager2) { tab, position ->
                tab.text = TAB_LAYOUT_LABELS[position]
            }.attach()
        }

    }

    companion object {
        @JvmStatic
        fun newInstance() = MainFragment()
        private val TAB_LAYOUT_LABELS = arrayOf("TO BE READ", "READING", "DONE")
    }
} 

ViewPagerAdapter:

class ViewPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
    override fun getItemCount(): Int = 3

//    TODO: Create a separate fragment for the DONE list
override fun createFragment(position: Int): Fragment {
    val fragment = ReadingListFragment()
    fragment.arguments = Bundle().apply {
        putInt(ReadingListFragment.EXTRA_TYPE, position)
    }
    return fragment
}
}

ReadingListFragment

class ReadingListFragment : Fragment() {

    companion object {
        fun newInstance() =
            ReadingListFragment()

        public const val EXTRA_TYPE = "extraType"
    }

    private val viewModel: ReadingListViewModel by viewModels<ReadingListViewModel> {
        val type = ReadingListType.getType(arguments?.getInt(EXTRA_TYPE) ?: 3)
        ReadingListViewModelFactory(requireActivity().application, type)
    }
    private lateinit var binding: ReadingListFragmentBinding
    private lateinit var readingListAdapter: ReadingListAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewModel.readingList.observe(this) {
            val adapter = ReadingListAdapter(viewModel)
//            binding.readingListRecyclerView.swapAdapter(adapter, false)
            this.readingListAdapter.changeData(viewModel)
        }
    }


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = ReadingListFragmentBinding.inflate(inflater, container, false)

        //Init the recycler view

        val layoutManager = LinearLayoutManager(activity)
        this.readingListAdapter = ReadingListAdapter(viewModel)
        binding.readingListRecyclerView.apply {
            val value = viewModel.readingList.value
            adapter = readingListAdapter
            this.layoutManager = layoutManager
        }

        return binding.root
    }

}

ReadingListViewModel:

class ReadingListViewModel(private val app: Application, private val type: ReadingListType) :
    AndroidViewModel(app) {

    val readingList: LiveData<List<GoodreadsBook>> by lazy {
        Database.getInstance(app.applicationContext).goodreadsBookDao()
            .getReadingListAsLiveData(type)
    }


    //    Move item to the next list
    fun moveToTheNextList(pos: Int) {

        val item = readingList.value?.get(pos)

        //Update the item in memory
        if (item?.owner != null) {
            val newOwner = ReadingListType.getType(item.owner!!.value + 1)
            item.owner = newOwner


            //Update the item in the database


            viewModelScope.launch {
                withContext(Dispatchers.IO) {
                    val db = Database.getInstance(app.applicationContext)
                    db.goodreadsBookDao().updateBook(item)
                }
            }
        }
    }

}

@Parcelize
enum class ReadingListType(val value: Int) : Parcelable {
    TO_BE_READ(0), READING(1), DONE(2), UNSET(3);

    companion object {
        fun getType(value: Int) = values().first { it.value == value }
    }
}

class ReadingListTypeConverter {
    @TypeConverter
    fun fromReadingListTypeToInt(it: ReadingListType) = it.value

    @TypeConverter
    fun fromIntToReadingListType(it: Int) = ReadingListType.getType(it)
}

ReadingListViewModelFactory:

class ReadingListViewModelFactory(private val app: Application, private val type: ReadingListType) :
    ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T =
        ReadingListViewModel(app, type) as T
}

ReadingListAdapter:

class ReadingListAdapter(private var viewModel: ReadingListViewModel) :
    ListAdapter<GoodreadsBook, ReadingListViewHolder>(ReadingListItemDiff()) {

    private var dataset = viewModel.readingList.value

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReadingListViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding = ReadingListItemBinding.inflate(inflater, parent, false)
        return ReadingListViewHolder(binding) {
            viewModel.moveToTheNextList(it)
        }
    }

    fun changeData(newData: ReadingListViewModel) {
        viewModel = newData
        this.dataset = newData.readingList.value
        submitList(dataset)
    }


    override fun getItemCount(): Int = dataset?.size ?: 0

    override fun onBindViewHolder(holder: ReadingListViewHolder, position: Int) {
        holder.bind(this.dataset?.get(position))
    }


}

private class ReadingListItemDiff() : ItemCallback<GoodreadsBook>() {
    override fun areItemsTheSame(oldItem: GoodreadsBook, newItem: GoodreadsBook): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: GoodreadsBook, newItem: GoodreadsBook): Boolean {
        return oldItem.id == newItem.id
    }

}

ReadingListViewHolder:

class ReadingListViewHolder(
    private var binding: ReadingListItemBinding,
    private val moveBookToNextList: (pos: Int) -> Unit
) :
    RecyclerView.ViewHolder(binding.root) {

    private var animationEndId: Int = 0;

    init {
//        Add the move animation
        binding.readingListItemMotion.setTransitionListener(object :
            MotionLayout.TransitionListener {
            override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {
            }


            override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
//                Set the end ID
                animationEndId = p2
            }

            override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
            }

            override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {

//                Check if it's end and not start
                if (p1 == animationEndId) {
                    moveBookToNextList(adapterPosition)
                }

            }
        })
    }

    fun bind(newData: GoodreadsBook?) {
        binding.book = newData;
        binding.executePendingBindings()
    }

}

fragment_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/main_fragment__root_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ui.main.MainFragment">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryAlt"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar"
        app:tabIndicatorColor="@color/colorAccent"
        app:tabTextColor="@color/design_default_color_background">

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/tab_item_first" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/tab_item_second" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/tab_item_third" />
    </com.google.android.material.tabs.TabLayout>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="48dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tab_layout">

    </androidx.viewpager2.widget.ViewPager2>

</LinearLayout>

reading_list_fragment.xml

<LinearLayout 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="match_parent"
    android:paddingTop="@dimen/search_result_padding"
    android:orientation="vertical"
    tools:context=".ui.main.readingList.ReadingListFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/reading_list_recycler_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>

Upvotes: 1

Views: 1045

Answers (1)

Carson Holzheimer
Carson Holzheimer

Reputation: 2963

You have constraints set which are unused because your view is not inside a ConstraintLayout. Change this:

<androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="48dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tab_layout">

    </androidx.viewpager2.widget.ViewPager2>
    

to this:

<androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_marginTop="48dp">

    </androidx.viewpager2.widget.ViewPager2>

Also, change your height of your recycler view to:

android:layout_height="match_parent"

Upvotes: 1

Related Questions