写在前面
这个是已经替代了ListView、GridView和瀑布流效果的一个插件,针对这个东西的使用我觉得有必要写一点东西来加深我对它的了解,一点点来,首先就是基本使用和分割线的设置。
一、基本使用
既然已经有了ListView、GridView为什么还需要RecyclerView这样的控件呢?
整体上看RecyclerView架构,提供了一种插拔式的体验,高度的解耦,异常的灵活,通过设置它提供的不同LayoutManager,ItemDecoration , ItemAnimator实现令人瞠目的效果。
首先我们来梳理一下他的基本使用:
功能点一:控制其显示的方式:通过布局管理器LayoutManager,ListView–>GridView–>瀑布流 只需要一行代码;
功能点二:控制item间的间隔,(可绘制),通过ItemDecoration(这个比较蛋疼)
功能点三:控制Item增删的动画,通过ItemAnimator;
功能点四:控制点击、长按事件,自己来写
二、具体实现
第一步:添加依赖:
implementation 'com.android.support:recyclerview-v7:27.1.1'
第二步:在xml中把recyclerView控件放进去:
<?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="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recy_clidfragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
</android.support.v7.widget.RecyclerView>
</RelativeLayout>
第三步:实现功能点一:设置显示方式;
在通过Adapter来绑定数据源之前我们需要通过setLayoutManage来设置不同显示方式(ListView、GridView、瀑布流)。
// 设置recyclerView的布局管理
// LinearLayoutManager -> ListView风格
// GridLayoutManager -> GridView风格
// StaggeredGridLayoutManager -> 瀑布流风格
LinearLayoutManager linearLayoutManager = new
LinearLayoutManager(this);
mRecyclerView.setLayoutManager(linearLayoutManager);
第四步:基本Adapter设置数据的方式:
package com.lay.laykypro.adapter.ClidFragment;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.lay.laykypro.R;
import java.util.HashMap;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MainClidRecyclerViewAdapter extends RecyclerView.Adapter<MainClidRecyclerViewAdapter.MainClidHolder> {
private List<HashMap<String, Object>> mdataList;
private Context mContext;
private LayoutInflater mLayoutInflater;
public MainClidRecyclerViewAdapter(List<HashMap<String, Object>> mdataList, Context mContext) {
this.mdataList = mdataList;
this.mContext = mContext;
this.mLayoutInflater = mLayoutInflater.from(mContext);
}
@Override
public MainClidHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = mLayoutInflater.inflate(R.layout.item_recyclerview_clidfragment, parent, false);
MainClidHolder mainClidHolder = new MainClidHolder(view);
return mainClidHolder;
}
@Override
public void onBindViewHolder(MainClidHolder holder, int position) {
//设置数据绑定
HashMap<String, Object> itemHashMap = mdataList.get(position);
//拿到控件
holder.tvRecyclerview.setText((String)itemHashMap.get("type"));
}
@Override
public int getItemCount() {
return mdataList.size();
}
class MainClidHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tv_recyclerview)
TextView tvRecyclerview;
public MainClidHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
}
【额外备注:我们一定要额外注意==和equals的区别,我拿的数据是抓取开眼的数据,比较复杂,我就没办法直接用一个JavaBean来代表,所以我用Retrofit+Rxjava下载Json数据之后,我使用原生Json解析来取的数据,结果就是在==上面粗心大意,还在那检查了好几遍,,,真是尴尬。】
【参考博客:java中==和equal的区别 - 小皇帝POP - 博客园 https://www.cnblogs.com/pop822/p/6215040.html】
言归正传,现在我们已经使用RecyclerView实现了简单ListView的功能,如下图:
第五步:实现分割线定制
RecyclerView并没有支持divider这样的属性。我们要实现他当然也可以通过直接在Item的布局中来写的方法,但是这样不太优雅,下面我们看看应该怎么在RecyclerView中定制一个它的分割线,代码如下:
1.首先我们要写一个类来继承一个RecyclerView.ItemDecoration,实现自定义分割线,代码如下:
package com.lay.laykypro.adapter.ClidFragment;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.view.View;
/**
* 定制RecyclerView的分割线
* 目标:10px的红色线
*/
public class MainClidItemDecoration extends RecyclerView.ItemDecoration {
private Paint mPaint;
public MainClidItemDecoration(int Color) {
this.mPaint = new Paint();
mPaint.setColor(Color);
mPaint.setAntiAlias(true);
}
/**
* 绘制分割线
* @param c
* @param parent
* @param state
*/
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
//利用Canvas想绘制什么就绘制什么
int childCount = parent.getChildCount();
Rect rect = new Rect();
rect.left=parent.getPaddingLeft();
rect.right=parent.getWidth()-parent.getPaddingRight();
for (int i = 1; i <childCount ; i++) {
View view = parent.getChildAt(i);
rect.bottom=view.getTop();
rect.top=rect.bottom-10;
c.drawRect(rect,mPaint);
}
}
/**
* 留出分割线位置
* @param outRect
* @param view
* @param parent
* @param state
*/
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
// super.getItemOffsets(outRect, view, parent, state);
//每个item下面流出10px(像素,单位可以单独找一篇博客来看看)来绘制分割线。 验证一号
//思路一:最后一个item下面不需要分割线;失败
//思路二:第一个上面不需要item的分割线;成功
int position=parent.getChildAdapterPosition(view);
//parent.getChildCount()是不断变化的,不能直接得到parent.getChildCount()的确定数字。所以思路一作废。
// if (position!=parent.getChildCount()-1){//如果不是最后一个item
// outRect.bottom=10;
// }
if (position!=0){
outRect.top=10;
}
}
}
2.我们要将这个分割线设置到RecyclerView上面去:
recyClidfragment.addItemDecoration(new MainClidItemDecoration(Color.RED));
结果如下:
注意:这里我写的分割线在第一个item的顶上和在最后一个item的底部没有。具体逻辑在代码中有请自行查阅。
3.使用Drawable来定制item分割线的方法。
Drawable资源代码:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="0.5dp" />
<solid android:color="@color/list_item_divider" />
</shape>
或者
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android:startColor="@color/colorAccent"
android:centerColor="@color/colorBlue"
android:endColor="@color/colorYellow"></gradient>
<size android:height="10dp" />
</shape>
或者
直接关联图片资源:
recyClidfragment.addItemDecoration(new MainClidItemDecoration(getActivity(),R.drawable.heng));
color资源代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="list_item_divider">#ccccff</color>
<color name="colorAccent">#FF4081</color>
<color name="colorYellow">#ffff00</color>
<color name="colorBlue">#66cccc</color>
</resources>
分割线类的代码如下:
package com.lay.laykypro.adapter.ClidFragment;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
/**
* 定制RecyclerView的分割线
* 目标:使用Drawable资源来定制分割线
*/
public class MainClidItemDecoration extends RecyclerView.ItemDecoration {
private Drawable mDrawable;
public MainClidItemDecoration(Context mContext, int drawableResId) {
this.mDrawable = ContextCompat.getDrawable(mContext,drawableResId);
}
/**
* 绘制分割线
* @param c
* @param parent
* @param state
*/
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
//利用Canvas想绘制什么就绘制什么
int childCount = parent.getChildCount();
Rect rect = new Rect();
rect.left=parent.getPaddingLeft();
rect.right=parent.getWidth()-parent.getPaddingRight();
for (int i = 1; i <childCount ; i++) {
View view = parent.getChildAt(i);
rect.bottom=view.getTop();
rect.top=rect.bottom-mDrawable.getIntrinsicHeight();
mDrawable.setBounds(rect);
mDrawable.draw(c);
}
}
/**
* 留出分割线位置
* @param outRect
* @param view
* @param parent
* @param state
*/
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
// super.getItemOffsets(outRect, view, parent, state);
//每个item下面流出10px(像素,单位可以单独找一篇博客来看看)来绘制分割线。 验证一号
//思路一:最后一个item下面不需要分割线;失败
//思路二:第一个上面不需要item的分割线;成功
int position=parent.getChildAdapterPosition(view);
//parent.getChildCount()是不断变化的,不能直接得到parent.getChildCount()的确定数字。所以思路一作废。
// if (position!=parent.getChildCount()-1){//如果不是最后一个item
// outRect.bottom=10;
// }
if (position!=0){
outRect.top=mDrawable.getIntrinsicHeight();
}
}
}
因为Color的不同,结果如下图所示:
一种颜色的细线:
三种颜色过度的粗线:
图片为资源的线:
结论:实际情况证明,用drawable来写的方式效率更高,上面基本实现了ListView样式下的万能分割线
4.GridView样式下的万能分割线。
xml代码如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorBlue"></solid>
<size android:height="1dp" android:width="1.5dp" />
</shape>
java部分代码:
Fragment部分:
//设置分割线
MainClidItemGridDecoration mainClidItemGridDecoration = new MainClidItemGridDecoration(getActivity(), R.drawable.itemdecoration);
recyClidfragment.addItemDecoration(mainClidItemGridDecoration);
分割线类:
package com.lay.laykypro.adapter.ClidFragment;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import com.lay.laykypro.R;
public class MainClidItemGridDecoration extends RecyclerView.ItemDecoration {
private Drawable mDrawable;
public MainClidItemGridDecoration(Context mContext,int drawableResId) {
this.mDrawable = ContextCompat.getDrawable(mContext, drawableResId);
}
@Override
public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
super.onDraw(canvas, parent, state);
//两个方向的绘制
drawVertical(canvas,parent);
drawHorizontal(canvas,parent);
}
private void drawHorizontal(Canvas c, RecyclerView parent) {
int childCount = parent.getChildCount();
Rect rect = new Rect();
for (int i = 0; i <childCount ; i++) {
View view = parent.getChildAt(i);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams();//使用这个是考虑到设置item中的背景间距的情况,在没有使用的时候这个部分可以删除。
rect.left=view.getLeft()-layoutParams.leftMargin;
rect.right=view.getRight()+layoutParams.rightMargin;
rect.top=view.getBottom();
rect.bottom=rect.top+mDrawable.getIntrinsicHeight();
mDrawable.setBounds(rect);
mDrawable.draw(c);
}
}
private void drawVertical(Canvas c, RecyclerView parent) {
int childCount = parent.getChildCount();
Rect rect = new Rect();
for (int i = 0; i <childCount ; i++) {
View view = parent.getChildAt(i);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams();//使用这个是考虑到设置item中的背景间距的情况,在没有使用的时候这个部分可以删除。
rect.left=view.getRight()+layoutParams.rightMargin;
rect.right=rect.left+mDrawable.getIntrinsicWidth();
rect.top=view.getTop()-layoutParams.topMargin-layoutParams.bottomMargin;
rect.bottom=view.getBottom()+mDrawable.getIntrinsicHeight();//高是线的厚度;
mDrawable.setBounds(rect);
mDrawable.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.bottom=mDrawable.getIntrinsicHeight();
outRect.right=mDrawable.getIntrinsicWidth();
}
}
由于这次考虑了item的布局文件中设置了layout_margin属性,所以附上item代码:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_margin="20dp"
android:background="@color/colorBlue"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_recyclerview"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="Keep"
android:textSize="45dp"/>
</RelativeLayout>
结果如下图:
注意:我目前的结果是最右边和最下边的item依然是有边框的,我们的目的应该是最右边和最下边item不能显示边框,在下一步解决它。
5.分割线的源码分析。
我打开了RecyclerView的源码,跟着网上一些大神的脚步,找到了关于分割线最重要的几部分源码,具体如下:
onDraw()
这段代码就是根据分割线的数量来不断的进行绘制。
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 回调出去直接在getItemOffsets留出分割线位置的基础上直接绘制
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
measureChild()
这个方法就是通过getItemDecorInsetsForChild()得到的Rect的结果来计算出item的高宽。
public void measureChild(View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// getItemDecorInsetsForChild 这个是关键
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
// 考虑分割线返回的Rect
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
getItemDecorInsetsForChild()
这个方法是通过getItemOffsets()方法中设置的Rect对象的参数来测量出item的分割线的大小。
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
// changed/invalid items should not be updated until they are rebound.
return lp.mDecorInsets;
}
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
// getItemOffsets()还是比较熟悉,获取分割线返回的占用位置
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
// 开始累加占用位置
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
// 返回分割线的Rect
return insets;
}
6.解决RecyclerView设置为GridView模式时的bug;
经过我各种查询,暂时将问题解决了一半,那就是,将最右边的一列的右侧分割线给去掉了,代码如下:
package com.lay.laykypro.adapter.ClidFragment;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import com.lay.laykypro.R;
/**
* 解决掉bug的GridView形式的分割线
* 思路:写出两个方法分别判断是否是最后一排和最后一列即可
*/
public class MainClidItemGridDecoration extends RecyclerView.ItemDecoration {
private Drawable mDrawable;
public MainClidItemGridDecoration(Context mContext,int drawableResId) {
this.mDrawable = ContextCompat.getDrawable(mContext, drawableResId);
}
@Override
public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
super.onDraw(canvas, parent, state);
//两个方向的绘制
drawVertical(canvas,parent);
drawHorizontal(canvas,parent);
}
/**
* 得到列数
* @param parent
* @return
*/
private int getSpanCount(RecyclerView parent){
int spanCount =-1;
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if(layoutManager instanceof GridLayoutManager){
spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
}
return spanCount;
}
/**
* 判断当前item是不是最后一列
* @param itemPosition
* @param parent
* @return
*/
private boolean isLastColum(int itemPosition,RecyclerView parent){
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if(layoutManager instanceof GridLayoutManager){
int spanCount = getSpanCount(parent);
if(itemPosition+1%spanCount==0){
return true;
}
}
return false;
}
/**
* 判断当前item是不是最后一行
* @param itemPosition
* @param parent
* @return
*/
private boolean isLastRow(int itemPosition,RecyclerView parent){
int spanCount = getSpanCount(parent);
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if(layoutManager instanceof GridLayoutManager){
int childCount = parent.getAdapter().getItemCount();//parent.getChildCount();这个不能用在这里,因为parent中的子个数在 getItemOffsets中是不断在变化的。依次加一。
//思路一:
// double currentRow = Math.ceil((double) (itemPosition + 1 )/ (double) spanCount);//当前行数
// double rowCount = Math.ceil((double) childCount / (double) spanCount);//总行数
// if(currentRow<rowCount){
// return false;
// }
//思路二:
childCount=childCount-childCount%spanCount;
if((itemPosition+1)>=childCount){
return true;
}
}
return false;
}
private void drawHorizontal(Canvas c, RecyclerView parent) {
int childCount = parent.getChildCount();
Rect rect = new Rect();
for (int i = 0; i <childCount ; i++) {
View view = parent.getChildAt(i);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams();//使用这个是考虑到设置item中的背景间距的情况,在没有使用的时候这个部分可以删除。
rect.left=view.getLeft()-layoutParams.leftMargin;
rect.right=view.getRight()+layoutParams.rightMargin;
rect.top=view.getBottom();
rect.bottom=rect.top+mDrawable.getIntrinsicHeight();
mDrawable.setBounds(rect);
mDrawable.draw(c);
}
}
private void drawVertical(Canvas c, RecyclerView parent) {
int childCount = parent.getChildCount();
Rect rect = new Rect();
for (int i = 0; i <childCount ; i++) {
View view = parent.getChildAt(i);
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams();//使用这个是考虑到设置item中的背景间距的情况,在没有使用的时候这个部分可以删除。
rect.left=view.getRight()+layoutParams.rightMargin;
rect.right=rect.left+mDrawable.getIntrinsicWidth();
rect.top=view.getTop()-layoutParams.topMargin-layoutParams.bottomMargin;
rect.bottom=view.getBottom()+mDrawable.getIntrinsicHeight();//高是线的厚度;
mDrawable.setBounds(rect);
mDrawable.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if(isLastColum(parent.getChildLayoutPosition(view),parent)){
outRect.set(0,0,mDrawable.getIntrinsicWidth(),0);
}
if(isLastRow(parent.getChildLayoutPosition(view),parent)){
outRect.set(0,0,0,mDrawable.getIntrinsicHeight());
}
}
}
结果如下:
但是我暂时还没有找到解决下面分割线的方法,如果有小哥指点一二,那就再好不过了。
第六步:实现RecyclerView的点击事件和长按事件。
写这个东西的思路就是;
1.首先先写个接口,(由于点击事件和长按事件的思路完全一样,我就写一个就行了。)
/**
* 点击事件接口
*/
public interface OnItemClickListener{
void onItemClick(int position);
}
2.然后,在onBindViewHolder方法中利用itemView的点击事件,将点击事件所做的事传给之前定义的接口,
//设置接口对象
private OnItemClickListener onItemClickListener;
//拿到控件
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onItemClickListener.onItemClick(position);
}
});
3.之后,在Adapter类中写一个设置点击事件的方法,setOnItemClickListener(OnItemClickListener itemClickListener)和
public void setOnItemClickListener(OnItemClickListener onItemClickListener){
this.onItemClickListener=onItemClickListener;
}
4.最后,在Activity或Fragment中将具体事件的执行写到内部类中即可完成;
mainClidRecyclerViewAdapter.setOnItemClickListener(new MainClidRecyclerViewAdapter.OnItemClickListener() {
@Override
public void onItemClick(int position) {
Toast.makeText(getActivity(),""+position,Toast.LENGTH_SHORT).show();
}
});
(如果需要设置某一个item中的控件的点击事件,思路还是如此,只需要在第二步把点击事件设置给item中的具体控件即可。)
写在最后
这个应该是我目前写得最长的一篇,不过写的过程很舒服,我还会再接再厉,加油!
参考:Android 图形基础类Rect,扎实基础助腾飞 - 王亟亟的博客 - CSDN博客 https://blog.csdn.net/ddwhan0123/article/details/53885732