自定義Adapter實現RecyclerView的可展開二級列表expand效果
網上實現可展開效果的RecyclerView做法很多,但轉發黨太多,幾乎找不到比較符合效率的做法,其中坑也不少。
想著RecyclerView這麼強大,決定自己研究一個,並基於以下四個原則:
1、作為一個有強迫症的人,我只想僅用一個RecyclerView搞定這個效果,不想任何RecyclerView巢狀GridView或者ListView之類的想著就蛋疼的做法,程式碼也不優美。
2、RecyclerView顯示什麼,它的資料列表應該也跟顯示一致,這樣比較好維護。
3、對item或者是item裡面的一些控制元件的點選處理,希望能在Activity中監聽處理,Adapter中僅對資料進行顯示處理,不涉及複雜的修改資料資訊的操作。
4、設想是通過RecyclerView的新增和移除item方式來做展開和收起的效果,這樣可以利用RecyclerVIewinsert和remove item的動畫效果,還可以自由設定展開的資訊類似GridView的形式。大致效果如下圖:
正式開始實現這個效果:
一、首先定義資料類CourseInfo、ChapterInfo、SectionInfo,這三個都繼承同一個BaseInfo。
public class CourseInfo extends BaseInfo { public int id; public String name; public List<ChapterInfo> chapterInfos = new ArrayList<>(); } public class ChapterInfo extends BaseInfo { public String name; public int chapterIndex; public List<SectionInfo> sectionInfos = new ArrayList<>(); } public class SectionInfo extends BaseInfo { public String name; public int chapterIndex; public int sectionIndex; } public class BaseInfo implements Serializable { }
二、重要的adapter實現方法如下:
之所以前面的資料型別都繼承同一個BaseInfo,就是為了能把不同資料都裝進一個list中。
原理主要是傳入的資料和顯示的資料分開,維護一個顯示資料列表,展開就新增item,收起就移除item,這樣新增和移除都可以利用RecyclerView自身的動畫效果。
當然,如果想更改動畫效果貌似還可以自定義自己的ItemAnimator,這個有空可以研究研究。
package com.ldw.testwork.expandrecyclerview; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.GridView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.ldw.testwork.R; import java.util.ArrayList; import java.util.List; import timber.log.Timber; /** * 一個可展開和收起的RecyclerView資料處理,傳進的資料和顯示的資料分開,展開新增item,收起則刪除item。 * Created by ldw on 2017/12/1. */ public class ChapterAdapter extends RecyclerView.Adapter implements View.OnClickListener { public static final int VIEW_TYPE_CHAPTER = 1; public static final int VIEW_TYPE_SECTION = 2; //傳進來的課程資訊 private CourseInfo courseInfo; //顯示的資料集 private List<BaseInfo> dataInfos = new ArrayList<>(); //當前展開的課時,-1代表沒有任何展開 private int curExpandChapterIndex = -1; public ChapterAdapter(CourseInfo _courseInfo) { this.courseInfo = _courseInfo; for(BaseInfo info : courseInfo.chapterInfos){ dataInfos.add(info); } } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView; if(viewType == VIEW_TYPE_CHAPTER){ itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_chapter, parent, false); return new ItemHolder(itemView); }else{ itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_section, parent, false); return new ItemSectionHolder(itemView); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) { //Timber.v("---onBindViewHolder---position = "+position); if(getItemViewType(position) == VIEW_TYPE_CHAPTER){ ItemHolder itemHolder = (ItemHolder) holder; itemHolder.itemView.setTag(position); itemHolder.tvPractise.setTag(position); ChapterInfo chapterInfo = (ChapterInfo) dataInfos.get(position); itemHolder.tvName.setText(chapterInfo.name); if(chapterInfo.sectionInfos.size() > 0){ itemHolder.ivArrow.setVisibility(View.VISIBLE); if(curExpandChapterIndex == position){ itemHolder.ivArrow.setBackgroundResource(R.drawable.arrow_up); }else{ itemHolder.ivArrow.setBackgroundResource(R.drawable.arrow_down); } }else{ itemHolder.ivArrow.setVisibility(View.INVISIBLE); } }else{ ItemSectionHolder itemSectionHolder = (ItemSectionHolder) holder; itemSectionHolder.tvName.setTag(position); SectionInfo sectionInfo = (SectionInfo) dataInfos.get(position); itemSectionHolder.tvName.setText(sectionInfo.name); } } //該方法只更改itemView的部分資訊,不全部重新整理 @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) { //Timber.v("---onBindViewHolder---payloads = "+payloads + ", "+position); if(payloads.isEmpty()){ super.onBindViewHolder(holder, position, payloads); }else{ String str = (String) payloads.get(0); //更改view的tag if(str.equals("change_position")){ if(getItemViewType(position) == VIEW_TYPE_CHAPTER){ ItemHolder itemHolder = (ItemHolder) holder; itemHolder.itemView.setTag(position); itemHolder.tvPractise.setTag(position); //改變箭頭方向 if(curExpandChapterIndex == position){ itemHolder.ivArrow.setBackgroundResource(R.drawable.arrow_up); }else{ itemHolder.ivArrow.setBackgroundResource(R.drawable.arrow_down); } }else{ ItemSectionHolder itemSectionHolder = (ItemSectionHolder) holder; itemSectionHolder.tvName.setTag(position); } } } } @Override public long getItemId(int i) { return i; } @Override public int getItemCount() { if(dataInfos == null){ return 0; }else{ return dataInfos.size(); } } @Override public int getItemViewType(int position) { if(dataInfos.get(position) instanceof ChapterInfo){ return VIEW_TYPE_CHAPTER; }else if(dataInfos.get(position) instanceof SectionInfo){ return VIEW_TYPE_SECTION; } return super.getItemViewType(position); } public class ItemHolder extends RecyclerView.ViewHolder { public LinearLayout llBg; public ImageView ivArrow; public TextView tvName; public TextView tvPractise; public LinearLayout llSection; public GridView gvSection; public ItemHolder(View itemView) { super(itemView); ivArrow = (ImageView) itemView.findViewById(R.id.iv_item_chapter_arrow); tvName = (TextView) itemView.findViewById(R.id.tv_item_chapter_name); tvPractise = (TextView) itemView.findViewById(R.id.tv_item_chapter_practise); //將建立的View註冊點選事件 itemView.setOnClickListener(ChapterAdapter.this); tvPractise.setOnClickListener(ChapterAdapter.this); } } public class ItemSectionHolder extends RecyclerView.ViewHolder { public TextView tvName; public ItemSectionHolder(View itemView) { super(itemView); tvName = (TextView) itemView.findViewById(R.id.tv_item_section_name); //將建立的View註冊點選事件 tvName.setOnClickListener(ChapterAdapter.this); } } ////////////////////////////以下為item點選處理/////////////////////////////// private OnRecyclerViewItemClickListener mOnItemClickListener = null; public void setOnItemClickListener(OnRecyclerViewItemClickListener listener) { this.mOnItemClickListener = listener; } /** item裡面有多個控制元件可以點選 */ public enum ViewName { CHAPTER_ITEM, CHAPTER_ITEM_PRACTISE, SECTION_ITEM } public interface OnRecyclerViewItemClickListener { void onClick(View view, ViewName viewName, int chapterIndex, int sectionIndex); } @Override public void onClick(View v) { if (mOnItemClickListener != null) { //注意這裡使用getTag方法獲取資料 int position = (int) v.getTag(); ViewName viewName = ViewName.CHAPTER_ITEM; int chapterIndex = -1; int sectionIndex = -1; if(getItemViewType(position) == VIEW_TYPE_CHAPTER){ ChapterInfo chapterInfo = (ChapterInfo) dataInfos.get(position); chapterIndex = chapterInfo.chapterIndex; sectionIndex = -1; if(v.getId() == R.id.tv_item_chapter_practise){ viewName = ViewName.CHAPTER_ITEM_PRACTISE; }else{ viewName = ViewName.CHAPTER_ITEM; if(chapterInfo.sectionInfos.size() > 0){ if(chapterIndex == curExpandChapterIndex){ narrow(curExpandChapterIndex); }else{ narrow(curExpandChapterIndex); expand(chapterIndex); } } } }else if(getItemViewType(position) == VIEW_TYPE_SECTION){ SectionInfo sectionInfo = (SectionInfo) dataInfos.get(position); viewName = ViewName.SECTION_ITEM; chapterIndex = sectionInfo.chapterIndex; sectionIndex = sectionInfo.sectionIndex; } mOnItemClickListener.onClick(v, viewName, chapterIndex, sectionIndex); } } /** * 展開某個item * @param chapterIndex */ private void expand(int chapterIndex){ dataInfos.addAll(chapterIndex+1, courseInfo.chapterInfos.get(chapterIndex).sectionInfos); curExpandChapterIndex = chapterIndex; Timber.v("---expand---"+(chapterIndex+1)+", "+courseInfo.chapterInfos.get(chapterIndex).sectionInfos.size()); notifyItemRangeInserted(chapterIndex+1, courseInfo.chapterInfos.get(chapterIndex).sectionInfos.size()); /*notifyItemRangeChanged(chapterIndex + 1 + courseInfo.chapterInfos.get(chapterIndex).sectionInfos.size(), getItemCount() - chapterIndex - 1, "change_position");*/ notifyItemRangeChanged(0, getItemCount(), "change_position"); } /** * 收起某個item * @param chapterIndex */ private void narrow(int chapterIndex){ if(chapterIndex != -1){ int removeStart = chapterIndex + 1; int removeCount = 0; for(int i=removeStart; i<dataInfos.size() && getItemViewType(i) == VIEW_TYPE_SECTION; i++){ removeCount++; } dataInfos.removeAll(courseInfo.chapterInfos.get(chapterIndex).sectionInfos); curExpandChapterIndex = -1; Timber.v("---narrow---"+removeStart+", "+removeCount); notifyItemRangeRemoved(removeStart, removeCount); //notifyItemRangeChanged(removeStart, getItemCount() - removeStart, "change_position"); notifyItemRangeChanged(0, getItemCount(), "change_position"); } } }
三、佈局檔案,activity只是一個RecyclerView,這裡不貼出來了。
以下是item_chapter.xml檔案:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#eff7e9"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_item_chapter_arrow"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:background="@drawable/arrow_down"/>
<TextView
android:id="@+id/tv_item_chapter_name"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="12sp"
android:paddingStart="5dp"
android:paddingEnd="30dp"
android:maxLines="1"
android:ellipsize="end"
android:gravity="start|center_vertical"
android:text="item_chapter_name"/>
<TextView
android:id="@+id/tv_item_chapter_practise"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:layout_gravity="center_vertical"
android:gravity="center"
android:text="practise"/>
</LinearLayout>
以下是item_section.xml檔案:<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_gravity="center"
android:gravity="center">
<TextView
android:id="@+id/tv_item_section_name"
android:layout_width="80dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:gravity="center"
android:text="section"/>
</RelativeLayout>
四、Activity中的程式碼:
package com.ldw.testwork;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import com.ldw.testwork.expandrecyclerview.ChapterAdapter;
import com.ldw.testwork.expandrecyclerview.ChapterInfo;
import com.ldw.testwork.expandrecyclerview.CourseInfo;
import com.ldw.testwork.expandrecyclerview.SectionInfo;
import com.ldw.testwork.utils.ToastUtil;
import timber.log.Timber;
public class ExpandRecyclerViewActivity extends AppCompatActivity {
RecyclerView mRecyclerView;
CourseInfo mCourseInfo;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_expandrecyclerview);
initData();
initViews();
}
private void initData(){
//假資料
mCourseInfo = new CourseInfo();
mCourseInfo.name = "假裝是課程的名稱";
for(int i=0; i<31; i++){
ChapterInfo chapterInfo = new ChapterInfo();
chapterInfo.name = "假裝是課時名稱"+(i+1);
chapterInfo.chapterIndex = i;
if(i==0){
for(int j=0; j<2; j++){
SectionInfo sectionInfo = new SectionInfo();
sectionInfo.name = "第"+(j+1)+"節";
sectionInfo.chapterIndex = i;
sectionInfo.sectionIndex = j;
chapterInfo.sectionInfos.add(sectionInfo);
}
}else if(i==1){
for(int j=0; j<3; j++){
SectionInfo sectionInfo = new SectionInfo();
sectionInfo.name = "第"+(j+1)+"節";
sectionInfo.chapterIndex = i;
sectionInfo.sectionIndex = j;
chapterInfo.sectionInfos.add(sectionInfo);
}
}else if(i==2){
}else{
for (int j = 0; j < 4; j++) {
SectionInfo sectionInfo = new SectionInfo();
sectionInfo.name = "第" + (j + 1) + "節";
sectionInfo.chapterIndex = i;
sectionInfo.sectionIndex = j;
chapterInfo.sectionInfos.add(sectionInfo);
}
}
mCourseInfo.chapterInfos.add(chapterInfo);
}
}
private void initViews(){
mRecyclerView = (RecyclerView) findViewById(R.id.rv_expand);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
final ChapterAdapter chapterAdapter = new ChapterAdapter(mCourseInfo);
mRecyclerView.setAdapter(chapterAdapter);
chapterAdapter.setOnItemClickListener(new ChapterAdapter.OnRecyclerViewItemClickListener() {
@Override
public void onClick(View view, ChapterAdapter.ViewName viewName, int chapterIndex, int sectionIndex) {
//Timber.v("---onClick---"+viewName+", "+chapterIndex+", "+sectionIndex);
switch (viewName){
case CHAPTER_ITEM:
if(mCourseInfo.chapterInfos.get(chapterIndex).sectionInfos.size() > 0){
Timber.v("---onClick---just expand or narrow: "+chapterIndex);
if(chapterIndex + 1 == mCourseInfo.chapterInfos.size()){
//如果是最後一個,則滾動到展開的最後一個item
mRecyclerView.smoothScrollToPosition(chapterAdapter.getItemCount());
Timber.v("---onClick---scroll to bottom");
}
}else{
onClickChapter(chapterIndex);
}
break;
case CHAPTER_ITEM_PRACTISE:
onClickPractise(chapterIndex);
break;
case SECTION_ITEM:
onClickSection(chapterIndex, sectionIndex);
break;
}
}
});
//以下是對佈局進行控制,讓課時佔據一行,小節每四個佔據一行,結果就是相當於一個ListView巢狀GridView的效果。
final GridLayoutManager manager = new GridLayoutManager(this, 4);
manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return chapterAdapter.getItemViewType(position) == ChapterAdapter.VIEW_TYPE_CHAPTER ? 4 : 1;
}
});
mRecyclerView.setLayoutManager(manager);
}
private void onClickChapter(int chapterIndex){
Timber.v("---onClick---play chapter: "+chapterIndex);
ToastUtil.showToast(ExpandRecyclerViewActivity.this, "播放"+chapterIndex);
}
private void onClickSection(int chapterIndex, int sectionIndex){
Timber.v("---onClick---play---section: "+chapterIndex+", "+sectionIndex);
ToastUtil.showToast(ExpandRecyclerViewActivity.this, "播放"+chapterIndex+", "+sectionIndex);
}
private void onClickPractise(int chapterIndex){
Timber.v("---onClick---practise: "+chapterIndex);
}
}
五、程式碼看似簡單,其中adapter有幾個坑:
1、插入或者移除後其他的item中的position不會變(此position不僅是指我設定的tag那個position),比如原來有4個item,position為0 1 2 3,中間插入兩個item後,position變為0 1 2 3 2 3,非常奇葩,會出現各種問題,當然如果呼叫notifyDataSetChanged();進行重新整理,那不會有什麼問題,只是RecyclerView的新增移除item動畫效果就沒了。網上解決方法千篇一律,重點是還達不到效果。個人的解決方法在2中說明。
2、解決之前,先說明另一個方法public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads),看到沒有,它筆平常使用的多了一個引數payloads,經過了解,它的用途是可以在不用重新整理整個item,而對部分item中的某些資訊進行修改。我們正好利用這個特點,前面說position不會自動更新,那好,我們就呼叫這個方法notifyItemRangeChanged(0, getItemCount(), "change_position");讓它去更新item,這樣既不會重新重新整理整個item,又能保證position的正確性。這樣就完美解決事情了。
六、總結,RecyclerView有以下幾個好東西:
ItemDecoration :設定item間隔
GridLayoutManager :設定顯示佈局,比如上面例子中,哪個item佔據一行,哪個item多個佔據一行,可以方便實現GridView效果。
itemAnimator:設定item新增和移除等的各種效果。
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads):改方法最後一個引數可以設定不同值,例如notifyItemRangeChanged(0, getItemCount(), "change_position");,從而在更新中能只對item中某個資訊進行修改,而不用整個item重新整理。
原創文章,轉載請註明出處:http://blog.csdn.net/lin_dianwei/article/details/78725014
相關推薦
自定義Adapter實現RecyclerView的可展開二級列表expand效果
網上實現可展開效果的RecyclerView做法很多,但轉發黨太多,幾乎找不到比較符合效率的做法,其中坑也不少。 想著RecyclerView這麼強大,決定自己研究一個,並基於以下四個原則: 1、作為一個有強迫症的人,我只想僅用一個RecyclerView搞定這個效果,不想
AutoCompleteTextView與自定義Adapter實現自動補全
AutoCompleteTextView繼承自TextView,是一種可以實現自動補全的功能的TextView。先看效果: 該功能在目前很常見,例如在使用者進行登入的時候或者註冊的時候都用到了這種功能,在Android中這樣的效果我們可以藉助AutoCo
自定義Adapter實現多檢視Item的ListView
From http://www.devdiv.com/adapter_item_listview-article-3730-1.html 1、原理分析 Adapter對於ListView是非常重要的,它處於listView和資料來源的中間,負責為L
Android中Spinner下拉列表(使用ArrayAdapter和自定義Adapter實現)
今天學習了Spinner元件,使用Spinner相當於從下拉列表中選擇專案,下面演示一下Spinner的使用(分別使用ArrayAdapter和自定義Adapter實現) (一):使用Arr
自定義ViewGroup實現多個單頁面上下滑動效果
閱讀過自定義ViewGroup實現仿淘寶的商品詳情頁的童鞋,應該都瞭解了ViewGroup中onMeasure、onLayout、onTouchEvent等相關方法的使用。在介紹仿淘寶商品詳情頁時,我們提到過現在網上很多實現方法是使用ScrollView巢狀兩個
可復用的自定義Adapter
text logs 存儲 group bili this resource list extend public abstract class MyAdapter<T> extends BaseAdapter { private ArrayList&
利用原生js實現自定義滾動條(可點選到達,拖動到達)
1.HTML檔案 div1是滾動條,div2是滾動小球,div3是文字區域容器,div4是文字區域。 <div id="div"> <div id="div1"> <div id="div2"> </div> </div&
RecyclerView自定義LayoutManager實現橫向瀑布流
最近由於公司專案需要,做了一個橫向瀑布流的元件,如下圖;這個元件是通過自定義LayoutManager實現,LayoutManager為我們提供了強大的自定義功能,但是實現過程卻不簡單,搗鼓了兩天,也就算基本可以用了;Demo原始碼在最下面,這裡主要記錄一些自定義Layou
Android 自定義Adapter以實現自定義填充ListView的Item
1>實體類---即ListView中各個Item中填充的內容 package com.demo.cxc.compoundview.com.demo.cxc.entity; import java.text.SimpleDateFormat; import java.
TextView使用自定義HtmlHttpImageGetter實現非同步載入網路圖片,可限制載入圖片數量
import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import androi
Spring+SpringMVC+Mybatis 利用AOP自定義註解實現可配置日
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 5
Android -- 自定義view實現keep歡迎頁倒計時效果
super onfinish -m use new getc awt ttr alt 1,最近打開keep的app的時候,發現它的歡迎頁面的倒計時效果還不錯,所以打算自己來寫寫,然後就有了這篇文章。 2,還是老規矩,先看一下我們今天實現的效果 相較於我們常見的倒計時
Android自定義View——實現水波紋效果類似剩余流量球
string 三個點 pre ber block span 初始化 move 理解 最近突然手癢就想搞個貝塞爾曲線做個水波紋效果玩玩,終於功夫不負有心人最後實現了想要的效果,一起來看下吧: 效果圖鎮樓 一:先一步一步來分解一下實現的過程 需要繪制一個正弦曲線(sin
Android自定義processor實現bindView功能
lis dds 定義 java代碼 cli 註冊 文章 type() mage 一、簡介 在現階段的Android開發中,註解越來越流行起來,比如ButterKnife,Retrofit,Dragger,EventBus等等都選擇使用註解來配置。按照處理時期,註解又分為兩
自定義toast實現
web javascript html5 toast ys_toast.css.ys-toast{ position:fixed; left:0; right:0; top:0; bottom:0; z-index: 999999; } .ys-toast>em{ pos
SpringVC 攔截器+自定義註解 實現權限攔截
json.js 加載 bean media tar attr esp 權限 encoding 1.springmvc配置文件中配置 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://w
自定義ScrollView 實現上拉下拉的回彈效果--並且子控件中有Viewpager的情況
是否 AS abs pri tar utils lda animation ted onInterceptTouchEvent就是對子控件中Viewpager的處理:左右滑動應該讓viewpager消費 1 public class MyScrollView ext
[python]RobotFramework自定義庫實現UI自動化
bubuko output source 自動 封裝 9.png 全局變量 詳細 變量 1.安裝教程 環境搭建不多說,網上資料一大堆,可參考https://www.cnblogs.com/puresoul/p/3854963.html,寫的比較詳細,值得推薦。目前pyt
NPOI+反射+自定義特性實現上傳excel轉List及驗證
type set custom pre script private property xssf don 1.自定義特性 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited
Android bc信用盤搭建自定義behavior 實現上滑 隱藏底部view
退出 Y軸 log rect app sum string dsl oss 布局 <android.support.design.widget.CoordinatorLayout android:layout_width="match_parent"