Webster
Webster

Reputation: 1153

Recyclerview with multiple view types inside RecyclerView shows wrong layout

I have a parent layout it has some TextViews and a RecyclerView in it. And the child layout, it has a RecyclerView in it and the adapter has 5 different View Types

For the child recyclerview, i use an Adapter which has 5 different layouts in it that consists of

  1. VIEW_TYPE_LANDSCAPE
  2. VIEW_TYPE_LANDSCAPE_CARD
  3. VIEW_TYPE_PORTRAIT
  4. VIEW_TYPE_SQUARE
  5. VIEW_TYPE_TWO_COLUMN

The problem occurs when the child RecyclerView has more than x items in it. When I scroll down to the bottom of the screen, and then scroll up to the top of the screen and scroll down again to the bottom of the screen, somehow the child RecyclerView shows wrong layout / View Types.

parent_layout.xml

<layout 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">

<data>
    <import type="android.view.View" />

    <variable
        name="viewModel"
        type="home.HomeTabViewModel" />
</data>

<android.support.constraint.ConstraintLayout
    android:id="@+id/cl_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include
        android:id="@+id/v_toolbar"
        layout="@layout/toolbar_home_v3" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_home_content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:clipToPadding="false"
        android:overScrollMode="never"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/v_toolbar" />

</android.support.constraint.ConstraintLayout>
</layout>

child_layout.xml

<layout 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.support.constraint.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/dimens_32dp">

    <TextView
        android:id="@+id/tv_title"
        style="@style/TextSoftBlackBold.20sp"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/dimens_16dp"
        android:layout_marginRight="@dimen/dimens_16dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        tools:text="Popular Ideas" />

    <TextView
        android:id="@+id/tv_subtitle"
        style="@style/TextDarkGrayNormal.14sp"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/dimens_16dp"
        android:layout_marginTop="@dimen/dimens_7dp"
        android:layout_marginRight="@dimen/dimens_16dp"
        android:ellipsize="end"
        android:maxLines="2"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_title"
        tools:text="Popular events happening during your stay in Bali" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_menu_grid"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dimens_2dp"
        app:layout_constraintTop_toBottomOf="@+id/tv_subtitle" />


</android.support.constraint.ConstraintLayout>
</layout>

BaseMenuAdapter.kt

