上一篇写了分组效果的初步实现:
这一篇就继续增加分组折叠效果和基类的抽取与解决上一篇的bug(item布局宽度match_parent没有生效)
效果如下图:
三、点击头布局实现展开折叠效果
根据上一片文章最后的代码,继续修改代码让RecyclerView实现点击班级布局可以显示隐藏学生的效果,
首先,先画图分析可折叠的效果有几种情况:
与不可折叠的差别:
1.由上图可以看出,班级布局的position的可变的,也就是说,当折叠的时候无展开的时候,其他的班级布局的position会动态改变位置,所以,上一篇中用HashMap存放班级布局的index和position不可用于该效果,所以改为List集合来存放班级的position
2.根据当前选中情况来单独显示隐藏某个班级下的学生
怎么实现点击班级后显示学生,再次点击就隐藏学生呢?这个我们可以在返回班级对应学生人数哪里做文章,当我需要隐藏该班级学生,就返回0,显示的话就返回该班级的所有人数
代码改造如下:
//存放班级对应的position
private List<Integer> mHeaderIndex = new ArrayList<>();
//存放班级对应的学生
private HashMap<Integer, List<String>> mContentMap = new HashMap<>();
//存放当前班级的是否展开
private SparseBooleanArray mBooleanMap;
* 条目的总数量
* @return
@Override
public int getItemCount() {
return getHeadersCount() + getContentCount();
* 头布局的数量
* @return
private int getHeadersCount(){
return mContent.size();
* item的数量
* @return
private int getContentCount(){
mHeaderIndex.clear();
mContentMap.clear();
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));
mHeaderIndex.add(new Integer(itemCount));
itemCount += getStudentSizeOfClass(i);
studentSize += getStudentSizeOfClass(i);
if(getStudentSizeOfClass(i) > 0){
mContentMap.put(i, mContent.get(i).classStudents);
return studentSize;
* 根据班级获取对应的学生人数
* @param classIndex
* @return
private int getStudentSizeOfClass(int classIndex){
int count = mContent.get(classIndex).classStudents.size();
if (!mBooleanMap.get(classIndex)) {
count = 0;
return count;
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
//如果是head布局
if(isHeaderView(position)){
((HeaderHolder)holder).tvClassName.setOnClickListener(null);
((HeaderHolder)holder).tvClassName.setText(mContent.get(getHeadRealCount(position)).className);
((HeaderHolder)holder).tvClassName.setTag(getHeadRealCount(position));
((HeaderHolder)holder).tvClassName.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = (int) view.getTag();
boolean isOpen = mBooleanMap.get(position);
mBooleanMap.put(position, !isOpen);
notifyDataSetChanged();
return ;
}else {
//根据position获取position对应的学生所在班级
int classId = getStudentOfClass(position);
//获取该班级所有学生
List<String> classStudent = mContentMap.get(classId);
//获取改班级head所在的position位置
int classOfPosition = mHeaderIndex.get(classId);
//根据当前位置position和班级head布局的position,计算当前学生在班级中的位置
int studentIndex = position - classOfPosition - 1;
//根据位置获取具体学生
String studentName = classStudent.get(studentIndex);
//显示出来学生名字
((ContentHolder) holder).tvInfo.setText(studentName);
* 获取position是第几个班级
* @param position
* @return
private int getHeadRealCount(int position){
return mHeaderIndex.indexOf(new Integer(position));
* 根据position获取所属的班级的index
* @return
private int getStudentOfClass(int position){
for (int i = 0; i < mHeaderIndex.size(); i++) {
if(mHeaderIndex.get(i) > position){
return i-1;
return mHeaderIndex.size() - 1;
}
从代码中可以看出,
getHeadersCount()
与
getItemCount()
并没有改动,主要就是
getContentCount()
、
getHeadRealCount(int position)
、
getStudentOfClass(int position)
和
onBindViewHolder
中改动较大,还多了个mBooleanMap,这个主要是存储当前班级是否展开。
运行效果如下:
基本效果是实现了,如果想要listView样式的我们只需要修改布局管理器setLayoutManager就可以很轻松的切换成List样式了,那么,如果换成其他场景还要重复写这么多代码也不想我们的作风,程序员都是非常懒的,所以,我们需要抽出一个基类,在不同的地方使用,只需要继承该基类然后写一些少量的代码就可以实现该效果,这才是我们想要的,那么开始把。
四、抽出基类
首先,需要思考那些需要自己实现的,那些需要基类实现的,大概整理了一下不同需求需要改变的地方:
-
创建班级布局
-
创建学生布局
-
填充班级布局信息
-
填充学生布局信息
-
班级ViewHolder
-
学生ViewHolder
-
一共有多少个班级
-
每个班级有多少个学生
所以,我们在基类中,把这些方法定义为抽象方法,让子类必须去实现,注意:ViewHolder需要使用泛型!:
/**
* 头布局的总数(一共有多少个班级)
* @return
public abstract int getHeadersCount();
* 头布局对应内容的总数(也就是改头布局里面有多少条item)(根据班级获取该班级有多少个学生)
* @param headerPosition 第几个头布局
* @return
public abstract int getContentCountForHeader(int headerPosition);
* 创建头布局(创建班级布局)
* @param parent
* @param viewType
* @return
public abstract C onCreateHeaderViewHolder(ViewGroup parent, int viewType);
* 创建内容布局(创建学生布局)
* @param parent
* @param viewType
* @return
public abstract S onCreateContentViewHolder(ViewGroup parent, int viewType);
* 填充头布局的数据(填充班级布局信息)
* @param holder
* @param position
public abstract void onBindHeaderViewHolder(C holder, int position);
* 填充(填充学生布局信息)
* @param holder
* @param HeaderPosition
* @param ContentPositionForHeader
public abstract void onBindContentViewHolder(S holder, int HeaderPosition, int ContentPositionForHeader);
下面把基类的adapter中对应的代码改一下:
/**
* 条目的总数量
* @return
@Override
public int getItemCount() {
mHeaderIndex.clear();
int count = 0;
int headSize = getHeadersCount();
for (int i = 0; i < headSize; i++) {
if(i != 0){
count++;
mHeaderIndex.add(new Integer(count));
count += getContentCountForHeader(i);
Log.e("fan", "--getItemCount:" + count + "--headSize" + headSize);
return count + 1;
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType == TYPE_HEADER){
//班级header布局
return onCreateHeaderViewHolder(parent, viewType);
}else {
//学生布局
return onCreateContentViewHolder(parent, viewType);
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
//如果是head布局
if(isHeaderView(position)){ //班级header布局填充
onBindHeaderViewHolder((C)holder, getHeadRealCount(position));
return ;
}else { //学生信息填充
//根据position获取position对应的学生所在班级
int classId = getStudentOfClass(position);
//获取改班级head所在的position位置
int classOfPosition = mHeaderIndex.get(classId);
//根据当前位置position和班级head布局的position,计算当前学生在班级中的位置
int studentIndex = position - classOfPosition - 1;
onBindContentViewHolder((S)holder, classId, studentIndex);
}
基类adapter完整的代码如下(如果不懂泛型的可以注意下类名后面跟着的代码):
这里写代码片
public abstract class ClassAdapter<C extends RecyclerView.ViewHolder, S extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 1;
private static final int TYPE_CONTENT = 0;
private List<Integer> mHeaderIndex = new ArrayList<>();
* 是否为头布局
* @param position
* @return
private boolean isHeaderView(int position){
return mHeaderIndex.contains(new Integer(position));
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType == TYPE_HEADER){
//班级header布局
return onCreateHeaderViewHolder(parent, viewType);
}else {
//学生布局
return onCreateContentViewHolder(parent, viewType);
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
//如果是head布局
if(isHeaderView(position)){ //班级header布局填充
onBindHeaderViewHolder((C)holder, getHeadRealCount(position));
return ;
}else { //学生信息填充
//根据position获取position对应的学生所在班级
int classId = getStudentOfClass(position);
//获取改班级head所在的position位置
int classOfPosition = mHeaderIndex.get(classId);
//根据当前位置position和班级head布局的position,计算当前学生在班级中的位置
int studentIndex = position - classOfPosition - 1;
onBindContentViewHolder((S)holder, classId, studentIndex);
@Override
public int getItemViewType(int position) {
if(isHeaderView(position)){
return TYPE_HEADER;
}else{
return TYPE_CONTENT;
* 条目的总数量
* @return
@Override
public int getItemCount() {
mHeaderIndex.clear();
int count = 0;
int headSize = getHeadersCount();
for (int i = 0; i < headSize; i++) {
if(i != 0){
count++;
mHeaderIndex.add(new Integer(count));
count += getContentCountForHeader(i);
return count + 1;
* 获取position是第几个头布局
* @param position
* @return
private int getHeadRealCount(int position){
return mHeaderIndex.indexOf(new Integer(position));
* 根据value获取所属的key
* @return
private int getStudentOfClass(int position){
for (int i = 0; i < mHeaderIndex.size(); i++) {
if(mHeaderIndex.get(i) > position){
return i-1;
return mHeaderIndex.size() - 1;
@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(viewType == TYPE_HEADER){
return ((GridLayoutManager) layoutManager).getSpanCount();
return 1;
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder 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);
* 头布局的总数
* @return
public abstract int getHeadersCount();
* 头布局对应内容的总数(也就是改头布局里面有多少条item)
* @param headerPosition 第几个头布局
* @return
public abstract int getContentCountForHeader(int headerPosition);
* 创建头布局
* @param parent
* @param viewType
* @return
public abstract C onCreateHeaderViewHolder(ViewGroup parent, int viewType);
* 创建内容布局
* @param parent
* @param viewType
* @return
public abstract S onCreateContentViewHolder(ViewGroup parent, int viewType);
* 填充头布局的数据
* @param holder
* @param position
public abstract void onBindHeaderViewHolder(C holder, int position);
* @param holder
* @param HeaderPosition
* @param ContentPositionForHeader
public abstract void onBindContentViewHolder(S holder, int HeaderPosition, int ContentPositionForHeader);
}
而我们平常使用的话就写个子类去继承该adapter,例如:
MyAdapter.class
public class MyAdapter extends ClassAdapter<MyAdapter.ClassHolder, MyAdapter.StudentHolder> {
private Context context;
private List<ClassBean> mContent;
//用于记录当前班级是隐藏还是显示
private SparseBooleanArray mBooleanMap;
public MyAdapter(Context context, List mContent) {
this.context = context;
this.mContent = mContent;
mBooleanMap = new SparseBooleanArray();
@Override
public int getHeadersCount() {
return mContent.size();
@Override
public int getContentCountForHeader(int headerPosition) {
int count = mContent.get(headerPosition).classStudents.size();
if (!mBooleanMap.get(headerPosition)) {
count = 0;
return count;
* 创建头布局header的viewholder
* @param parent
* @param viewType
* @return
@Override
public MyAdapter.ClassHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType) {
return new ClassHolder(View.inflate(context, R.layout.item, null));
* 创建内容布局item的viewholder
* @param parent
* @param viewType
* @return
@Override
public MyAdapter.StudentHolder onCreateContentViewHolder(ViewGroup parent, int viewType) {
return new StudentHolder(View.inflate(context, R.layout.item, null));
@Override
public void onBindHeaderViewHolder(MyAdapter.ClassHolder holder, int position) {
holder.tvClassName.setOnClickListener(null);
holder.tvClassName.setText(mContent.get(position).className);
holder.tvClassName.setTag(position);
holder.tvClassName.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = (int) view.getTag();
boolean isOpen = mBooleanMap.get(position);
mBooleanMap.put(position, !isOpen);
notifyDataSetChanged();
@Override
public void onBindContentViewHolder(StudentHolder holder, int HeaderPosition, int ContentPositionForHeader) {
holder.tvInfo.setText(mContent.get(HeaderPosition).classStudents.get(ContentPositionForHeader));
class ClassHolder extends RecyclerView.ViewHolder{
public TextView tvClassName;
public ClassHolder(View itemView) {
super(itemView);
tvClassName = itemView.findViewById(R.id.tvInfo);
class StudentHolder extends RecyclerView.ViewHolder{
public TextView tvInfo;
public StudentHolder(View itemView) {
super(itemView);
tvInfo = itemView.findViewById(R.id.tvInfo);
}
这里需要注意一点的是,我们把显示隐藏的放到子类的
getContentCountForHeader(int headerPosition)
里面了,所以,如果我们不想要隐藏,只要一直显示内容的话就把里面的map给注释了,如下:
@Override
public int getContentCountForHeader(int headerPosition) {
int count = mContent.get(headerPosition).classStudents.size();
//这里是控制显示隐藏内容的部分
//if (!mBooleanMap.get(headerPosition)) {
// count = 0;
return count;
}
对了,还有Activity中使用的话就直接创建我们写的子类就可以了,比如:
for (int i = 1; i < 4; i++) {
List<String> studentName = new ArrayList<>();
for (int j = 1; j < 56; j++) {
studentName.add(i + "班 学生" + j);
ClassBean bean = new ClassBean();
bean.className = "二年级" + i + "班";
bean.classStudents = studentName;
mListClass.add(bean);
//可以修改布局管理器来显示网格布局还是线性布局
rvShow.setLayoutManager(new GridLayoutManager(this, 4));
//这个就用我们的子类adapter
MyAdapter mWrapper = new MyAdapter(this, mListClass);
rvShow.setAdapter(mWrapper);
ok,搞定,对了,还有最后一步,就是修改之前的match_parent不生效问题
五、修复布局match_parent不生效问题
这个百度一下估计多的是,这里就简要说一下解决方法:
在创建ViewHolder的时候,我们之前使用的是用的以前Listview的adapter的写法:
View.inflate(context, R.layout.item, null)
需要修改为:
LayoutInflater.from(context).inflate(R.layout.item, parent, false)
就可以了,最终效果图就是:
完整版代码:
https://github.com/fan0424/RecyclerViewGroupDemo
教程结束!
番外一:根据点击child获取真实的position
/**
* 获取真实位置
* @param section
* @param position 如果是标题栏,为-1
* @return
private int getRealPosition(int headerPosition, int ContentPositionForHeader){
int realPosition = -1;
if(headerPosition!= -1){
realPosition = 0;
for (int i = 0; i < headerPosition; i++) {
realPosition++;
realPosition += getContentCountForHeader(i);
realPosition++;
realPosition += ContentPositionForHeader;
return realPosition;
docker-compose下载很慢 docker下载太慢
Docker默认是国外的源,配置国内镜像仓库。1.进入docker路径 cd /etc/docker/ 2.编辑daemon.json文件加入以下内容:{
"registry-mirrors": ["https://registry.docker-cn.com"]