使用RecyclerView创建动态列表:

当我们创建一个RecyclerView时。适配器我们必须指定ViewHolder,它将绑定到适配器。

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {

    private String[] mDataset;

    public MyAdapter(String[] myDataset) {
        mDataset = myDataset;
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView mTextView;
        public ViewHolder(TextView v) {
            super(v);
            mTextView = v;
        }
    }

    @Override
    public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.some_layout, parent, false);

        //findViewById...

        ViewHolder vh = new ViewHolder(v);
        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.mTextView.setText(mDataset[position]);
    }

    @Override
    public int getItemCount() {
        return mDataset.length;
    }
}

有可能创建多个视图类型的RecyclerView吗?


当前回答

下面是一个完整的示例,展示了一个具有两种类型的RecyclerView,视图类型由对象决定。

类模型

open class RecyclerViewItem
class SectionItem(val title: String) : RecyclerViewItem()
class ContentItem(val name: String, val number: Int) : RecyclerViewItem()

适配器代码

const val VIEW_TYPE_SECTION = 1
const val VIEW_TYPE_ITEM = 2

class UserAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    var data = listOf<RecyclerViewItem>()

    override fun getItemViewType(position: Int): Int {
        if (data[position] is SectionItem) {
            return VIEW_TYPE_SECTION
        }
        return VIEW_TYPE_ITEM
    }

    override fun getItemCount(): Int {
        return data.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        if (viewType == VIEW_TYPE_SECTION) {
            return SectionViewHolder(
                LayoutInflater.from(parent.context).inflate(R.layout.item_user_section, parent, false)
            )
        }
        return ContentViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_user_content, parent, false)
        )
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = data[position]
        if (holder is SectionViewHolder && item is SectionItem) {
            holder.bind(item)
        }
        if (holder is ContentViewHolder && item is ContentItem) {
            holder.bind(item)
        }
    }

    internal inner class SectionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(item: SectionItem) {
            itemView.text_section.text = item.title
        }
    }

    internal inner class ContentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(item: ContentItem) {
            itemView.text_name.text = item.name
            itemView.text_number.text = item.number.toString()
        }
    }
}

item_user_section.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/text_section"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#eee"
    android:padding="16dp" />

item_user_content.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="32dp">

    <TextView
        android:id="@+id/text_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="Name" />

    <TextView
        android:id="@+id/text_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

示例使用

val dataSet = arrayListOf<RecyclerViewItem>(
    SectionItem("A1"),
    ContentItem("11", 11),
    ContentItem("12", 12),
    ContentItem("13", 13),

    SectionItem("A2"),
    ContentItem("21", 21),
    ContentItem("22", 22),

    SectionItem("A3"),
    ContentItem("31", 31),
    ContentItem("32", 32),
    ContentItem("33", 33),
    ContentItem("33", 34),
)

recyclerAdapter.data = dataSet
recyclerAdapter.notifyDataSetChanged()

其他回答

我首先推荐你阅读Hannes Dorfmann关于这个主题的优秀文章。

当一个新的视图类型出现时,你必须编辑适配器,你必须处理这么多乱七八糟的事情。您的适配器应该对扩展开放,但对修改关闭。

你可以看看这两个项目,他们可以给出如何在适配器中处理不同的ViewTypes的想法:

https://github.com/sockeqwe/AdapterDelegates https://github.com/ibrahimyilmaz/kiel

我有一个更好的解决方案,它允许以声明性和类型安全的方式创建多个视图类型。它是用Kotlin写的,顺便说一下,这真的很棒。

所有所需视图类型的简单视图持有者

class ViewHolderMedium(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val icon: ImageView = itemView.findViewById(R.id.icon) as ImageView
    val label: TextView = itemView.findViewById(R.id.label) as TextView
}

有一个适配器数据项的抽象。注意,视图类型由特定视图持有者类(Kotlin中的KClass)的hashCode表示。

trait AdapterItem {
   val viewType: Int
   fun bindViewHolder(viewHolder: RecyclerView.ViewHolder)
}

abstract class AdapterItemBase<T>(val viewHolderClass: KClass<T>) : AdapterItem {
   override val viewType: Int = viewHolderClass.hashCode()
   abstract fun bindViewHolder(viewHolder: T)
   override fun bindViewHolder(viewHolder: RecyclerView.ViewHolder) {
       bindViewHolder(viewHolder as T)
   }
}

只有bindViewHolder需要在具体的适配器项类中重写(类型安全的方式)。

class AdapterItemMedium(val icon: Drawable, val label: String, val onClick: () -> Unit) : AdapterItemBase<ViewHolderMedium>(ViewHolderMedium::class) {
    override fun bindViewHolder(viewHolder: ViewHolderMedium) {
        viewHolder.icon.setImageDrawable(icon)
        viewHolder.label.setText(label)
        viewHolder.itemView.setOnClickListener { onClick() }
    }
}

