1. 程式人生 > >一種優雅的方式實現RecyclerView條目多型別

一種優雅的方式實現RecyclerView條目多型別

下面以一個故事來講來說明這中方式是怎麼實現的。

放棄vlayout

大家都瞭解一般首頁是非常複雜的,去年初專案引入vlayout來解決首頁複雜佈局的問題,後來對vlayout和databinding結合進行了封裝,使用起來更方便簡單,不僅首頁使用,很多頁面都在用,還封裝了單純列表樣式的Activity,重新整理載入的Activity,這樣很開心的過了很久。由於vlayout專案一直比較活躍,在滿足各種各樣的需求上一直在打補丁,我也是一直在把它更新為最新版本,直到又一次更新我的的列表不顯示內容了,經過一上午的排查,找到了問題。是在合併一個同學的PR時引入的,當時我還提了個issue 升級後出現onBindViewHolder未分發的問題

,並給作者提了建議,加強Code Review,其實這時候我就沒有那麼happy了。

在一次需求中,PM提出了可以刪除列表中某一條目的需求,在之前封裝的基礎上很簡單就實現了。這時想加一個移除的動畫吧,讓APP活潑點,不是那麼生硬。這可難住了我,一上午硬是沒搞出來,在別的同學的issue 怎麼正確的使用notifyItemRemoved,正是這個問題,使得我有了放棄使用vlayout的想法。不禁問自己,我為什麼要使用它,沒錯就是為了使複雜佈局更方便管理,現在看來有悖於初衷。也許vlayout有刪除動畫的簡單實現方式,而我沒有找到,但是我決定不再使用它。

尋找輪子

放棄之後面臨的另一個問題是需求還是要做,專案還是要按時上線,冒出了第一個想法是找找其他的輪子吧,

MultiItem github上的介紹是一個優雅的實現多型別的RecyclerView類庫,竊喜,這不正是我想要的。他的思想是給BaseItemAdapter(設定給RecyclerView)註冊一系列的Adapter,然後根據需要處理的類來區分是要選擇哪個被代理的Adapter。

  1. // 初始化adapter

  2. BaseItemAdapter adapter = new BaseItemAdapter();

  3. // 為TextBean資料來源註冊ViewHolderManager管理類

  4. adapter.register(TextBean.class, new TextViewManager());

  5. // 為更多資料來源註冊ViewHolderManager管理類

  6. adapter.register(ImageTextBean.class, new ImageAndTextManager());

  7. adapter.register(ImageBean.class, new ImageViewManager());

  8. // 為RecyclerView設定Adapter

  9. recyclerView.setAdapter(adapter);

我要做的就是快速拿它匹配下我的場景,能不能滿足我的需求。

  1. 是否支援多種型別條目?廢話肯定支援;
  2. 是否支援不同條目不同資料型別?人家就是很久需要處理的資料型別來進行選擇的,肯定沒問題;
  3. 能不能支援一個數據型別對應多個樣式?我看作者也是支援的,即通過資料實體中的標誌來判斷使用哪個Adapter;
  4. 能不能支援一個數據實體對應多個樣式?由於是基於資料的型別進行選擇代理Adapter的,這看來是無法實現。

我的微笑還沒收場,就尷尬的定住了,5秒鐘後,晃過神來,為什麼我不按照這種思想自己封裝一個。說實話我對作者的代理Adapter的管理還是不太滿意的,這種思想很好,還是忍不住給作者點贊,那就自己來擼個輪子吧。

需求迭代

簡單列表

PM在一次迭代過程中提出了要加一個新聞列表的需求,很簡單,就是左邊一個圖片,右邊一個標題、來源、釋出時間,點選可以檢視詳情。

你一看,心中默唸so easy,三下五除二,你就用RecyclerView很快實現了。

activity xml layout

  1. <?xml version="1.0" encoding="utf-8"?>

  2. <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

  3. xmlns:app="http://schemas.android.com/apk/res-auto"

  4. xmlns:tools="http://schemas.android.com/tools"

  5. android:layout_width="match_parent"

  6. android:layout_height="match_parent"

  7. tools:context="com.kevin.myapplication.MainActivity">

  8. <android.support.v7.widget.RecyclerView

  9. android:id="@+id/recycler_view"

  10. android:layout_width="match_parent"

  11. android:layout_height="match_parent"

  12. app:layout_constraintBottom_toBottomOf="parent"

  13. app:layout_constraintLeft_toLeftOf="parent"

  14. app:layout_constraintRight_toRightOf="parent"

  15. app:layout_constraintTop_toTopOf="parent" />

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

實體物件

  1. public class News {

  2. public String imgUrl = "";

  3. public String content = "";

  4. public String source = "";

  5. public String time = "";

  6. public String link = "";

  7. }