class BaseMenuAdapter(val context: Context?,
                           private var contents: List<HomeViewParam.Content>?,
                  private val menuGrid: HomeItem.MenuGrid?) :
    RecyclerView.Adapter<BaseMenuAdapter.MyViewHolder>() {

companion object {
    const val VIEW_TYPE_LANDSCAPE = 0
    const val VIEW_TYPE_LANDSCAPE_CARD = 1
    const val VIEW_TYPE_PORTRAIT = 2
    const val VIEW_TYPE_TWO_COLUMN = 3
    const val VIEW_TYPE_SQUARE = 4
}

inner class MyViewHolder : RecyclerView.ViewHolder {
    var landscapeBinding: ViewTemplateLandscapeBinding? = null
    var landscapeCardBinding: ViewTemplateLandscapeCardBinding? = null
    var portraitBinding: ItemHomeMenuPortraitBinding? = null
    var twoColumnBinding: ItemHomeMenuTwoColumnBinding? = null
    var squareBinding: ItemHomeMenuSquareBinding? = null


    constructor(binding: ViewTemplateLandscapeBinding) : super(binding.root) {
        landscapeBinding = binding
    }

    constructor(binding: ViewTemplateLandscapeCardBinding) : super(binding.root) {
        landscapeCardBinding = binding
    }

    constructor(binding: ItemHomeMenuPortraitBinding) : super(binding.root) {
        portraitBinding = binding
    }

    constructor(binding: ItemHomeMenuTwoColumnBinding) : super(binding.root) {
        twoColumnBinding = binding
    }

    constructor(binding: ItemHomeMenuSquareBinding) : super(binding.root) {
        squareBinding = binding
    }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseMenuAdapter.MyViewHolder {
    val inflater = LayoutInflater.from(parent.context)
    val binding: ViewDataBinding
    return when (viewType) {
        0 -> {
            binding = DataBindingUtil.inflate(inflater, R.layout.view_template_landscape, parent, false)
            MyViewHolder(binding as ViewTemplateLandscapeBinding)
        }
        1 -> {
            binding = DataBindingUtil.inflate(inflater, R.layout.view_template_landscape_card, parent, false)
            MyViewHolder(binding as ViewTemplateLandscapeCardBinding)
        }
        2 -> {
            binding = DataBindingUtil.inflate(inflater, R.layout.item_home_menu_portrait, parent, false)
            MyViewHolder(binding as ItemHomeMenuPortraitBinding)
        }
        3 -> {
            binding = DataBindingUtil.inflate(inflater, R.layout.item_home_menu_two_column, parent, false)
            MyViewHolder(binding as ItemHomeMenuTwoColumnBinding)
        }
        4 -> {
            binding = DataBindingUtil.inflate(inflater, R.layout.item_home_menu_square, parent, false)
            MyViewHolder(binding as ItemHomeMenuSquareBinding)
        }
        else -> {
            throw RuntimeException("The type has to be ONE or TWO")
        }
    }
}

override fun onBindViewHolder(holder: BaseMenuAdapter.MyViewHolder, position: Int) {
    context?.resources.run {
        when (holder.itemViewType) {
            VIEW_TYPE_LANDSCAPE -> {
                val binding = holder.landscapeBinding
                binding?.run {
                    val item = contents?.let { it[holder.adapterPosition] }
                    binding.content = item
                    root.setOnClickListener { AllWebViewActivityV2.startThisActivity(context, item?.title ?: "",
                            item?.url ?: "") }
                    executePendingBindings()
                }
            }
            VIEW_TYPE_LANDSCAPE_CARD -> {
                val binding = holder.landscapeCardBinding
                binding?.run {
                    val item = contents?.let { it[holder.adapterPosition] }
                    binding.content = item
                    root.setOnClickListener { AllWebViewActivityV2.startThisActivity(context, item?.title ?: "",
                            item?.url ?: "") }
                    executePendingBindings()
                }
            }
            VIEW_TYPE_TWO_COLUMN -> {
                val binding = holder.twoColumnBinding
                binding?.run {
                    val item = contents?.let { it[holder.adapterPosition] }
                    binding.content = item
                    root.setOnClickListener { AllWebViewActivityV2.startThisActivity(context, item?.title ?: "",
                            item?.url ?: "") }
                    executePendingBindings()
                }
            }
            VIEW_TYPE_PORTRAIT -> {
                val binding = holder.portraitBinding
                binding?.run {
                    val item = contents?.let { it[holder.adapterPosition] }
                    binding.content = item
                    root.setOnClickListener { AllWebViewActivityV2.startThisActivity(context, item?.title ?: "",
                            item?.url ?: "") }
                    executePendingBindings()
                }
            }
            VIEW_TYPE_SQUARE -> {
                val binding = holder.squareBinding
                binding?.run {
                    val item = contents?.let { it[holder.adapterPosition] }
                    binding.content = item
                    root.setOnClickListener { AllWebViewActivityV2.startThisActivity(context, item?.title ?: "",
                            item?.url ?: "") }
                    executePendingBindings()
                }
            }
            else -> {}
        }
    }
}

override fun getItemViewType(position: Int): Int {
    contents?.let {
        return when (menuGrid?.templateType) {
            TemplateType.LANDSCAPE -> VIEW_TYPE_LANDSCAPE
            TemplateType.LANDSCAPE_CARD -> VIEW_TYPE_LANDSCAPE_CARD
            TemplateType.TWO_COLUMN -> VIEW_TYPE_TWO_COLUMN
            TemplateType.POTRAIT -> VIEW_TYPE_PORTRAIT
            TemplateType.SQUARE -> VIEW_TYPE_SQUARE
            else -> -1
        }
    }
    return -1
}

override fun getItemCount(): Int {
    return contents?.size ?: 0
}

override fun setHasStableIds(hasStableIds: Boolean) {
    super.setHasStableIds(false)
}

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

fun setItems(contents: List<HomeViewParam.Content>?) {
    this.contents = contents
}
}

EDIT Here's my view looks like enter image description here

So as I've mentioned above, when I scroll up or down, sometimes some items use wrong view type

Upvotes: 2

Views: 242

Answers (1)

Alessio
Alessio

Reputation: 3173

You need to define one ViewHolder for each of your layout types, and then you need to use each ViewHolder's class instance type to bind the data on the fly, respectively in getItemViewType() and onBindViewHolder(), something along these lines:

@Override
public int getItemViewType(int position) {
    if (isValidPosition(position)) {
        Data d = mDataset[position];

        if (d instanceof LandscapeData) {
            return VIEW_TYPE_LANDSCAPE;
        } else if (d instanceof PortraitData) {
            return VIEW_TYPE_PORTRAIT;
        }
        // more else-ifs here
    }
    // default to landscape
    return VIEW_TYPE_LANDSCAPE;
}

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
    Data d = mDataset[position];

    if (holder instanceof LandscapeVH) {
        // binding of dataset for landscape layouts
        LandscapeData data = (LandscapeData) d;
        LandscapeVH vh = (LandscapeVH) holder;
        // bind the data to the view on the fly here
        vh.myTextView.setText(data.getLandscapeTitle());
    } else if (holder instanceof PortraitVH) {
        // binding of dataset for portrait layouts
        PortraitData data = (PortraitData) d;
        PortraitVH vh = (PortraitVH) holder;
        // bind the data to the view on the fly here
        vh.myTextView.setText(data.getPortraitTitle());
    }
    // more else-ifs here
}

Note: your mDataset[] is your only source of truth, and it contains datasets of type Data which can be subclassed to more specialized datasets like LandscapeData and PortraitData which go hand in hand with your layout's types.

Upvotes: 2

Related Questions