去年在大神公众号评论点赞前三名获得了《Android App开发从入门到精通》这本书,这两天闲来无事,就大致浏览一下书的内容。看到了关于recycleView部分,提到了一些关于recycleView自己会用但是不知道为啥这么用的的知识,就抽个周日的时间,根据书上的东西,敲了敲代码,收获还是不少的,再次记录下来,希望留着以后备用,能帮到更多的人最好,争取不误人子弟哈(偷笑)
废话不多说,进入正题,这篇文章分为以下几个部分层层递进:
- recycleView 基础使用;
- recycleView 添加分割线;
- recycleView 添加头布局和尾布局;
- recycleView 数据删除item动画实现;
- recycleView 下拉刷新和上拉加载;
一:recycleView 基础使用
思路如下:
1: recycleView 初始化并设置layouManager
RecyclerView mRecycleView=findViewById(R.id.mRecycleView);
//按照listView垂直方向使用;
mRecycleView.setLayoutManager(new LinearLayoutManager(this));
/**
* 卡片式布局
* 一行四列;
* 第三个参数(reverseLayout) 表示的是是否将数据按照倒叙进行展示,
* false表示 1234 这样展示;
* 5678
* 9
true 表示:9 这样展示;
* 8765
* 4321
* 第三个参数开发这么多年,一般都是false,不知道谷歌为啥要多整个这个
*/
mRecycleView.setLayoutManager(new GridLayoutManager(this, 4,GridLayoutManager.VERTICAL,false));
/**
* 表示所有数据总共就四行进行展示
* */
mRecycleView.setLayoutManager(new GridLayoutManager(this, 4,GridLayoutManager.HORIZONTAL,false));
/**
* 瀑布流,方向参考gridView;
*/
mRecycleView.setLayoutManager(new StaggeredGridLayoutManager(4,StaggeredGridLayoutManager.VERTICAL));
2: 编写adapter
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
Context context;
List<ItemBean>lists;
public MyAdapter(Context context,List<ItemBean>lists) {
this.context = context;
this.lists=lists;
}
@Override
public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// //这样写的话,itemView不能铺满,不知道为啥
// //例如说:recycleView作为listView使用,item水平上就不能充满屏幕
// View itemView= LayoutInflater.from(context).inflate(R.layout.item_layout,null);
//这种方式就可以充满屏幕
View itemView= LayoutInflater.from(context).inflate(R.layout.item_layout,parent,false);
return new MyViewHolder(itemView);
}
@Override
public void onBindViewHolder(MyAdapter.MyViewHolder holder, final int position) {
holder.mTv.setText(bean.getName());
}
@Override
public int getItemCount() {
return lists.size();
}
static class MyViewHolder extends RecyclerView.ViewHolder{
TextView mTv;
public MyViewHolder(View itemView) {
super(itemView);
mTv=itemView.findViewById(R.id.mTv);
}
}
}
3:获取数据源并设置adapter;
List<ItemBean> lists=new ArrayList<>();
for (int i = 0; i <54 ; i++) {
ItemBean b=new ItemBean();
b.setName("编号"+i);
b.setHeight(i*50+10);
lists.add(b);
}
MyAdapter adapter = new MyAdapter(this, lists,this);
mRecycleView.setAdapter(adapter);
这就是关于cecycleView 的基础使用,基本上用过recycleView的兄台们,都对这部分熟悉的不能再熟悉了,但是有几点细节需要注意:
细节点:
- recycleView设置adapter之前一定要记得设置layoutManager(),否则数据不展示,没有任何异常;
- adapter中onCreateViewHolder方法中注释详解
@Override
public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// /**
* 这样写的话,如果是垂直方向上的recycleView(listView),item水平方向上是不能充满屏幕的,具体原因不知
* 可以自己试试;
*
*/
// View itemView= LayoutInflater.from(context).inflate(R.layout.item_layout,null);
/**
* //这种方式就可以充满屏幕,解决上面提到的问题
* /**
* recource 布局文件资源id
* root 父View
* attachToRoot
* true 布局文件将转化为View 并绑定到root,然后返回root作为根节点的整个View
* false 布局文件将转化为View 不绑定到root ,然后返回以布局文件根节点作为根节点的View
*/
View itemView= LayoutInflater.from(context).inflate(R.layout.item_layout,parent,false);
return new MyViewHolder(itemView);
}
二 recycleView 添加分割线
思路如下:
recycleView添加分割线,我认为主要有三种方式:
1:分割线divider在item中添加,根据item的位置索引position动态设置divider的显示和隐藏。例如 recycleView作为listView使用,item位于中间部分divider可以正常展示,最后一个item的divider可以根据实际情况进行显示和隐藏;
2:设置默认分割线
mRecyclerView.addItemDecoration(new RecycleViewDivider(mContext, LinearLayoutManager.VERTICAL));
这种方式,分割线颜色和宽度高度等都是不可控的,不是很实用;
3:对RecyclerView.ItemDecoration进行重写,等同于自定义,自己实现测量绘制布局等方法,实现起来对于类似我这样基础的同学还是比较困难的(书中有详细代码,时间有限,我懒得看)。
总结
以上三种方法,个人比较倾向于第一种,为啥类,因为易操作,可实现性强,唯一缺点就是adapter中增加了item下标位置的逻辑判断。
参考文章 recycleView设置分割线的三种方式
三 recycleView 添加头布局和尾布局
思路总结
日常开发中,我们经常遇到这样的情况,app首页上半部分是个Banner,中间部分是一个列表(recycleView)展示数据,下半部分展示一些其他信息,实现这样一个整体部分方法有两种:
- scrollView 嵌套RecycleView 并且需要暴力解决嵌套出现的一些滑动冲突,数据展示不全等问题,可以参考我的另外一篇杂记(东西比较多,比较乱哈)中关于recycleView部分;
- 再就是以recycleView作为主体,为其添加头布局和尾布局;继续思考,为什么需要单独添加头布局和尾布局呢,为啥不能按照正常的item进行使用呢?你可能投来一脸的鄙视,然后说:“头布局/尾布局/item布局显示不一样啊”。好吧,我接受你的鄙视,问了个白痴问题,顺着这个思路往下走,为recycleView 添加头布局和尾布局的效果,是否可以理解为等同于为recycleView添加不一样布局的item?只不过就是头部,尾部,中间部分三部分不一样而已。那这和使用recycleView实现对话列表的思路是不是一致呢?对喽!!!两种实现思路是一样的(惭愧,我是写这篇文章的时候突然才意识到这个问题)。参考recycleView 添加不同的item.看完参考文章,相信大体思路都应该有了,下面受累看一下添加头布局和尾布局的代码
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
Context context;
List<ItemBean>lists;
private RecycleViewItemClickCallBack clickCallBack;
public static final int TYPE_HEADER=1;
public static final int TYPE_FOOTER=2;
private View headView;
private View footView;
public MyAdapter(Context context,List<ItemBean>lists,RecycleViewItemClickCallBack callBack) {
this.context = context;
this.lists=lists;
this.clickCallBack=callBack;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType==TYPE_HEADER){
return new RecyclerView.ViewHolder(headView){};
}else if(viewType==TYPE_FOOTER){
return new RecyclerView.ViewHolder(footView){};
}else{
/**
* 这样写的话,如果是垂直方向上的recycleView(listView),item水平方向上是不能充满屏幕的,具体原因不知
* 可以自己试试;
*
*/
// View itemView= LayoutInflater.from(context).inflate(R.layout.item_layout,null);
/**
* //这种方式就可以充满屏幕,解决上面提到的问题
* /**
* recource 布局文件资源id
* root 父View
* attachToRoot
* true 布局文件将转化为View 并绑定到root,然后返回root作为根节点的整个View
* false 布局文件将转化为View 不绑定到root ,然后返回以布局文件根节点作为根节点的View
*/
View itemView= LayoutInflater.from(context).inflate(R.layout.item_layout,parent,false);
return new MyViewHolder(itemView);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
if(getItemViewType(position)==TYPE_FOOTER||getItemViewType(position)==TYPE_HEADER){
return;
}
final MyViewHolder myViewHolder= (MyViewHolder) holder;
ItemBean bean=lists.get(getRealPosition(position));
myViewHolder.mTv.setText(bean.getName());
myViewHolder.mTv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clickCallBack.onItemClick(position,myViewHolder.mTv,getRealPosition(position));
}
});
}
private int getRealPosition(int position) {
return headView==null?position:position-1;
}
@Override
public int getItemViewType(int position) {
if(position==0&&headView!=null){
return TYPE_HEADER;
}else if(position==getItemCount()-1&&footView!=null){
return TYPE_FOOTER;
}
return super.getItemViewType(position);
}
@Override
public int getItemCount() {
if(headView==null&&footView==null){
return lists.size();
}else if((headView==null&&footView!=null)||(headView!=null&&footView==null)){
return lists.size()+1;
}else{
return lists.size()+2;
}
}
static class MyViewHolder extends RecyclerView.ViewHolder{
TextView mTv;
public MyViewHolder(View itemView) {
super(itemView);
mTv=itemView.findViewById(R.id.mTv);
}
}
public void setHeadView(RecyclerView mrecycleView,int reasouceId){
/**
* recource 布局文件资源id
* root 父View
* attachToRoot
* true 布局文件将转化为View 并绑定到root,然后返回root作为根节点的整个View
* false 布局文件将转化为View 不绑定到root ,然后返回以布局文件根节点作为根节点的View
*/
View headView = LayoutInflater.from(context).inflate(reasouceId,mrecycleView,false);
this.headView=headView;
notifyItemInserted(0);
}
public void setFontView(RecyclerView mrecycleView,int reasouceId){
View footView = LayoutInflater.from(context).inflate(reasouceId,mrecycleView,false);
this.footView=headView;
this.footView=footView;
notifyItemInserted(headView==null?lists.size()-1:lists.size());
}
public interface RecycleViewItemClickCallBack{
void onItemClick(int position,View mView,int realClickPosition);
}
}
细节归纳
- getItemViewType()方法用于区别返回不同的item类型;对应 onCreateViewHolder(ViewGroup parent, int viewType) {}参数中的viewType,可以根据该参数区别加载不同的item;
- 另外书中重点点出了加载头布局和尾布局要使用
View footView = LayoutInflater.from(context).inflate(reasouceId,mrecycleView,false);
看到这里,我一直在考虑之前试图添加头布局和尾布局总失败的原因是不是因为我使用的是
View itemView= LayoutInflater.from(context).inflate(R.layout.item_layout,null);
我没有详细测这里,小伙伴们可以自己看看自己的代码,验证一下这里,日常开发中也要注意这里。
四 recycleView 数据删除item动画实现
关键代码如下:
Toast.makeText(this,"点击的是"+realClickPosition,Toast.LENGTH_LONG).show();
/**
* recycleView删除数据的方法步骤如下:
* 1:删除原有数据源数据;
* 2:adapter。notifyItemRemoved(指定位置的index);adapter通知指定位置的item进行删除操作;
* 3:adapter.notifyItemRangeChanged(删除位置的index,以及到最后的数据长度)adapter通知后面的item进行调整补齐
*/
//移除数据源
lists.remove(realClickPosition);
//item 移除动画
adapter.notifyItemRemoved(position);
/**
* 第一个参数是变化的item七点位置index;
* 第二个参数是从idex必须变化的长度;
* 比方说总item 10 ,index=5,那么必须变化的长度就是5后面的item,也就是10-5-1=4;
*/
adapter.notifyItemRangeChanged(position,adapter.getItemCount()-position-1);
以后列表删除数据,不用整体刷新了,可以考虑用这种方法,还有动画效果Nice。
五 recycleView 下拉刷新和上拉加载
思路归纳
- 实现下拉刷新可以使用系统自带的SwipeRefreshLayout,包裹recycleView实现下拉刷新;
- 上拉加载系统没有可以直接使用的控件,只能自己实现;
- recycleView添加footer布局,布局里面包含进度条,模拟加载更多效果(这里可以自定义,根据自己需求来)
- recycleView 添加setOnScrollListener监听,在监听 中判断recycleView是否滑动到底部,从而决定是否加载更多,加载更多数据之前foot布局中的进度条展示,数据加载完了之后进行隐藏;
- 根据实现上拉加载更多的思路同样可以实现下拉刷新的功能(就不需求SwipeRefreshLayout包裹了),这种实现方式,可配置性比较高,完全可以根据需求进行刷新和加载更多布局展示。
具体代码如下
主布局xml部分
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mSr"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.bcj.myrecycleview.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/mRecycleView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</android.support.v4.widget.SwipeRefreshLayout>
头布局和尾布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp">
<TextView
android:background="#f00"
android:id="@+id/mTv"
android:text="这是头布局哈哈"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="50dp" />
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="#ff0"
android:orientation="vertical"
android:layout_height="wrap_content">
<TextView
android:gravity="center"
android:layout_gravity="center_horizontal"
android:text="这是尾布局哈哈"
android:layout_width="wrap_content"
android:layout_height="50dp" />
<ProgressBar
android:id="@+id/mTv_foot"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="50dp" />
</LinearLayout>
java代码部分
public class MainActivity extends AppCompatActivity implements MyAdapter.RecycleViewItemClickCallBack,
SwipeRefreshLayout.OnRefreshListener{
List<ItemBean> lists=new ArrayList<>();
private MyAdapter adapter;
private ProgressBar pb;
private Handler mHandler=new Handler();
private View footView;
private SwipeRefreshLayout mSr;
RecyclerView mRecycleView;
private int refrashNum=4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecycleView=findViewById(R.id.mRecycleView);
mSr=findViewById(R.id.mSr);
mRecycleView.setLayoutManager(new LinearLayoutManager(this));
footView = LayoutInflater.from(this).inflate(R.layout.foot_item_layout, mRecycleView,false);
pb=footView.findViewById(R.id.mTv_foot);
pb.setVisibility(View.GONE);
mSr.setColorSchemeResources(R.color.colorAccent,android.R.color.holo_blue_light, R.color.colorPrimary);
mSr.setRefreshing(true);
initLists();
setListener();
}
private void setAdapter() {
if(adapter==null){
adapter = new MyAdapter(this, lists,this);
adapter.setHeadView(mRecycleView,R.layout.head_item_layout);
adapter.setFontView(mRecycleView,footView);
mRecycleView.setAdapter(adapter);
}else{
adapter.notifyDataSetChanged();
}
}
private void setListener() {
mSr.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
initLists();
}
});
mRecycleView.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
RecyclerView.LayoutManager mLm=recyclerView.getLayoutManager();
int lastVisibleItem=((LinearLayoutManager)mLm).findLastVisibleItemPosition();
int totalItemCount=mLm.getItemCount();
//最后一项显示&&下滑状态的时候回调加载更多
if(pb.getVisibility()==View.GONE&&lastVisibleItem>=totalItemCount-1&&dy>0){
pb.setVisibility(View.VISIBLE);
loadMoreInfo();
}
}
});
}
private void loadMoreInfo() {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
pb.setVisibility(View.GONE);
for (int i = 0; i <5 ; i++) {
ItemBean b=new ItemBean();
b.setName("加载更多"+i);
lists.add(b);
}
setAdapter();
}
},1000);
}
private void initLists() {
lists.clear();
refrashNum++;
for (int i = 0; i <refrashNum+6 ; i++) {
ItemBean b=new ItemBean();
b.setName("编号"+i);
b.setHeight(i*50+10);
lists.add(b);
}
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
setAdapter();
mSr.setRefreshing(false);
}
},1000);
}
@Override
public void onItemClick(int position, View mView, int realClickPosition) {
Toast.makeText(this,"点击的是"+realClickPosition,Toast.LENGTH_LONG).show();
/**
* recycleView删除数据的方法步骤如下:
* 1:删除原有数据源数据;
* 2:adapter。notifyItemRemoved(指定位置的index);adapter通知指定位置的item进行删除操作;
* 3:adapter.notifyItemRangeChanged(删除位置的index,以及到最后的数据长度)adapter通知后面的item进行调整补齐
*/
//移除数据源
lists.remove(realClickPosition);
//item 移除动画
adapter.notifyItemRemoved(position);
/**
* 第一个参数是变化的item七点位置index;
* 第二个参数是从idex必须变化的长度;
* 比方说总item 10 ,index=5,那么必须变化的长度就是5后面的item,也就是10-5-1=4;
*/
adapter.notifyItemRangeChanged(position,adapter.getItemCount()-position-1);
}
@Override
public void onRefresh() {
}
}
adapter 部分
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
Context context;
List<ItemBean>lists;
private RecycleViewItemClickCallBack clickCallBack;
public static final int TYPE_HEADER=1;
public static final int TYPE_FOOTER=2;
private View headView;
private View footView;
public MyAdapter(Context context,List<ItemBean>lists,RecycleViewItemClickCallBack callBack) {
this.context = context;
this.lists=lists;
this.clickCallBack=callBack;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if(viewType==TYPE_HEADER){
return new RecyclerView.ViewHolder(headView){};
}else if(viewType==TYPE_FOOTER){
return new RecyclerView.ViewHolder(footView){};
}else{
/**
* 这样写的话,如果是垂直方向上的recycleView(listView),item水平方向上是不能充满屏幕的,具体原因不知
* 可以自己试试;
*
*/
// View itemView= LayoutInflater.from(context).inflate(R.layout.item_layout,null);
/**
* //这种方式就可以充满屏幕,解决上面提到的问题
* /**
* recource 布局文件资源id
* root 父View
* attachToRoot
* true 布局文件将转化为View 并绑定到root,然后返回root作为根节点的整个View
* false 布局文件将转化为View 不绑定到root ,然后返回以布局文件根节点作为根节点的View
*/
View itemView= LayoutInflater.from(context).inflate(R.layout.item_layout,parent,false);
return new MyViewHolder(itemView);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
if(getItemViewType(position)==TYPE_FOOTER||getItemViewType(position)==TYPE_HEADER){
return;
}
final MyViewHolder myViewHolder= (MyViewHolder) holder;
ItemBean bean=lists.get(getRealPosition(position));
myViewHolder.mTv.setText(bean.getName());
myViewHolder.mTv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clickCallBack.onItemClick(position,myViewHolder.mTv,getRealPosition(position));
}
});
}
private int getRealPosition(int position) {
return headView==null?position:position-1;
}
@Override
public int getItemViewType(int position) {
if(position==0&&headView!=null){
return TYPE_HEADER;
}else if(position==getItemCount()-1&&footView!=null){
return TYPE_FOOTER;
}
return super.getItemViewType(position);
}
@Override
public int getItemCount() {
if(headView==null&&footView==null){
return lists.size();
}else if((headView==null&&footView!=null)||(headView!=null&&footView==null)){
return lists.size()+1;
}else{
return lists.size()+2;
}
}
static class MyViewHolder extends RecyclerView.ViewHolder{
TextView mTv;
public MyViewHolder(View itemView) {
super(itemView);
mTv=itemView.findViewById(R.id.mTv);
}
}
public void setHeadView(RecyclerView mrecycleView,int reasouceId){
/**
* recource 布局文件资源id
* root 父View
* attachToRoot
* true 布局文件将转化为View 并绑定到root,然后返回root作为根节点的整个View
* false 布局文件将转化为View 不绑定到root ,然后返回以布局文件根节点作为根节点的View
*/
View headView = LayoutInflater.from(context).inflate(reasouceId,mrecycleView,false);
this.headView=headView;
notifyItemInserted(0);
}
public void setFontView(RecyclerView mrecycleView,View footView){
this.footView=headView;
this.footView=footView;
notifyItemInserted(headView==null?lists.size()-1:lists.size());
}
public interface RecycleViewItemClickCallBack{
void onItemClick(int position,View mView,int realClickPosition);
}
}
代码基本思路如下:
- 对控件初始化,做相应的设置,头布局尾布局初始化,隐层尾布局的进度条;
- 进入页面手动刷新,加载数据,延迟1s模拟网络请求,加载数据,停止刷新(每次刷新都在原有基础上增加一条数据,区别刷新前后);
- recycleView.setOnScrollListener,监听中判断是否滑动到最底部,进而决定是否加载更多;
总结:
我个人觉得这一部分的关键点是recycleView.setOnScrollListener()监听中的逻辑判断,这里是实现上拉加载更多的关键地方,有没有小伙伴想到用这种方式实现下拉刷新呢?我之前也是认为可以实现的,但是真正去尝试去试着实现的时候,发现不太现实,主要问题就是:用这种方式实现下拉刷新,刷新效果的进度条肯定在头布局中,那么进入页面先刷新,后加载数据的效果就实现不了,因为先刷新,就要显示头布局,头布局显示的前提就是recycleView数据显示完整,也就是刷新之前,要先加载完整数据,这简直是个悖论!!!!
备注
- 这部分贴的代码是关于整篇文章内容的最终代码,算是一个demo,代码不够严谨(比如进度条隐藏展示前后是否手动停止或开启进度条更节省内存),重点看实现思路哈。
- 关于recycleView的item点击事件的实现思路,在代码中也已经实现了,没有单独拎出来而已,需要的同学可以关注adaper中接口RecycleViewItemClickCallBack的实现相关代码,或者自行www.baidu.com哈.
总结
到这里这篇文章基本接近尾声了,我有点小感触,作为Android开发人员,看再多的书,不上手敲代码,效率很低,上手敲代码,才会想到很多问题,才能看到很多细节东西。所以看书不错,但是一定要看着书敲一遍,才能学到更多东西。再者,有收获,就要写出来,写思路写收获的过程,其实是个自我梳理的过程,在这个过程中,你会强迫自己梳理,触类旁通,会有更多收获!这是我2019第一篇文章,希望自己能够坚持写文章,至少每个月一篇文章。另外劝各位王者召唤师们,少玩游戏吧,特么我今天驾驶本到期换本去,竟然说我色弱(最后给中介200大洋,才让我过了nnd),这在之前就从来都没有的,先不说色弱和玩手机有没关系,但是我感觉我视力一天不如一天,少玩手机,多看书,终究不会错的!!!