adapter

  1. public class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {

  2. private List<News> dataItems = new ArrayList<>();

  3. public void setDataItems(List<News> dataItems) {

  4. this.dataItems = dataItems;

  5. notifyDataSetChanged();

  6. }

  7. @Override

  8. public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

  9. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news, parent, false);

  10. ViewHolder holder = new ViewHolder(view);

  11. return holder;

  12. }

  13. @Override

  14. public void onBindViewHolder(ViewHolder holder, int position) {

  15. News news = dataItems.get(position);

  16. holder.tvContent.setText(news.content);

  17. holder.tvSource.setText(news.source);

  18. holder.tvTime.setText(news.time);

  19. Glide.with(holder.itemView.getContext()).load(news.imgUrl).into(holder.ivPic);

  20. }

  21. @Override

  22. public int getItemCount() {

  23. return dataItems.size();

  24. }

  25. static class ViewHolder extends RecyclerView.ViewHolder {

  26. ImageView ivPic;

  27. TextView tvContent;

  28. TextView tvSource;

  29. TextView tvTime;

  30. public ViewHolder(View view) {

  31. super(view);

  32. ivPic = view.findViewById(R.id.iv_pic);

  33. tvContent = view.findViewById(R.id.tv_content);

  34. tvSource = view.findViewById(R.id.tv_source);

  35. tvTime = view.findViewById(R.id.tv_time);

  36. }

  37. }

  38. }

adapter item layout

  1. <?xml version="1.0" encoding="utf-8"?>

  2. <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

  3. xmlns:app="http://schemas.android.com/apk/res-auto"

  4. xmlns:tools="http://schemas.android.com/tools"

  5. android:layout_width="match_parent"

  6. android:layout_height="wrap_content"

  7. android:paddingLeft="10dp"

  8. android:paddingRight="10dp"

  9. android:paddingTop="10dp">

  10. <ImageView

  11. android:id="@+id/iv_pic"

  12. android:layout_width="0dp"

  13. android:layout_height="80dp"

  14. android:scaleType="centerCrop"

  15. app:layout_constraintHorizontal_weight="1"

  16. app:layout_constraintLeft_toLeftOf="parent"

  17. app:layout_constraintRight_toLeftOf="@+id/tv_content"

  18. app:layout_constraintTop_toTopOf="parent"

  19. tools:src="@mipmap/ic_launcher" />

  20. <TextView

  21. android:id="@+id/tv_content"

  22. android:layout_width="0dp"

  23. android:layout_height="wrap_content"

  24. android:layout_marginLeft="10dp"

  25. android:ellipsize="end"

  26. android:maxLines="2"

  27. android:textColor="#333333"

  28. android:textSize="18sp"

  29. app:layout_constraintHorizontal_chainStyle="spread"

  30. app:layout_constraintHorizontal_weight="2"

  31. app:layout_constraintLeft_toRightOf="@+id/iv_pic"

  32. app:layout_constraintRight_toRightOf="parent"

  33. app:layout_constraintTop_toTopOf="parent"

  34. tools:text="這是一條新聞,這是一條新聞,這是一條新聞" />

  35. <TextView

  36. android:id="@+id/tv_source"

  37. android:layout_width="wrap_content"

  38. android:layout_height="wrap_content"

  39. android:layout_marginLeft="10dp"

  40. android:textColor="#888888"

  41. android:textSize="12sp"

  42. app:layout_constraintBottom_toBottomOf="@+id/iv_pic"

  43. app:layout_constraintLeft_toRightOf="@+id/iv_pic"

  44. tools:text="澎湃新聞" />

  45. <TextView

  46. android:id="@+id/tv_time"

  47. android:layout_width="wrap_content"

  48. android:layout_height="wrap_content"

  49. android:layout_marginLeft="8dp"

  50. android:textColor="#888888"

  51. android:textSize="12sp"

  52. app:layout_constraintBottom_toBottomOf="@+id/iv_pic"

  53. app:layout_constraintLeft_toRightOf="@+id/tv_source"

  54. tools:text="07:33" />

  55. <View

  56. android:layout_width="0dp"

  57. android:layout_height="1px"

  58. android:layout_margin="10dp"

  59. android:background="#EEEEEE"

  60. app:layout_constraintTop_toBottomOf="@id/iv_pic" />

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

看一下實現,還不錯的樣子。

擴充樣式

某天產品經理找到你說,只有一張圖片的看著太單調了,能不能擴充出另外一種樣式,一張圖片的時候還是原來的樣子,如果三張圖片的時候上面是標題,下面是圖片。你覺得沒什麼,也比較好實現,就沒有做任何反抗去做了。

應該是這樣,在之前一個圖片的ViewHolder基礎上擴充套件一個三個圖片的ViewHoder,通過實體物件的圖片數量進行區分是選擇哪一個ViewHolder,不同的ViewHolder繫結不同的資料。

修改實體物件

把原來的String型別的圖片資料改為List<String>的集合。

  1. public class News {

  2. public List<String> imgUrls = null;

  3. public String content = "";

  4. public String source = "";

  5. public String time = "";

  6. public String link = "";

  7. }

