Solution 1 :
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"
Problem :
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 RecyclerView
s 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>
Comments
Comment posted by Shaishav
Does the size of list changes or, does the item size changes? Also, what happens when you set the
Comment posted by Adrian Pascu
The size of the list changes when moving an item from one list to another. For the recyclerview I tried match_parent,fill_parent,wrap_content. None work
Comment posted by Adrian Pascu
I tried to implement your changes. Wrap content for width produced a width of 0, but before I realized that was why there was nothing rendering on the screen, I looked over the xml layout of the activity that holds my fragment and made some changes there. Apparently, it was the fragment that had sizing problems, not the ViewPager or the recyclerview. Now it seems to be working. Thx anyways, you helped me spot the issue