android RecyclerView一步步打造分组效果、类似QQ分组、折叠菜单、分组效果(二)
效果图:
Android 优雅的为RecyclerView添加HeaderView和FooterView
突然来了灵感,对哦,可以用头布局来实现分组效果!有了思路,就拿起键盘开搞。
注,demo代码最第二篇文章结尾!!!
文章中RecyclerView使用GridLayoutManager的大概思路是,如果是头布局,就让它占一整行,如果不是就根据设置的来显示一行多少个,主要就是adapter中的onAttachedToRecyclerView
方法来实现的,效果如下图:
示例代码:
原adapter:ContentAdapter.class
public class ContentAdapter extends RecyclerView.Adapter {
private Context context;
private List mContent;
public ContentAdapter(Context context, List mContent) {
this.context = context;
this.mContent = mContent;
}
@Override
public ContentAdapter.ContentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ContentHolder(View.inflate(context, R.layout.item, null));
}
@Override
public void onBindViewHolder(ContentAdapter.ContentHolder holder, int position) {
holder.tvInfo.setText(mContent.get(position) + "--adapter的position:" + position);
}
@Override
public int getItemCount() {
return mContent.size();
}
class ContentHolder extends RecyclerView.ViewHolder{
public TextView tvInfo;
public ContentHolder(View itemView) {
super(itemView);
tvInfo = itemView.findViewById(R.id.tvInfo);
}
}
}
拓展adapter:HeaderAndFooterWrapper.class
public class HeaderAndFooterWrapper extends RecyclerView.Adapter {
private static final int BASE_ITEM_TYPE_HEADER = 100000;
private static final int BASE_ITEM_TYPE_FOOTER = 200000;
private SparseArrayCompat mHeaderViews = new SparseArrayCompat<>();
private SparseArrayCompat mFootViews = new SparseArrayCompat<>();
private RecyclerView.Adapter mInnerAdapter;
public HeaderAndFooterWrapper(RecyclerView.Adapter mInnerAdapter) {
this.mInnerAdapter = mInnerAdapter;
}
/**
* 是否为头布局
* @param position
* @return
*/
private boolean isHeaderView(int position){
return position < getHeadersCount();
}
/**
* 是否为尾布局
* @param position
* @return
*/
private boolean isFooterView(int position){
return position >= getHeadersCount() + getRealItemCount();
}
/**
* 添加头布局
* @param view
*/
public void addHeaderView(View view){
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}
/**
* 添加尾布局
* @param view
*/
public void addFooterView(View view){
mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
}
/**
* 头布局的数量
* @return
*/
private int getHeadersCount(){
return mHeaderViews.size();
}
/**
* 尾布局的数量
* @return
*/
public int getFootersCount(){
return mFootViews.size();
}
/**
* item的数量
* @return
*/
private int getRealItemCount(){
return mInnerAdapter.getItemCount();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(mHeaderViews.get(viewType) != null){
RecyclerView.ViewHolder holder = new HeaderHolder(mHeaderViews.get(viewType));
return holder;
}
if(mFootViews.get(viewType) != null){
RecyclerView.ViewHolder holder = new FooterHolder(mFootViews.get(viewType));
return holder;
}
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(isHeaderView(position)){
return ;
}
if(isFooterView(position)){
return;
}
mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
}
@Override
public int getItemViewType(int position) {
if(isHeaderView(position)){
return mHeaderViews.keyAt(position);
}else if(isFooterView(position)){
return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
}else{
return mInnerAdapter.getItemViewType(position - getHeadersCount());
}
}
/**
* 条目的总数量
* @return
*/
@Override
public int getItemCount() {
return getHeadersCount() + getRealItemCount() + getFootersCount();
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
mInnerAdapter.onAttachedToRecyclerView(recyclerView);
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if(layoutManager instanceof GridLayoutManager){
GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
@Override
public int getSpanSize(int position) {
int viewType = getItemViewType(position);
if(mHeaderViews.get(viewType) != null || mFootViews.get(viewType) != null){
return ((GridLayoutManager) layoutManager).getSpanCount();
}
return 1;
}
});
}
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder){
mInnerAdapter.onViewAttachedToWindow(holder);
int position = holder.getLayoutPosition();
if (isHeaderView(position) || isFooterView(position))
{
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams)
{
StaggeredGridLayoutManager.LayoutParams p =
(StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
class HeaderHolder extends RecyclerView.ViewHolder{
public HeaderHolder(View itemView) {
super(itemView);
}
}
class FooterHolder extends RecyclerView.ViewHolder{
public FooterHolder(View itemView) {
super(itemView);
}
}
}
Activity中使用:
for (int i = 0; i < 25; i++) {
mContent.add("当前的内容是:" + i);
}
rvShow.setLayoutManager(new GridLayoutManager(this, 2));
ContentAdapter adapter = new ContentAdapter(this, mContent);
HeaderAndFooterWrapper mWrapper = new HeaderAndFooterWrapper(adapter);
TextView tvHead1 = new TextView(this);
tvHead1.setText("当前是头布局1111");
tvHead1.setGravity(Gravity.CENTER);
TextView tvHead2 = new TextView(this);
tvHead2.setText("当前是头布局222");
tvHead2.setGravity(Gravity.CENTER);
TextView tvFoot1 = new TextView(this);
tvFoot1.setText("当前是尾布局1111");
tvFoot1.setGravity(Gravity.CENTER);
mWrapper.addHeaderView(tvHead1);
mWrapper.addHeaderView(tvHead2);
mWrapper.addFooterView(tvFoot1);
rvShow.setAdapter(mWrapper);
运行后效果图:
该文章是在保持原来adapter不变的情况下,写成父类的形式对原adapter进行了以下拓展,如果把拓展的adapter与之前adapter合并一起,代码如下:
一个adapter:HeaderAndFooterWrapper.class
public class HeaderAndFooterWrapper extends RecyclerView.Adapter {
private static final int BASE_ITEM_TYPE_HEADER = 100000;
private static final int BASE_ITEM_TYPE_FOOTER = 200000;
private SparseArrayCompat mHeaderViews = new SparseArrayCompat<>();
private SparseArrayCompat mFootViews = new SparseArrayCompat<>();
private Context context;
private List mContent;
// private RecyclerView.Adapter mInnerAdapter;
//
// public HeaderAndFooterWrapper(RecyclerView.Adapter mInnerAdapter) {
// this.mInnerAdapter = mInnerAdapter;
// }
public HeaderAndFooterWrapper(Context context, List mContent) {
this.context = context;
this.mContent = mContent;
}
/**
* 是否为头布局
* @param position
* @return
*/
private boolean isHeaderView(int position){
return position < getHeadersCount();
}
/**
* 是否为尾布局
* @param position
* @return
*/
private boolean isFooterView(int position){
return position >= getHeadersCount() + getRealItemCount();
}
/**
* 添加头布局
* @param view
*/
public void addHeaderView(View view){
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}
/**
* 添加尾布局
* @param view
*/
public void addFooterView(View view){
mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
}
/**
* 头布局的数量
* @return
*/
private int getHeadersCount(){
return mHeaderViews.size();
}
/**
* 尾布局的数量
* @return
*/
public int getFootersCount(){
return mFootViews.size();
}
// /**
// * item的数量
// * @return
// */
// private int getRealItemCount(){
// return mInnerAdapter.getItemCount();
// }
/**
* item的数量
* @return
*/
private int getRealItemCount(){
return mContent.size();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(mHeaderViews.get(viewType) != null){
RecyclerView.ViewHolder holder = new HeaderHolder(mHeaderViews.get(viewType));
return holder;
}
if(mFootViews.get(viewType) != null){
RecyclerView.ViewHolder holder = new FooterHolder(mFootViews.get(viewType));
return holder;
}
// return mInnerAdapter.onCreateViewHolder(parent, viewType);
return new HeaderAndFooterWrapper.ContentHolder(View.inflate(context, R.layout.item, null));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(isHeaderView(position)){
return ;
}
if(isFooterView(position)){
return;
}
// mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
((ContentHolder)holder).tvInfo.setText(mContent.get(position - getHeadersCount()) + "--" + (position - getHeadersCount()));
}
@Override
public int getItemViewType(int position) {
if(isHeaderView(position)){
return mHeaderViews.keyAt(position);
}else if(isFooterView(position)){
return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
}else{
// return mInnerAdapter.getItemViewType(position - getHeadersCount());
return 0;
}
}
/**
* 条目的总数量
* @return
*/
@Override
public int getItemCount() {
return getHeadersCount() + getRealItemCount() + getFootersCount();
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
// mInnerAdapter.onAttachedToRecyclerView(recyclerView);
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if(layoutManager instanceof GridLayoutManager){
GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
@Override
public int getSpanSize(int position) {
int viewType = getItemViewType(position);
if(mHeaderViews.get(viewType) != null || mFootViews.get(viewType) != null){
return ((GridLayoutManager) layoutManager).getSpanCount();
}
return 1;
}
});
}
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder){
// mInnerAdapter.onViewAttachedToWindow(holder);
int position = holder.getLayoutPosition();
if (isHeaderView(position) || isFooterView(position))
{
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams)
{
StaggeredGridLayoutManager.LayoutParams p =
(StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
class HeaderHolder extends RecyclerView.ViewHolder{
public HeaderHolder(View itemView) {
super(itemView);
}
}
class FooterHolder extends RecyclerView.ViewHolder{
public FooterHolder(View itemView) {
super(itemView);
}
}
class ContentHolder extends RecyclerView.ViewHolder{
public TextView tvInfo;
public ContentHolder(View itemView) {
super(itemView);
tvInfo = itemView.findViewById(R.id.tvInfo);
}
}
}
Activity中使用:
for (int i = 0; i < 25; i++) {
mContent.add("当前的内容是:" + i);
}
rvShow.setLayoutManager(new GridLayoutManager(this, 2));
HeaderAndFooterWrapper adapter = new HeaderAndFooterWrapper(this, mContent);
TextView tvHead1 = new TextView(this);
tvHead1.setText("当前是头布局1111");
tvHead1.setGravity(Gravity.CENTER);
TextView tvHead2 = new TextView(this);
tvHead2.setText("当前是头布局222");
tvHead2.setGravity(Gravity.CENTER);
TextView tvFoot1 = new TextView(this);
tvFoot1.setText("当前是尾布局1111");
tvFoot1.setGravity(Gravity.CENTER);
adapter.addHeaderView(tvHead1);
adapter.addHeaderView(tvHead2);
adapter.addFooterView(tvFoot1);
rvShow.setAdapter(adapter);
运行效果图还是跟之前一样,只不过是合并到一个adapter里了
这样不就可以实现了么!
那么我们怎么去实现它呢,很简单,在adapter的getItemViewType
中可以发现,头布局其实也是根据当前position,通过isHeaderView
和isFooterView
方法来区分当前是头布局、尾布局还是内容布局的
@Override
public int getItemViewType(int position) {
if(isHeaderView(position)){
return mHeaderViews.keyAt(position); //头布局
}else if(isFooterView(position)){
return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount()); //尾布局
}else{
// return mInnerAdapter.getItemViewType(position - getHeadersCount());
return 0; //内容布局
}
}
为了方便理解,我们以班级和学生为例子,并且去除尾布局,例子如下:
BeanClassBean.class
public class ClassBean {
public String className;
public List classStudents;
}
Activity中写法:
for (int i = 1; i < 4; i++) {
List studentName = new ArrayList<>();
for (int j = 1; j < 57; j++) {
studentName.add(i + "班 学生" + j);
}
ClassBean bean = new ClassBean();
bean.className = "二年级" + i + "班";
bean.classStudents = studentName;
mListClass.add(bean);
}
rvShow.setLayoutManager(new GridLayoutManager(this, 2));
HeaderAndFooterWrapper mWrapper = new HeaderAndFooterWrapper(this, mListClass);
TextView tvHead1 = new TextView(this);
tvHead1.setText("当前是头布局1111");
tvHead1.setGravity(Gravity.CENTER);
TextView tvHead2 = new TextView(this);
tvHead2.setText("当前是头布局222");
tvHead2.setGravity(Gravity.CENTER);
mWrapper.addHeaderView(tvHead1);
mWrapper.addHeaderView(tvHead2);
rvShow.setAdapter(mWrapper);
Adapter:HeaderAndFooterWrapper.class
public class HeaderAndFooterWrapper extends RecyclerView.Adapter {
private static final int BASE_ITEM_TYPE_HEADER = 100000;
private SparseArrayCompat mHeaderViews = new SparseArrayCompat<>();
private Context context;
private List mContent;
public HeaderAndFooterWrapper(Context context, List mContent) {
this.context = context;
this.mContent = mContent;
}
/**
* 是否为头布局
* @param position
* @return
*/
private boolean isHeaderView(int position){
return position < getHeadersCount();
}
/**
* 添加头布局
* @param view
*/
public void addHeaderView(View view){
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}
/**
* 头布局的数量
* @return
*/
private int getHeadersCount(){
return mHeaderViews.size();
}
/**
* item的数量
* @return
*/
private int getContentCount(){
return mContent.size();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(mHeaderViews.get(viewType) != null){
RecyclerView.ViewHolder holder = new HeaderHolder(mHeaderViews.get(viewType));
return holder;
}
return new HeaderAndFooterWrapper.ContentHolder(View.inflate(context, R.layout.item, null));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(isHeaderView(position)){
return ;
}
((ContentHolder)holder).tvInfo.setText(mContent.get(position - getHeadersCount()).className);
}
@Override
public int getItemViewType(int position) {
if(isHeaderView(position)){
return mHeaderViews.keyAt(position);
}else{
return 0;
}
}
/**
* 条目的总数量
* @return
*/
@Override
public int getItemCount() {
return getHeadersCount() + getContentCount();
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if(layoutManager instanceof GridLayoutManager){
GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
@Override
public int getSpanSize(int position) {
int viewType = getItemViewType(position);
if(mHeaderViews.get(viewType) != null){
return ((GridLayoutManager) layoutManager).getSpanCount();
}
return 1;
}
});
}
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder){
// mInnerAdapter.onViewAttachedToWindow(holder);
int position = holder.getLayoutPosition();
if (isHeaderView(position))
{
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams)
{
StaggeredGridLayoutManager.LayoutParams p =
(StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
class HeaderHolder extends RecyclerView.ViewHolder{
public HeaderHolder(View itemView) {
super(itemView);
}
}
class ContentHolder extends RecyclerView.ViewHolder{
public TextView tvInfo;
public ContentHolder(View itemView) {
super(itemView);
tvInfo = itemView.findViewById(R.id.tvInfo);
}
}
}
这里,getItemCount()
里面返回了头布局总数getHeadersCount()
和内容布局总数getContentCount()
的和,而getContentCount()
只返回了mContent.size()
也就是只看所有的班级,并不显示班级里的学生,所以,这里的条目一共5个,效果如下图:
很简单,思路就是把头布局替换成班级,内容布局(上图显示班级的)替换成学生,这样就可以达到粗略的分组效果了,怎么做呢?我们先画图来分析一下,如图:
上图看出,现在有三个班级,一班有3人,二班有2人,三班有3人,那么合在一起的话,就是需要我们先确定一班的班级布局位置,然后是一班的学生,接着是二班班级布局的位置,接着是二班学生,接着是三班班级布局,最后是三班学生…
好了,开始改造代码,首先是返回adapter的getItemCount()
,需要我们把所有班级以及每个班级学生的总数返回去用来创建我们的adapter一共有多少个item
@Override
public int getItemCount() {
return getHeadersCount() + getContentCount();
}
private int getHeadersCount(){
return mContent.size();
}
private int getContentCount(){
int itemCount = 0;
int studentSize = 0;
for (int i = 0; i < mContent.size(); i++) {
if(i != 0){
itemCount++;
}
//存储第几班的index位置
mHeaderIndex.put(i,new Integer(itemCount));
itemCount += getStudentSizeOfClass(i);
studentSize += getStudentSizeOfClass(i);
}
return studentSize;
}
上面的getHeadersCount()
是共有多少个班级,getContentCount()
是每个班级下的学生的总和,mHeaderIndex是一个HashMap,用来记录班级的position位置,比如一班是第一个,里面有3个学生,那么一班的position位置是0,一班第一位学生的position就是1,二班是紧跟着一班的,所以二班的position位置是4(可以参考上面的图),
上面的完成了,主要就是确认每个班级的position和所有班级加学生共需要多少个item,
下面在getItemViewType
方法中,根据position来确定当前item是班级布局还是学生布局,代码如下:
@Override
public int getItemViewType(int position) {
if(isHeaderView(position)){
return TYPE_HEADER;
}else{
return TYPE_CONTENT;
}
}
/**
* 是否为头布局
* @param position
* @return
*/
private boolean isHeaderView(int position){
return mHeaderIndex.containsValue(new Integer(position));
}
在getItemViewType
中我们知道,mHeaderIndex
是用来存放班级对应的position,所以这里通过position来判断是否为班级布局
那么在onCreateViewHolder
中就可以根据上面的ViewType来区分当前是班级布局还是学生布局了
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType == TYPE_HEADER){ //班级布局
return new HeaderHolder(View.inflate(context, R.layout.item, null));
}
//学生布局
return new ContentHolder(View.inflate(context, R.layout.item, null));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(isHeaderView(position)){
//填充班级信息 ((HeaderHolder)holder).tvClassName.setText(mContent.get(getHeadRealCount(position)).className);
return ;
}
//填充学生信息(下面有详解该代码的意思)
((ContentHolder)holder).tvInfo.setText(
mContent.get(getStudentOfClass(position)) //获取指定的班级
.classStudents //该班级下所有学生
.get(position - mHeaderIndex.get(getStudentOfClass(position)) - 1)); //具体的学生
}
/**
* 根据position获取当前是第几个班级
* @param position
* @return
*/
private int getHeadRealCount(int position){
int result = 0;
Set> set = mHeaderIndex.entrySet();
for(Map.Entry entry : set){
if(entry.getValue().equals(position)){
result = entry.getKey();
break;
}
}
return result;
}
/**
* 根据学生的position获取所属的班级
* @return
*/
private int getStudentOfClass(int position){
int tempSize = 0;
for (int i = 0; i < mContent.size(); i++) {
tempSize += mContent.get(i).classStudents.size() + 1;
if(position <= tempSize){
return i;
}
}
return 0;
}
上面的getHeadRealCount(int position)
方法主要作用是,在onBindViewHolder
中,如果当前的position是班级布局,那么获取该position是第几个班级,比如一班在集合mContent
中的index是0,一班是1,二班是2,这里返回的就是index,
而getStudentOfClass(int position)
方法是根据position获取该学生所属的班级的index,比如,当前position是1,那么该返回的就是0,为什么是0,因为一班的position是0,那么position 1的位置是一班的学生1(参考下面的图),所以返回当前一班第一个学生所属班级的index,也就是一班的index,是0,
上面代码中的mHeaderIndex.get(getStudentOfClass(position))
这段代码是根据getStudentOfClass(position)
获取到该学生所属班级的index,也就是mHeaderIndex中对应的key,然后根据mHeaderIndex.get(key)
方法获取到该班级的position,如下图,“二班学生2”的position是6,通过getStudentOfClass(position)
方法获取到该学生所属的班级为二班在集合mContent
中的index是1,mHeaderIndex.get(key)
方法获取到二班的position是4;
那么position - mHeaderIndex.get(getStudentOfClass(position)) - 1
就是“二班学生2”的position为6,二班的position为4,那么“二班学生2”在二班中的位置为6-4=2,第二位,也就是班级里学生集合的index=1的位置(index从0开始的),
所以,根据上面就可以得知mContent.get(getStudentOfClass(position)).classStudents.get(position - mHeaderIndex.get(getStudentOfClass(position)) - 1)
获取的就是某个班级下的某个学生
好了,代码运行效果如下:
想要线性的,更换个布局管理器一行代码搞定:
//rvShow.setLayoutManager(new GridLayoutManager(this, 2));
rvShow.setLayoutManager(new LinearLayoutManager(this));
效果如下图(为了区分班级布局,改变了下班级布局的背景颜色):
上图发现布局的宽度设置的为match_parent
居然没有生效,为什么呢?还有如果我想要默认不显示学生,当我点击班级布局后才展示当前班级的学生该怎么实现呢?下一篇来讲解
android RecyclerView一步步打造分组效果、类似QQ分组、折叠菜单、分组效果(二)