新樣式的xml layout

  1. <?xml version="1.0" encoding="utf-8"?>

  2. <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

  3. xmlns:app="http://schemas.android.com/apk/res-auto"

  4. xmlns:tools="http://schemas.android.com/tools"

  5. android:layout_width="match_parent"

  6. android:layout_height="wrap_content"

  7. android:paddingLeft="10dp"

  8. android:paddingRight="10dp"

  9. android:paddingTop="10dp">

  10. <TextView

  11. android:id="@+id/tv_content"

  12. android:layout_width="0dp"

  13. android:layout_height="wrap_content"

  14. android:ellipsize="end"

  15. android:maxLines="2"

  16. android:textColor="#333333"

  17. android:textSize="18sp"

  18. app:layout_constraintLeft_toLeftOf="parent"

  19. app:layout_constraintRight_toRightOf="parent"

  20. app:layout_constraintTop_toTopOf="parent"

  21. tools:text="這是一條新聞,這是一條新聞,這是一條新聞" />

  22. <ImageView

  23. android:id="@+id/iv_pic1"

  24. android:layout_width="0dp"

  25. android:layout_height="80dp"

  26. android:layout_marginTop="10dp"

  27. android:scaleType="centerCrop"

  28. app:layout_constraintHorizontal_weight="1"

  29. app:layout_constraintLeft_toLeftOf="parent"

  30. app:layout_constraintRight_toLeftOf="@+id/iv_pic2"

  31. app:layout_constraintTop_toBottomOf="@+id/tv_content"

  32. tools:src="@mipmap/ic_launcher" />

  33. <ImageView

  34. android:id="@+id/iv_pic2"

  35. android:layout_width="0dp"

  36. android:layout_height="80dp"

  37. android:layout_marginLeft="4dp"

  38. android:layout_marginTop="10dp"

  39. android:scaleType="centerCrop"

  40. app:layout_constraintHorizontal_weight="1"

  41. app:layout_constraintLeft_toRightOf="@+id/iv_pic1"

  42. app:layout_constraintRight_toLeftOf="@+id/iv_pic3"

  43. app:layout_constraintTop_toBottomOf="@+id/tv_content"

  44. tools:src="@mipmap/ic_launcher" />

  45. <ImageView

  46. android:id="@+id/iv_pic3"

  47. android:layout_width="0dp"

  48. android:layout_height="80dp"

  49. android:layout_marginLeft="4dp"

  50. android:layout_marginTop="10dp"

  51. android:scaleType="centerCrop"

  52. app:layout_constraintHorizontal_weight="1"

  53. app:layout_constraintLeft_toRightOf="@+id/iv_pic2"

  54. app:layout_constraintRight_toRightOf="parent"

  55. app:layout_constraintTop_toBottomOf="@+id/tv_content"

  56. tools:src="@mipmap/ic_launcher" />

  57. <TextView

  58. android:id="@+id/tv_source"

  59. android:layout_width="wrap_content"

  60. android:layout_height="wrap_content"

  61. android:layout_marginTop="10dp"

  62. android:textColor="#888888"

  63. android:textSize="12sp"

  64. app:layout_constraintLeft_toLeftOf="parent"

  65. app:layout_constraintTop_toBottomOf="@+id/iv_pic1"

  66. tools:text="澎湃新聞" />

  67. <TextView

  68. android:id="@+id/tv_time"

  69. android:layout_width="wrap_content"

  70. android:layout_height="wrap_content"

  71. android:layout_marginLeft="8dp"

  72. android:textColor="#888888"

  73. android:textSize="12sp"

  74. app:layout_constraintLeft_toRightOf="@+id/tv_source"

  75. app:layout_constraintTop_toTopOf="@+id/tv_source"

  76. tools:text="07:33" />

  77. <View

  78. android:layout_width="0dp"

  79. android:layout_height="1px"

  80. android:layout_margin="10dp"

  81. android:background="#EEEEEE"

  82. app:layout_constraintTop_toBottomOf="@id/tv_source" />

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

Adapter改造

複寫getItemViewType方法,通過判斷圖片的個數是不是3個來區分樣式。如果是則ViewType為1,如果不是則ViewType為0。

  1. @Override

  2. public int getItemViewType(int position) {

  3. News news = dataItems.get(position);

  4. boolean isThreePic = news.imgUrls.size() == 3;

  5. int viewType = isThreePic ? 1 : 0;

  6. return viewType;

  7. }

增加三張圖片樣式的ViewHolder

  1. static class ThreePicViewHolder extends RecyclerView.ViewHolder {

  2. ImageView ivPic1;

  3. ImageView ivPic2;

  4. ImageView ivPic3;

  5. TextView tvContent;

  6. TextView tvSource;

  7. TextView tvTime;

  8. public ThreePicViewHolder(View view) {

  9. super(view);

  10. ivPic1 = view.findViewById(R.id.iv_pic1);

  11. ivPic2 = view.findViewById(R.id.iv_pic2);

  12. ivPic3 = view.findViewById(R.id.iv_pic3);

  13. tvContent = view.findViewById(R.id.tv_content);

  14. tvSource = view.findViewById(R.id.tv_source);

  15. tvTime = view.findViewById(R.id.tv_time);

  16. }

  17. }

在onCreateViewHolder中,通過判斷ViewType的型別,建立對應的ViewHolder。

  1. @Override

  2. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

  3. if (viewType == 0) {

  4. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news, parent, false);

  5. OnePicViewHolder holder = new OnePicViewHolder(view);

  6. return holder;

  7. } else {

  8. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news_extend, parent, false);

  9. ThreePicViewHolder holder = new ThreePicViewHolder(view);

  10. return holder;

  11. }

  12. }

