Reputation: 1039
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>
Upvotes: 1
Views: 1045
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