转载请说明出处:http://www.jianshu.com/p/302d24db5423
今天给大家介绍的是一个可以实现数据分组显示的RecyclerViewAdapter:GroupedRecyclerViewAdapter。它可以很方便的实现RecyclerView的分组显示,并且每个组都可以包含组头、组尾和子项;可以方便实现多种Type类型的列表,可以实现如QQ联系人的列表一样的列表展开收起功能等。下面先让我们看一下它所能够实现的一些效果:
还可以很容易的实时列表的展开收起效果:
可展开收起的列表
以上展示的只是GroupedRecyclerViewAdapter能实现的一些常用效果,其实使用GroupedRecyclerViewAdapter还可以很容易的实现一些更加复杂的列表效果。在我的GroupedRecyclerViewAdapter项目的Demo中给出了上面几种效果的实现例子,并且有详细的注释说明,有兴趣的同学可以到我的GitHub下载源码。下面直接讲解GroupedRecyclerViewAdapter的使用。
**1、引入依赖 **
在Project的build.gradle在添加以下代码
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
在Module的build.gradle在添加以下代码
compile 'com.github.donkingliang:GroupedRecyclerViewAdapter:1.2.0'
2、继承GroupedRecyclerViewAdapter
public class GroupedListAdapter extends GroupedRecyclerViewAdapter {
}
3、实现GroupedRecyclerViewAdapter里的方法
GroupedRecyclerViewAdapter是一个抽象类,它提供了一系列需要子类去实现的方法。
//返回组的数量
public abstract int getGroupCount();
//返回当前组的子项数量
public abstract int getChildrenCount(int groupPosition);
//当前组是否有头部
public abstract boolean hasHeader(int groupPosition);
//当前组是否有尾部
public abstract boolean hasFooter(int groupPosition);
//返回头部的布局id。(如果hasHeader返回false,这个方法不会执行)
public abstract int getHeaderLayout(int viewType);
//返回尾部的布局id。(如果hasFooter返回false,这个方法不会执行)
public abstract int getFooterLayout(int viewType);
//返回子项的布局id。
public abstract int getChildLayout(int viewType);
//绑定头部布局数据。(如果hasHeader返回false,这个方法不会执行)
public abstract void onBindHeaderViewHolder(BaseViewHolder holder, int groupPosition);
//绑定尾部布局数据。(如果hasFooter返回false,这个方法不会执行)
public abstract void onBindFooterViewHolder(BaseViewHolder holder, int groupPosition);
//绑定子项布局数据。
public abstract void onBindChildViewHolder(BaseViewHolder holder,
int groupPosition, int childPosition);
还可是重写GroupedRecyclerViewAdapter方法实现头、尾和子项的多种类型item。效果就像上面的第6张图一样。
//返回头部的viewType。
public int getHeaderViewType(int groupPosition);
//返回尾部的viewType。
public int getFooterViewType(int groupPosition) ;
//返回子项的viewType。
public int getChildViewType(int groupPosition, int childPosition) ;
4、设置点击事件的监听
GroupedRecyclerViewAdapter提供了对列表的点击事件的监听方法。
//设置组头点击事件
public void setOnHeaderClickListener(OnHeaderClickListener listener) {
mOnHeaderClickListener = listener;
}
//设置组尾点击事件
public void setOnFooterClickListener(OnFooterClickListener listener) {
mOnFooterClickListener = listener;
}
// 设置子项点击事件
public void setOnChildClickListener(OnChildClickListener listener) {
mOnChildClickListener = listener;
}
注意事项:
1、对方法重写的注意。
如果我们直接继承RecyclerView.Adapter去实现自己的Adapter时,一般会重写Adapter中的以下几个方法:
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position);
public int getItemCount();
public int getItemViewType(int position);
但如果是使用GroupedRecyclerViewAdapter,就一定不能去重写这几个方法,因为在GroupedRecyclerViewAdapter中已经对这几个方法做了实现,而且是对实现列表分组至关重要的,如果子类重写了这几个方法,可能会破坏GroupedRecyclerViewAdapter的功能。
从前面给出的GroupedRecyclerViewAdapter的方法我们可以看到,这些方法其实就是对应RecyclerView.Adapter的这4个方法的,所以我们直接使用GroupedRecyclerViewAdapter提供的方法即可。
RecyclerView.Adapter中的
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);
对应GroupedRecyclerViewAdapter中的
//返回头部的布局id。(如果hasHeader返回false,这个方法不会执行)
public abstract int getHeaderLayout(int viewType);
//返回尾部的布局id。(如果hasFooter返回false,这个方法不会执行)
public abstract int getFooterLayout(int viewType);
//返回子项的布局id。
public abstract int getChildLayout(int viewType);
这里之所以返回的是布局id而不是ViewHolder ,是因为在GroupedRecyclerViewAdapter项目中已经提供了一个通用的ViewHolder:BaseViewHolder。所以使用者只需要提供布局的id即可,不需要自己去实现ViewHolder。
@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(getLayoutId(mTempPosition, viewType), parent, false);
return new BaseViewHolder(view);
}
private int getLayoutId(int position, int viewType) {
int type = judgeType(position);
if (type == TYPE_HEADER) {
return getHeaderLayout(viewType);
} else if (type == TYPE_FOOTER) {
return getFooterLayout(viewType);
} else if (type == TYPE_CHILD) {
return getChildLayout(viewType);
}
return 0;
}
RecyclerView.Adapter中的
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position);
对应GroupedRecyclerViewAdapter中的
//绑定头部布局数据。(如果hasHeader返回false,这个方法不会执行)
public abstract void onBindHeaderViewHolder(BaseViewHolder holder, int groupPosition);
//绑定尾部布局数据。(如果hasFooter返回false,这个方法不会执行)
public abstract void onBindFooterViewHolder(BaseViewHolder holder, int groupPosition);
//绑定子项布局数据。
public abstract void onBindChildViewHolder(BaseViewHolder holder,
int groupPosition, int childPosition);
RecyclerView.Adapter中的
public int getItemCount();
对应GroupedRecyclerViewAdapter中的
//返回组的数量
public abstract int getGroupCount();
//返回当前组的子项数量
public abstract int getChildrenCount(int groupPosition);
RecyclerView.Adapter中的
public int getItemViewType(int position);
对应GroupedRecyclerViewAdapter中的
//返回头部的viewType。
public int getHeaderViewType(int groupPosition);
//返回尾部的viewType。
public int getFooterViewType(int groupPosition) ;
//返回子项的viewType。
public int getChildViewType(int groupPosition, int childPosition) ;
2、对列表操作的注意
RecyclerView.Adapter提供了一系列对列表进行操作的方法。如:
//更新操作
public final void notifyDataSetChanged();
public final void notifyItemChanged(int position);
public final void notifyItemChanged(int position, Object payload);
public final void notifyItemRangeChanged(int positionStart, int itemCount);
public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload);
//插入操作
public final void notifyItemInserted(int position);
public final void notifyItemRangeInserted(int positionStart, int itemCount);
//删除操作
public final void notifyItemRemoved(int position)
public final void notifyItemRangeRemoved(int positionStart, int itemCount);
在GroupedRecyclerViewAdapter不建议使用RecyclerView.Adapter的任何对列表的操作方法,因为这些方法都是基于列表的操作,它的position是相对于整个列表而言的,而GroupedRecyclerViewAdapter是分组的列表,它对列表的操作应该是基于组的。同时GroupedRecyclerViewAdapter使用了组结构来维护整个列表的结构,使我们可以对列表进行组的操作,在列表发生变化时GroupedRecyclerViewAdapter需要及时对组结构进行调整,如果使用了RecyclerView.Adapter中的方法对列表进行更新,GroupedRecyclerViewAdapter可能因为无法及时调整组结构而发生异常。所以在使用中应该避免使用这些方法。GroupedRecyclerViewAdapter同样提供了一系列对列表进行操作的方法,我们应该使用GroupedRecyclerViewAdapter所提供的方法。
//****** 刷新操作 *****//
//刷新数据列表。对应 notifyDataSetChanged();
public void changeDataSet();
//刷新一组数据,包括组头,组尾和子项
public void changeGroup(int groupPosition);
//刷新多组数据,包括组头,组尾和子项
public void changeRangeGroup(int groupPosition, int count);
// 刷新组头
public void changeHeader(int groupPosition);
//刷新组尾
public void changeFooter(int groupPosition);
// 刷新一组里的某个子项
public void changeChild(int groupPosition, int childPosition);
//刷新一组里的多个子项
public void changeRangeChild(int groupPosition, int childPosition, int count);
// 刷新一组里的所有子项
public void changeChildren(int groupPosition);
//****** 删除操作 *****//
// 删除所有数据
public void removeAll();
//删除一组数据,包括组头,组尾和子项
public void removeGroup(int groupPosition);
// 删除多组数据,包括组头,组尾和子项
public void removeRangeGroup(int groupPosition, int count);
// 删除组头
public void removeHeader(int groupPosition);
// 删除组尾
public void removeFooter(int groupPosition);
//删除一组里的某个子项
public void removeChild(int groupPosition, int childPosition);
// 删除一组里的多个子项
public void removeRangeChild(int groupPosition, int childPosition, int count);
//删除一组里的所有子项
public void removeChildren(int groupPosition);
//****** 插入操作 *****//
// 插入一组数据
public void insertGroup(int groupPosition);
//插入一组数据
public void insertRangeGroup(int groupPosition, int count);
//插入组头
public void insertHeader(int groupPosition);
// 插入组尾
public void insertFooter(int groupPosition);
//插入一个子项到组里
public void insertChild(int groupPosition, int childPosition);
// 插入一组里的多个子项
public void insertRangeChild(int groupPosition, int childPosition, int count);
//插入一组里的所有子项
public void insertChildren(int groupPosition);
3、使用GridLayoutManager的注意
如果要使用GridLayoutManager,一定要使用项目中所提供的GroupedGridLayoutManager。因为分组列表如果要使用GridLayoutManager实现网格布局,就要保证组的头部和尾部是要单独占用一行的。否则组的头、尾可能会跟子项混着一起,造成布局混乱。同时GroupedGridLayoutManager提供了对子项的SpanSize的修改方法,使用GroupedGridLayoutManager可以实现更多的复杂列表布局。
//直接使用GroupedGridLayoutManager实现子项的Grid效果
GroupedGridLayoutManager gridLayoutManager = new GroupedGridLayoutManager(this, 2, adapter);
rvList.setLayoutManager(gridLayoutManager);
GroupedGridLayoutManager gridLayoutManager = new GroupedGridLayoutManager(this, 4, adapter){
//重写这个方法 改变子项的SpanSize。
//这个跟重写SpanSizeLookup的getSpanSize方法的使用是一样的。
@Override
public int getChildSpanSize(int groupPosition, int childPosition) {
if(groupPosition % 2 == 1){
return 2;
}
return super.getChildSpanSize(groupPosition, childPosition);
}
};
rvList.setLayoutManager(gridLayoutManager);
4、BaseViewHolder的使用
项目中提供了一个通用的ViewHolder:BaseViewHolder。提供了根据viewId获取View的方法和对View、TextView、ImageView的常用设置方法。
//根据id获取View
TextView textView = holder.get(R.id.tv_header);
//View、TextView、ImageView的常用设置方法。并且支持方法连缀调用
holder.setText(R.id.tv_header, "内容")
.setImageResource(R.id.iv_image, 资源id)
.setBackgroundRes(R.id.view,资源id);
BaseViewHolder是可以通用的,在普通的Adapter中也可以使用,可以省去每次都要创建ViewHolder的麻烦。
下面看一个简单的使用列子:
public class GroupedListAdapter extends GroupedRecyclerViewAdapter {
private ArrayList<GroupEntity> mGroups;
public GroupedListAdapter(Context context, ArrayList<GroupEntity> groups) {
super(context);
mGroups = groups;
}
@Override
public int getGroupCount() {
return mGroups == null ? 0 : mGroups.size();
}
@Override
public int getChildrenCount(int groupPosition) {
ArrayList<ChildEntity> children = mGroups.get(groupPosition).getChildren();
return children == null ? 0 : children.size();
}
@Override
public boolean hasHeader(int groupPosition) {
return true;
}
@Override
public boolean hasFooter(int groupPosition) {
return true;
}
@Override
public int getHeaderLayout(int viewType) {
return R.layout.adapter_header;
}
@Override
public int getFooterLayout(int viewType) {
return R.layout.adapter_footer;
}
@Override
public int getChildLayout(int viewType) {
return R.layout.adapter_child;
}
@Override
public void onBindHeaderViewHolder(BaseViewHolder holder, int groupPosition) {
GroupEntity entity = mGroups.get(groupPosition);
holder.setText(R.id.tv_header, entity.getHeader());
}
@Override
public void onBindFooterViewHolder(BaseViewHolder holder, int groupPosition) {
GroupEntity entity = mGroups.get(groupPosition);
holder.setText(R.id.tv_footer, entity.getFooter());
}
@Override
public void onBindChildViewHolder(BaseViewHolder holder, int groupPosition, int childPosition) {
ChildEntity entity = mGroups.get(groupPosition).getChildren().get(childPosition);
holder.setText(R.id.tv_child, entity.getChild());
}
}
public class GroupedListActivity extends AppCompatActivity {
private TextView tvTitle;
private RecyclerView rvList;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_group_list);
tvTitle = (TextView) findViewById(R.id.tv_title);
rvList = (RecyclerView) findViewById(R.id.rv_list);
tvTitle.setText(R.string.group_list);
rvList.setLayoutManager(new LinearLayoutManager(this));
GroupedListAdapter adapter = new GroupedListAdapter(this, GroupModel.getGroups(10, 5));
adapter.setOnHeaderClickListener(new GroupedRecyclerViewAdapter.OnHeaderClickListener() {
@Override
public void onHeaderClick(GroupedRecyclerViewAdapter adapter, BaseViewHolder holder,
int groupPosition) {
Toast.makeText(GroupedListActivity.this, "组头:groupPosition = " + groupPosition,
Toast.LENGTH_LONG).show();
}
});
adapter.setOnFooterClickListener(new GroupedRecyclerViewAdapter.OnFooterClickListener() {
@Override
public void onFooterClick(GroupedRecyclerViewAdapter adapter, BaseViewHolder holder,
int groupPosition) {
Toast.makeText(GroupedListActivity.this, "组尾:groupPosition = " + groupPosition,
Toast.LENGTH_LONG).show();
}
});
adapter.setOnChildClickListener(new GroupedRecyclerViewAdapter.OnChildClickListener() {
@Override
public void onChildClick(GroupedRecyclerViewAdapter adapter, BaseViewHolder holder,
int groupPosition, int childPosition) {
Toast.makeText(GroupedListActivity.this, "子项:groupPosition = " + groupPosition
+ ", childPosition = " + childPosition,
Toast.LENGTH_LONG).show();
}
});
rvList.setAdapter(adapter);
}
}
这是我在项目Demo中的实现的其中一个列子。效果是上面的图1。想看源码和更多的使用列子的欢迎访问我的GitHub。
头部悬浮吸顶功能
应一些朋友的反馈,我在1.2.0版本中新加了列表的头部悬浮吸顶功能。使用起来非常的简单,只需要用框架里提供的StickyHeaderLayout包裹一下你的RecyclerView就可以了。当然,你需要使用GroupedRecyclerViewAdapter才能看到效果。
<!-- 用StickyHeaderLayout包裹RecyclerView -->
<com.donkingliang.groupedadapter.widget.StickyHeaderLayout
android:id="@+id/sticky_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.donkingliang.groupedadapter.widget.StickyHeaderLayout>
StickyHeaderLayout提供了一个设置是否显示悬浮吸顶的方法。
//是否吸顶,默认为true。
stickyLayout.setSticky(true);
对悬浮吸顶功能的更多介绍,请看我的另一篇文章:《Android RecyclerView实现头部悬浮吸顶效果》
效果图:
网友评论
https://github.com/donkingliang/RefreshLayout
使用还是挺简单的,可以满足你的需求。
<a href="openFile:C:/Users/admin/AndroidStudioProjects/MyApplication/app/build.gradle">Open File</a><br><a href="Unable to resolve dependency for ':app@debug/compileClasspath': Could not resolve com.github.donkingliang:GroupedRecyclerViewAdapter:1.2.0.">Show Details</a>
就是报这个。无法依赖。 网络应该没问题,我试了添加其他依赖可以成功。
Process: woxingwoxiu.com, PID: 17687
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
at com.donkingliang.groupedadapter.holder.BaseViewHolder.setText(BaseViewHolder.java:43)
at woxingwoxiu.com.login.activity.GetPhoneActivity$GroupedListTestAdapter.onBindChildViewHolder(GetPhoneActivity.java:230)
at com.donkingliang.groupedadapter.adapter.GroupedRecyclerViewAdapter.onBindViewHolder(GroupedRecyclerViewAdapter.java:126)
at android.support.v7.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:6673)
at android.support.v7.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:6714)
这个怎么解决呢?
至于上拉加载,如果你想自己实现的话,关键在于判断列表是否滚动到底部。思路是监听列表滚动,在滚动停止的时候判断最后可见的item是不是列表的最后一个item(lastVisibleItem == adapter.getItemCount() - 1)。
如果你不想自己去写上拉加载,网上也有很多基于RecyclerView或嵌套RecyclerView的上下拉框架,你可以搜一下。我自己也有写过上下拉框架,但目前只是在自己和公司的项目用,以后会整理放到GitHub。谢谢!