在onBindingViewHolder中,通過判斷ViewType的型別,繫結對應的資料到控制元件。

  1. @Override

  2. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

  3. int viewType = holder.getItemViewType();

  4. if (viewType == 0) {

  5. OnePicViewHolder viewHolder = (OnePicViewHolder) holder;

  6. News news = dataItems.get(position);

  7. viewHolder.tvContent.setText(news.content);

  8. viewHolder.tvSource.setText(news.source);

  9. viewHolder.tvTime.setText(news.time);

  10. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(0)).into(viewHolder.ivPic);

  11. } else {

  12. ThreePicViewHolder viewHolder = (ThreePicViewHolder) holder;

  13. News news = dataItems.get(position);

  14. viewHolder.tvContent.setText(news.content);

  15. viewHolder.tvSource.setText(news.source);

  16. viewHolder.tvTime.setText(news.time);

  17. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(0)).into(viewHolder.ivPic1);

  18. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(1)).into(viewHolder.ivPic2);

  19. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(2)).into(viewHolder.ivPic3);

  20. }

  21. }

OK,很快就完成了,這次的需求,看了下效果,還可以。但是沒有第一次那麼開心了,隱隱感覺這裡的程式碼有點噁心。

又擴充樣式

過了一段時間,產品又找到你,說想增加一種圖集型別的樣式,上面是標題,下面是一張大圖,點開是可以左右滑動翻頁的圖集。說完一張呆萌臉問你是不是很簡單,你想到又要加一種型別的ViewHolder,而之前的實現已經讓你不爽。你說:倒是不復雜,但是你也不能無限制的加啊。抱怨完了,還是要做的。

新增xml佈局

  1. <?xml version="1.0" encoding="utf-8"?>

  2. <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

  3. xmlns:app="http://schemas.android.com/apk/res-auto"

  4. xmlns:tools="http://schemas.android.com/tools"

  5. android:layout_width="match_parent"

  6. android:layout_height="wrap_content"

  7. android:paddingLeft="10dp"

  8. android:paddingRight="10dp"

  9. android:paddingTop="10dp">

  10. <TextView

  11. android:id="@+id/tv_content"

  12. android:layout_width="0dp"

  13. android:layout_height="wrap_content"

  14. android:ellipsize="end"

  15. android:maxLines="2"

  16. android:textColor="#333333"

  17. android:textSize="18sp"

  18. app:layout_constraintLeft_toLeftOf="parent"

  19. app:layout_constraintRight_toRightOf="parent"

  20. app:layout_constraintTop_toTopOf="parent"

  21. tools:text="這是一條新聞,這是一條新聞,這是一條新聞" />

  22. <ImageView

  23. android:id="@+id/iv_pic"

  24. android:layout_width="0dp"

  25. android:layout_height="0dp"

  26. android:layout_marginTop="10dp"

  27. android:scaleType="centerCrop"

  28. app:layout_constraintDimensionRatio="5:3"

  29. app:layout_constraintLeft_toLeftOf="parent"

  30. app:layout_constraintRight_toRightOf="parent"

  31. app:layout_constraintTop_toBottomOf="@+id/tv_content"

  32. tools:src="@mipmap/ic_launcher" />

  33. <TextView

  34. android:id="@+id/tv_count"

  35. android:layout_width="wrap_content"

  36. android:layout_height="wrap_content"

  37. android:drawableLeft="@mipmap/icon_img"

  38. android:drawablePadding="8dp"

  39. android:paddingBottom="6dp"

  40. android:paddingRight="10dp"

  41. android:textColor="@android:color/white"

  42. app:layout_constraintBottom_toBottomOf="@+id/iv_pic"

  43. app:layout_constraintRight_toRightOf="@+id/iv_pic"

  44. tools:text="圖3" />

  45. <TextView

  46. android:id="@+id/tv_source"

  47. android:layout_width="wrap_content"

  48. android:layout_height="wrap_content"

  49. android:layout_marginTop="10dp"

  50. android:textColor="#888888"

  51. android:textSize="12sp"

  52. app:layout_constraintLeft_toLeftOf="parent"

  53. app:layout_constraintTop_toBottomOf="@+id/iv_pic"

  54. tools:text="澎湃新聞" />

  55. <TextView

  56. android:id="@+id/tv_time"

  57. android:layout_width="wrap_content"

  58. android:layout_height="wrap_content"

  59. android:layout_marginLeft="8dp"

  60. android:textColor="#888888"

  61. android:textSize="12sp"

  62. app:layout_constraintLeft_toRightOf="@+id/tv_source"

  63. app:layout_constraintTop_toTopOf="@+id/tv_source"

  64. tools:text="07:33" />

  65. <View

  66. android:layout_width="0dp"

  67. android:layout_height="1px"

  68. android:layout_margin="10dp"

  69. android:background="#EEEEEE"

  70. app:layout_constraintTop_toBottomOf="@id/tv_source" />

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

又改造Adapter

複寫getItemViewType方法,通過判斷圖片的個數區分樣式。

  1. @Override

  2. public int getItemViewType(int position) {

  3. News news = dataItems.get(position);

  4. int imgSize = news.imgUrls.size();

  5. if (imgSize < 3) {

  6. return 0;

  7. } else if (imgSize == 3) {

  8. return 2;

  9. } else {

  10. return 3;

  11. }

  12. }

增加圖集樣式的ViewHolder

  1. static class MorePicViewHolder extends RecyclerView.ViewHolder {

  2. ImageView ivPic;

  3. TextView tvContent;

  4. TextView tvCount;

  5. TextView tvSource;

  6. TextView tvTime;

  7. public MorePicViewHolder(View view) {

  8. super(view);

  9. ivPic = view.findViewById(R.id.iv_pic);

  10. tvContent = view.findViewById(R.id.tv_content);

  11. tvCount = view.findViewById(R.id.tv_count);

  12. tvSource = view.findViewById(R.id.tv_source);

  13. tvTime = view.findViewById(R.id.tv_time);

  14. }

  15. }