这样的AdapterItemMedium对象的列表是适配器的数据源,它实际上接受List<AdapterItem>。见下文。

这个解决方案的重要部分是视图持有者工厂,它将提供特定ViewHolder的新实例:

class ViewHolderProvider {
    private val viewHolderFactories = hashMapOf<Int, Pair<Int, Any>>()

    fun provideViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val (layoutId: Int, f: Any) = viewHolderFactories.get(viewType)
        val viewHolderFactory = f as (View) -> RecyclerView.ViewHolder
        val view = LayoutInflater.from(viewGroup.getContext()).inflate(layoutId, viewGroup, false)
        return viewHolderFactory(view)
    }

    fun registerViewHolderFactory<T>(key: KClass<T>, layoutId: Int, viewHolderFactory: (View) -> T) {
        viewHolderFactories.put(key.hashCode(), Pair(layoutId, viewHolderFactory))
    }
}

简单的适配器类是这样的:

public class MultitypeAdapter(val items: List<AdapterItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

   val viewHolderProvider = ViewHolderProvider() // inject ex Dagger2

   init {
        viewHolderProvider!!.registerViewHolderFactory(ViewHolderMedium::class, R.layout.item_medium, { itemView ->
            ViewHolderMedium(itemView)
        })
   }

   override fun getItemViewType(position: Int): Int {
        return items[position].viewType
    }

    override fun getItemCount(): Int {
        return items.size()
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder? {
        return viewHolderProvider!!.provideViewHolder(viewGroup, viewType)
    }

    override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
        items[position].bindViewHolder(viewHolder)
    }
}

创建一个新的视图类型只需要三个步骤:

创建一个视图持有者类 创建一个适配器项类(从AdapterItemBase扩展) 在ViewHolderProvider中注册视图持有者类

下面是这个概念的一个例子:android-drawer-template。

它甚至更进一步—视图类型充当旋转组件,具有可选择的适配器项。

这是非常简单直接的。

只需在适配器中重写getItemViewType()方法。根据数据返回不同的itemViewType值。例如,考虑一个Person类型的对象,其成员为male,如果isMale为真,则返回1,isMale为假,则在getItemViewType()方法中返回2。

现在来到createViewHolder (ViewGroup parent, int viewType),在不同的viewType的基础上,你可以膨胀不同的布局文件。像下面这样:

 if (viewType == 1){
    View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.male, parent, false);
    return new AdapterMaleViewHolder(view);
}
else{
    View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.female, parent, false);
    return new AdapterFemaleViewHolder(view);
}

在onBindViewHolder (VH holder,int position)检查holder是AdapterFemaleViewHolder或AdapterMaleViewHolder的实例,并相应地分配值。

ViewHolder可能是这样的

    class AdapterMaleViewHolder extends RecyclerView.ViewHolder {
            ...
            public AdapterMaleViewHolder(View itemView){
            ...
            }
        }

    class AdapterFemaleViewHolder extends RecyclerView.ViewHolder {
         ...
         public AdapterFemaleViewHolder(View itemView){
            ...
         }
    }

您可以通过使getItemViewType()返回该位置的预期viewType值来处理multipleViewTypes RecyclerAdapter。

我准备了一个MultipleViewTypeAdapter用于构造一个MCQ列表的考试,它可能抛出一个可能有两个或多个有效答案(复选框选项)和一个单一答案问题(单选按钮选项)的问题。

为此,我从API响应中获得问题的类型,并使用它来决定我必须为该问题显示哪个视图。

public class MultiViewTypeAdapter extends RecyclerView.Adapter {

    Context mContext;
    ArrayList<Question> dataSet;
    ArrayList<String> questions;
    private Object radiobuttontype1;


    //Viewholder to display Questions with checkboxes
    public static class Checkboxtype2 extends RecyclerView.ViewHolder {
        ImageView imgclockcheck;
        CheckBox checkbox;

        public Checkboxtype2(@NonNull View itemView) {
            super(itemView);
            imgclockcheck = (ImageView) itemView.findViewById(R.id.clockout_cbox_image);
            checkbox = (CheckBox) itemView.findViewById(R.id.clockout_cbox);
        }
    }

    //Viewholder to display Questions with radiobuttons

    public static class Radiobuttontype1 extends RecyclerView.ViewHolder {
        ImageView clockout_imageradiobutton;
        RadioButton clockout_radiobutton;
        TextView sample;