在onCreateViewHolder中,通過判斷ViewType的型別,建立對應的ViewHolder。

  1. @Override

  2. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

  3. if (viewType == 0) {

  4. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news_one_pic, parent, false);

  5. OnePicViewHolder holder = new OnePicViewHolder(view);

  6. return holder;

  7. } else if (viewType == 2) {

  8. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news_three_pic, parent, false);

  9. ThreePicViewHolder holder = new ThreePicViewHolder(view);

  10. return holder;

  11. } else if (viewType == 3) {

  12. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news_more_pic, parent, false);

  13. MorePicViewHolder holder = new MorePicViewHolder(view);

  14. return holder;

  15. } else {

  16. // Can't reach;

  17. return null;

  18. }

  19. }

在onBindingViewHolder中,通過判斷ViewType的型別,繫結對應的資料到控制元件。

  1. @Override

  2. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

  3. int viewType = holder.getItemViewType();

  4. if (viewType == 0) {

  5. OnePicViewHolder viewHolder = (OnePicViewHolder) holder;

  6. News news = dataItems.get(position);

  7. viewHolder.tvContent.setText(news.content);

  8. viewHolder.tvSource.setText(news.source);

  9. viewHolder.tvTime.setText(news.time);

  10. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(0)).into(viewHolder.ivPic);

  11. } else if (viewType == 2) {

  12. ThreePicViewHolder viewHolder = (ThreePicViewHolder) holder;

  13. News news = dataItems.get(position);

  14. viewHolder.tvContent.setText(news.content);

  15. viewHolder.tvSource.setText(news.source);

  16. viewHolder.tvTime.setText(news.time);

  17. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(0)).into(viewHolder.ivPic1);

  18. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(1)).into(viewHolder.ivPic2);

  19. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(2)).into(viewHolder.ivPic3);

  20. } else if (viewType == 3) {

  21. MorePicViewHolder viewHolder = (MorePicViewHolder) holder;

  22. News news = dataItems.get(position);

  23. viewHolder.tvContent.setText(news.content);

  24. viewHolder.tvSource.setText(news.source);

  25. viewHolder.tvTime.setText(news.time);

  26. viewHolder.tvCount.setText(news.count + " 圖");

  27. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(0)).into(viewHolder.ivPic);

  28. }

  29. }

OK,很快又改完了,這次你的心情不太愉快,之前的太不利於擴充套件了,每次新增一種新的都要把Adapter改一遍。

反思

在上次需求上線之後,你開始反思,為什麼我這麼反感產品擴充樣式,而不是我的程式碼有一定的業務相容性,而是把自己搞的這麼被動。算了,下樓喝杯咖啡冷靜冷靜。你順便叫了下旁邊的哥們,他說:“我比較忙,你給我帶杯吧。”,“帶什麼?”你問,“摩卡小杯”。你心想,本來叫你一塊兒去的,你小子這麼懶叫我給你帶。唉,對啊,我的Adapter不就可以這樣嘛,自己不做處理,委託給其他的Adapter去做。

一開始的時候註冊三個的委託Adapter物件,A委託Adapter可以處理一張圖片的樣式,B委託Adapter可以處理三張圖片的樣式,C委託可以處理多張圖片的樣式。根據資料裡面圖片的數量選擇對應的委託Adapter去處理就可以啦。

委託

所有的委託都有相同的方法,為了方便定義一個介面。

  1. public interface IDelegateAdapter {

  2. // 查詢委託時呼叫的方法,返回自己能處理的型別即可。

  3. boolean isForViewType(News news);

  4. // 用於委託Adapter的onCreateViewHolder方法

  5. RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);

  6. // 用於委託Adapter的onBindViewHolder方法

  7. void onBindViewHolder(RecyclerView.ViewHolder holder, int position, News news);

  8. }

然後是三個委託

  1. public class OnePicDelegateAdapter implements IDelegateAdapter {

  2. @Override

  3. public boolean isForViewType(News news) {

  4. // 我能處理一張圖片

  5. return news.imgUrls.size() == 1;

  6. }

  7. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

  8. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news_one_pic, parent, false);

  9. OnePicViewHolder holder = new OnePicViewHolder(view);

  10. return holder;

  11. }

  12. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, News news) {

  13. OnePicViewHolder viewHolder = (OnePicViewHolder) holder;

  14. viewHolder.tvContent.setText(news.content);

  15. viewHolder.tvSource.setText(news.source);

  16. viewHolder.tvTime.setText(news.time);

  17. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(0)).into(viewHolder.ivPic);

  18. }

  19. static class OnePicViewHolder extends RecyclerView.ViewHolder {

  20. ImageView ivPic;

  21. TextView tvContent;

  22. TextView tvSource;

  23. TextView tvTime;

  24. public OnePicViewHolder(View view) {

  25. super(view);

  26. ivPic = view.findViewById(R.id.iv_pic);

  27. tvContent = view.findViewById(R.id.tv_content);

  28. tvSource = view.findViewById(R.id.tv_source);

  29. tvTime = view.findViewById(R.id.tv_time);

  30. }

  31. }

  32. }

  1. public class ThreePicDelegateAdapter implements IDelegateAdapter {

  2. @Override

  3. public boolean isForViewType(News news) {

  4. // 我能處理三張圖片

  5. return news.imgUrls.size() == 3;

  6. }

  7. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

  8. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news_three_pic, parent, false);

  9. ThreePicViewHolder holder = new ThreePicViewHolder(view);

  10. return holder;

  11. }

  12. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, News news) {

  13. ThreePicViewHolder viewHolder = (ThreePicViewHolder) holder;

  14. viewHolder.tvContent.setText(news.content);

  15. viewHolder.tvSource.setText(news.source);

  16. viewHolder.tvTime.setText(news.time);

  17. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(0)).into(viewHolder.ivPic1);

  18. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(1)).into(viewHolder.ivPic2);

  19. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(2)).into(viewHolder.ivPic3);

  20. }

  21. static class ThreePicViewHolder extends RecyclerView.ViewHolder {

  22. ImageView ivPic1;

  23. ImageView ivPic2;

  24. ImageView ivPic3;

  25. TextView tvContent;

  26. TextView tvSource;

  27. TextView tvTime;

  28. public ThreePicViewHolder(View view) {

  29. super(view);

  30. ivPic1 = view.findViewById(R.id.iv_pic1);

  31. ivPic2 = view.findViewById(R.id.iv_pic2);

  32. ivPic3 = view.findViewById(R.id.iv_pic3);

  33. tvContent = view.findViewById(R.id.tv_content);

  34. tvSource = view.findViewById(R.id.tv_source);

  35. tvTime = view.findViewById(R.id.tv_time);

  36. }

  37. }

  38. }

  1. public class MorePicDelegateAdapter implements IDelegateAdapter {

  2. @Override

  3. public boolean isForViewType(News news) {

  4. // 我能處理多張圖片

  5. return news.imgUrls.size() > 3;

  6. }

  7. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

  8. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news_more_pic, parent, false);

  9. MorePicViewHolder holder = new MorePicViewHolder(view);

  10. return holder;

  11. }

  12. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, News news) {

  13. MorePicViewHolder viewHolder = (MorePicViewHolder) holder;

  14. viewHolder.tvContent.setText(news.content);

  15. viewHolder.tvSource.setText(news.source);

  16. viewHolder.tvTime.setText(news.time);

  17. viewHolder.tvCount.setText(news.count + " 圖");

  18. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(0)).into(viewHolder.ivPic);

  19. }

  20. static class MorePicViewHolder extends RecyclerView.ViewHolder {

  21. ImageView ivPic;

  22. TextView tvContent;

  23. TextView tvCount;

  24. TextView tvSource;

  25. TextView tvTime;

  26. public MorePicViewHolder(View view) {

  27. super(view);

  28. ivPic = view.findViewById(R.id.iv_pic);

  29. tvContent = view.findViewById(R.id.tv_content);

  30. tvCount = view.findViewById(R.id.tv_count);

  31. tvSource = view.findViewById(R.id.tv_source);

  32. tvTime = view.findViewById(R.id.tv_time);

  33. }

  34. }

  35. }

再看下之前冗餘的Adapter,現在已經非常清瘦了

  1. public class NewsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

  2. private List<News> dataItems = new ArrayList<>();

  3. public void setDataItems(List<News> dataItems) {

  4. this.dataItems = dataItems;

  5. notifyDataSetChanged();

  6. }

  7. List<IDelegateAdapter> delegateAdapters = new ArrayList<>();

  8. public void addDelegate(IDelegateAdapter delegateAdapter) {

  9. delegateAdapters.add(delegateAdapter);

  10. }

  11. @Override

  12. public int getItemViewType(int position) {

  13. }

  14. @Override

  15. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

  16. }

  17. @Override

  18. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

  19. }

  20. @Override

  21. public int getItemCount() {

  22. return dataItems.size();

  23. }

  24. }

有三個方法需要我們去填空,在之前的圖中我們說過,getItemViewType通過委託在集合中的index去標識。

  1. @Override

  2. public int getItemViewType(int position) {

  3. // 找到當前位置的資料

  4. News news = dataItems.get(position);

  5. // 遍歷所有的代理,問下他們誰能處理

  6. for (IDelegateAdapter delegateAdapter : delegateAdapters) {

  7. if (delegateAdapter.isForViewType(news)) {

  8. // 誰能處理返回他的index

  9. return delegateAdapters.indexOf(delegateAdapter);

  10. }

  11. }

  12. throw new RuntimeException("沒有找到可以處理的委託Adapter");

  13. }

然後是onCreateViewHolder,既然是通過委託Adapter的在集合中的index去標記的ViewType,那麼在onCreateViewHolder中就非常簡單了。

  1. @Override

  2. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

  3. // 找到對應的委託Adapter

  4. IDelegateAdapter delegateAdapter = delegateAdapters.get(viewType);

  5. // 把onCreateViewHolder交給委託Adapter去處理

  6. RecyclerView.ViewHolder viewHolder = delegateAdapter.onCreateViewHolder(parent, viewType);

  7. return viewHolder;

  8. }