        public radiobuttontype1(View itemView) {
            super(itemView);
            clockout_imageradiobutton = (ImageView) itemView.findViewById(R.id.clockout_imageradiobutton);
            clockout_radiobutton = (RadioButton) itemView.findViewById(R.id.clockout_radiobutton);
            sample = (TextView) itemView.findViewById(R.id.sample);
        }
    }

    public MultiViewTypeAdapter(ArrayList<QueDatum> data, Context context) {
        this.dataSet = data;
        this.mContext = context;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
        if (viewType.equalsIgnoreCase("1")) {
            View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.clockout_radio_list_row, viewGroup, false);
            return new radiobuttontype1(view);

        } else if (viewType.equalsIgnoreCase("2")) {
            View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.clockout_cbox_list_row, viewGroup, false);
            view.setHorizontalFadingEdgeEnabled(true);
            return new Checkboxtype2(view);

        } else if (viewType.equalsIgnoreCase("3")) {
            View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.clockout_radio_list_row, viewGroup, false);
            return new Radiobuttontype1(view);

        } else if (viewType.equalsIgnoreCase("4")) {
            View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.clockout_radio_list_row, viewGroup, false);
            return new Radiobuttontype1(view);

        } else if (viewType.equalsIgnoreCase("5")) {
            View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.clockout_radio_list_row, viewGroup, false);
            return new Radiobuttontype1(view);
        }

        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int viewType) {
        if (viewType.equalsIgnoreCase("1")) {
            options =  dataSet.get(i).getOptions();
            question = dataSet.get(i).getQuestion();
            image = options.get(i).getValue();
            ((radiobuttontype1) viewHolder).clockout_radiobutton.setChecked(false);
            ((radiobuttontype1) viewHolder).sample.setText(question);
            //Loading image bitmap in the ViewHolder's View
            Picasso.with(mContext)
                    .load(image)
                    .into(((radiobuttontype1) viewHolder).clockout_imageradiobutton);

        } else if (viewType.equalsIgnoreCase("2")) {
            options = (ArrayList<Clockout_questions_Option>) dataSet.get(i).getOptions();
            question = dataSet.get(i).getQuestion();
            image = options.get(i).getValue();
            //Loading image bitmap in the ViewHolder's View
            Picasso.with(mContext)
                    .load(image)
                    .into(((Checkboxtype2) viewHolder).imgclockcheck);

        } else if (viewType.equalsIgnoreCase("3")) {
            //Fit data to viewHolder for ViewType 3
        } else if (viewType.equalsIgnoreCase("4")) {
            //Fit data to viewHolder for ViewType 4
        } else if (viewType.equalsIgnoreCase("5")) {
            //Fit data to viewHolder for ViewType 5
        }
    }

    @Override
    public int getItemCount() {
        return dataSet.size();
    }

    /**
     * Returns viewType for that position by picking the viewType value from the
     *     dataset
     */
    @Override
    public int getItemViewType(int position) {
        return dataSet.get(position).getViewType();
    }
}

你可以避免在onBindViewHolder()中基于多个条件的viewHolder数据填充,通过为不同位置的viewHolder中相似的视图分配相同的id。

比以往任何时候都简单,忘记ViewTypes。不建议在一个适配器中使用多个视图类型。这将使代码混乱,并打破单一责任原则,因为现在适配器需要处理逻辑来知道要填充哪个视图。

现在,想象一下在大型团队中工作,每个团队都必须在这些视图类型中的一种功能中工作。如果在不同视图类型中工作的所有团队都使用同一个适配器,那将是一团糟。这是使用ConcatAdapter解决的,其中隔离了适配器。逐个编码,然后将它们合并到一个视图中。

在recyclerview:1.2.0-alpha04中,您现在可以使用ConcatAdapter。

如果你需要一个具有不同viewTypes的视图,你可以为每个部分编写适配器,并使用ConcatAdapter将它们合并到一个recyclerview中。

ConcatAdapter

这张图片显示了一个recyclerview拥有的三种不同的视图类型,页眉,内容和页脚。

你只需要为每个section创建一个适配器,然后使用ConcatAdapter将它们合并到一个recyclerview中:

val firstAdapter: FirstAdapter = …
val secondAdapter: SecondAdapter = …
val thirdAdapter: ThirdAdapter = …
val concatAdapter = ConcatAdapter(firstAdapter, secondAdapter,
                                  thirdAdapter)
recyclerView.adapter = concatAdapter

这就是你需要知道的。如果您想处理加载状态,例如在某些加载发生后删除最后一个适配器,您可以使用LoadState。

欲了解更多深入信息,请关注Florina Muntenescu的帖子https://medium.com/androiddevelopers/merge-adapters-sequentially-with-mergeadapter-294d2942127a