接下來是onBindViewHolder,類似onCreateViewHolder,這裡也是找到委託Adapter,交給他去處理。

  1. @Override

  2. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

  3. // 找到當前ViewHolder的ViewType,也就是委託Adapter在集合中的index

  4. int viewType = holder.getItemViewType();

  5. // 找到對應的委託Adapter

  6. IDelegateAdapter delegateAdapter = delegateAdapters.get(viewType);

  7. // 把onBindViewHolder交給委託Adapter去處理

  8. delegateAdapter.onBindViewHolder(holder, position, dataItems.get(position));

  9. }

使用的時候也非常簡單,在之前的基礎上註冊委託Adapter就可以了。

  1. RecyclerView recyclerView = findViewById(R.id.recycler_view);

  2. LinearLayoutManager layoutManager = new LinearLayoutManager(this);

  3. recyclerView.setLayoutManager(layoutManager);

  4. NewsAdapter mAdapter = new NewsAdapter();

  5. // 新增委託Adapter

  6. mAdapter.addDelegate(new OnePicDelegateAdapter());

  7. mAdapter.addDelegate(new ThreePicDelegateAdapter());

  8. mAdapter.addDelegate(new MorePicDelegateAdapter());

  9. recyclerView.setAdapter(mAdapter);

雙擴充樣式

產品經理又來了,說又要新增一種視訊型別的樣式。你爽快地說:“沒問題”。

之前一直是通過圖片的個數來判斷是那種形式,但這次明顯不能這麼幹了,修改原來的實體,新增一個type來標識是那種型別。

  1. public class News {

  2. public int type = 0; // 0:一張圖片;1:三張圖片;2:多張圖片;3:視訊型別

  3. public List<String> imgUrls = null;

  4. public String content = "";

  5. public String count = "";

  6. public String duration = "";

  7. public String source = "";

  8. public String time = "";

  9. public String link = "";

  10. }

新增xml佈局

  1. <?xml version="1.0" encoding="utf-8"?>

  2. <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

  3. xmlns:app="http://schemas.android.com/apk/res-auto"

  4. xmlns:tools="http://schemas.android.com/tools"

  5. android:layout_width="match_parent"

  6. android:layout_height="wrap_content"

  7. android:paddingLeft="10dp"

  8. android:paddingRight="10dp"

  9. android:paddingTop="10dp">

  10. <TextView

  11. android:id="@+id/tv_content"

  12. android:layout_width="0dp"

  13. android:layout_height="wrap_content"

  14. android:ellipsize="end"

  15. android:maxLines="2"

  16. android:textColor="#333333"

  17. android:textSize="18sp"

  18. app:layout_constraintLeft_toLeftOf="parent"

  19. app:layout_constraintRight_toRightOf="parent"

  20. app:layout_constraintTop_toTopOf="parent"

  21. tools:text="這是一條新聞,這是一條新聞,這是一條新聞" />

  22. <ImageView

  23. android:id="@+id/iv_pic"

  24. android:layout_width="0dp"

  25. android:layout_height="0dp"

  26. android:layout_marginTop="10dp"

  27. android:scaleType="centerCrop"

  28. app:layout_constraintDimensionRatio="5:3"

  29. app:layout_constraintLeft_toLeftOf="parent"

  30. app:layout_constraintRight_toRightOf="parent"

  31. app:layout_constraintTop_toBottomOf="@+id/tv_content"

  32. tools:src="@mipmap/ic_launcher" />

  33. <ImageView

  34. android:layout_width="48dp"

  35. android:layout_height="48dp"

  36. android:src="@mipmap/play90"

  37. app:layout_constraintBottom_toBottomOf="@+id/iv_pic"

  38. app:layout_constraintLeft_toLeftOf="@+id/iv_pic"

  39. app:layout_constraintRight_toRightOf="@+id/iv_pic"

  40. app:layout_constraintTop_toTopOf="@+id/iv_pic" />

  41. <TextView

  42. android:id="@+id/tv_duration"

  43. android:layout_width="wrap_content"

  44. android:layout_height="wrap_content"

  45. android:paddingBottom="6dp"

  46. android:paddingRight="10dp"

  47. android:textColor="@android:color/white"

  48. app:layout_constraintBottom_toBottomOf="@+id/iv_pic"

  49. app:layout_constraintRight_toRightOf="@+id/iv_pic"

  50. tools:text="12:34" />

  51. <TextView

  52. android:id="@+id/tv_source"

  53. android:layout_width="wrap_content"

  54. android:layout_height="wrap_content"

  55. android:layout_marginTop="10dp"

  56. android:textColor="#888888"

  57. android:textSize="12sp"

  58. app:layout_constraintLeft_toLeftOf="parent"

  59. app:layout_constraintTop_toBottomOf="@+id/iv_pic"

  60. tools:text="澎湃新聞" />

  61. <TextView

  62. android:id="@+id/tv_time"

  63. android:layout_width="wrap_content"

  64. android:layout_height="wrap_content"

  65. android:layout_marginLeft="8dp"

  66. android:textColor="#888888"

  67. android:textSize="12sp"

  68. app:layout_constraintLeft_toRightOf="@+id/tv_source"

  69. app:layout_constraintTop_toTopOf="@+id/tv_source"

  70. tools:text="07:33" />

  71. <View

  72. android:layout_width="0dp"

  73. android:layout_height="1px"

  74. android:layout_margin="10dp"

  75. android:background="#EEEEEE"

  76. app:layout_constraintTop_toBottomOf="@id/tv_source" />

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

新增委託Adapter

  1. public class VideoDelegateAdapter implements IDelegateAdapter {

  2. @Override

  3. public boolean isForViewType(News news) {

  4. // 我能處理視訊型別

  5. return news.type == 3;

  6. }

  7. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

  8. View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_news_video, parent, false);

  9. VideoViewHolder holder = new VideoViewHolder(view);

  10. return holder;

  11. }

  12. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, News news) {

  13. VideoViewHolder viewHolder = (VideoViewHolder) holder;

  14. viewHolder.tvContent.setText(news.content);

  15. viewHolder.tvSource.setText(news.source);

  16. viewHolder.tvTime.setText(news.time);

  17. viewHolder.tvDuration.setText(news.duration);

  18. Glide.with(holder.itemView.getContext()).load(news.imgUrls.get(0)).into(viewHolder.ivPic);

  19. }

  20. static class VideoViewHolder extends RecyclerView.ViewHolder {

  21. ImageView ivPic;

  22. TextView tvContent;

  23. TextView tvDuration;

  24. TextView tvSource;

  25. TextView tvTime;

  26. public VideoViewHolder(View view) {

  27. super(view);

  28. ivPic = view.findViewById(R.id.iv_pic);

  29. tvContent = view.findViewById(R.id.tv_content);

  30. tvDuration = view.findViewById(R.id.tv_duration);

  31. tvSource = view.findViewById(R.id.tv_source);

  32. tvTime = view.findViewById(R.id.tv_time);

  33. }

  34. }

  35. }

添加註冊委託代理

  1. // 新增委託Adapter

  2. mAdapter.addDelegate(new OnePicDelegateAdapter());

  3. mAdapter.addDelegate(new ThreePicDelegateAdapter());

  4. mAdapter.addDelegate(new MorePicDelegateAdapter());

  5. mAdapter.addDelegate(new VideoDelegateAdapter()); // 新新增的視訊型別

經過非常簡單的三步之後,就很清爽地完成了這次需求,大家都很開心。

叒擴充樣式

產品經理再一次找到你,說我們的新聞很火爆,希望加入廣告。每10條加入一個廣告,是不是很好做。你心想,臥槽,這他麼不是坑我麼。沒辦法,產品經理的性子你也知道,這個肯定是要做的。

這麼看來之前的封裝還是不能滿足的,之前的是要求所有的資料都有相同的型別。那能不能加入不同資料型別的支援呢?

看下之前的是通過一個List來儲存託管Adapter的,這樣是無法儲存不同的型別資訊的。

List<IDelegateAdapter> delegateAdapters = new ArrayList<>();

那就再增加一個儲存型別資訊的List吧,為了便於查詢,這裡使用Android提供的SparseArrayCompat。兩個集合定義如下:

  1. // 用於儲存委託Adapter

  2. private SparseArrayCompat<AdapterDelegate<Object>> delegates = new SparseArrayCompat();

  3. // 用於儲存委託Adapter能處理的型別

  4. private SparseArray<String> dataTypes = new SparseArray<>();

  1. public AdapterDelegatesManager addDelegate(AdapterDelegate<Object, VH> delegate) {

  2. Type superclass = delegate.getClass().getGenericSuperclass();

  3. if (superclass instanceof ParameterizedType) {

  4. Class<?> clazz = (Class<?>) ((ParameterizedType) superclass).getActualTypeArguments()[0];

  5. String typeWithTag = clazz.getName();

  6. int viewType = delegates.size();

  7. // Save the delegate to the collection;

  8. delegates.put(viewType, delegate);

  9. // Save the index of the delegate to the collection;

  10. dataTypes.put(viewType, typeWithTag);

  11. } else {

  12. // Has no generics.

  13. throw new IllegalArgumentException(

  14. String.format("Please set the correct generic parameters on %s.", delegate.getClass().getName()));

  15. }

  16. return this;

  17. }

每向delegates中新增一個委託Adapter,則向dataTypes中新增該委託Adapter能處理的型別。比如:向delegates新增一個能處理String型別的委託 AdapterDelegate<String>位置是1,那麼向dataType中新增一個java.lang.String位置也是1。那麼在處理String型別的資料的時候在dataType中查詢到對應的位置為1,就可以去delegate的1位置取對應的委託Adapter就可以啦。

叕來需求

經過以上封裝,你開心地工作了相當長的一段時間。有一天產品又找到你,我們要做一個賬單詳情頁,很簡單,就是顯示他的消費資訊。

你一看,上面是固定的資訊,下面是固定的資訊,中間是一個列表。使用ListView的新增頭部、尾部會非常簡單,但是你又不想使用ListView。能不能使用封裝的RecyclerView委託Adapter實現呢?

  1. public class Bill {

  2. public String title = ""; // 標題

  3. public String waiter = ""; // 服務員

  4. public String cashier = ""; // 收銀員

  5. public int ramadhin = 0; // 桌號

  6. public int guests = 0; // 客人數

  7. public String beginTime = ""; // 開臺時間

  8. public String endTime = ""; // 結賬時間

  9. public String duration = ""; // 用餐時長

  10. public List<Item> details = null; // 用餐詳情

  11. public String total = ""; // 合計

  12. public String discounts = ""; // 優惠

  13. public String receivable = ""; // 應收

  14. public String describe = ""; // 描述資訊

  15. public static class Item {

  16. public String name = ""; // 名稱