RecyclerView是谷歌推出的代替ListView的列表控件。关于它的基本使用,它的封装,添加头脚布局等,比较简单就不再描述,这里主要是记录一些知识点。
一 . RecyclerView间隔线
RecyclerView并没有现成的间隔线,我们需要自己去绘制间隔线,创建一个类,继承RecyclerView.ItemDecoration,重写其中的两个方法,就可以绘制出自己的间隔线了。
1 . LinearLayoutManager的垂直和水平状态下的间隔线。
第一个方法:getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)。
第二个方法:onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)
2 . 获得该矩形区域之后,在getItemOffsets中设置该矩形区域的左上右下的偏移量。
3 . 在onDraw(Canvas c, RecyclerView parent,RecyclerView.State state)中,将事先准备好的分割线(使用系统的或者使用shape文件自己绘制)画到参数canvas上,在画之前需要计算出左上右下的坐标。
代码如下:绘制LinearLayoutManager中水平和垂直的分割线,注释已经写好
public class MyItemDecoration extends RecyclerView.ItemDecoration {
private final int[] attrs = new int[]{
android.R.attr.listDivider
};
private int orientation;
private Drawable mDivider;//分割线
public MyItemDecoration(Context context, int orientation) {
//1. 获取系统的分割线
TypedArray ta = context.obtainStyledAttributes(attrs);//获取系统的分割线
mDivider = ta.getDrawable(0);
ta.recycle();
//2. 获取自定义的分割线
mDivider = context.getResources().getDrawable(R.drawable.item_divier);
setOrientation(orientation);
}
//设置垂直或者是水平方向
private void setOrientation(int orientation){
if(orientation != LinearLayoutManager.HORIZONTAL &&
orientation != LinearLayoutManager.VERTICAL){
throw new IllegalArgumentException(
"请传入LinearLayoutManager.HORIZONTAL或者LinearLayoutManager.VERTICAL");
}
this.orientation = orientation;
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
//2。调用该方法,在这里用户自己绘制剑阁线,RecyclerView会在获取条目间隙之后调用该方法
if(orientation == LinearLayoutManager.VERTICAL){
//垂直
drawVertical(c,parent);
}else if(orientation == LinearLayoutManager.HORIZONTAL){
//水平
drawHorizontal(c,parent);
}
}
/**
* 当RecyclerView为水平方向时,画分割线
* 方法和drawVertical很像,也是先算出上下左右,再把图片画到画布上
* @param c
* @param parent
*/
private void drawHorizontal(Canvas c, RecyclerView parent) {
//左侧,就是RecyclerView左侧值,如果有paddingLeft,那么需要减去
int top = parent.getPaddingTop();
//右侧,RecyclerView右侧值,如果有paddingRight值,那么需要减去
int bottom = parent.getHeight() - parent.getPaddingBottom();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++){
View child = parent.getChildAt(i);
//计算top和bottom,
RecyclerView.LayoutParams params
= (RecyclerView.LayoutParams) child.getLayoutParams();
//top child.getBottom()获取到该条目,距离顶部的位置, + 该条目的margin值, + 可能使用动画效果,产生的偏移量
int left = child.getRight() + params.rightMargin + Math.round(ViewCompat.getTranslationX(child));
//bottom 算好了top bottom只需要在top的基础上添加一个分割线的宽度即可。
int right = left + mDivider.getIntrinsicWidth();
mDivider.setBounds(left,top,right,bottom);
mDivider.draw(c);
}
}
/**
* 当RecyclerView为垂直的时,画分割线
* 方法很简单: 算好分割线的左上右下坐标,使用canvas,把图片画到canvas上
* @param c
* @param parent
*/
private void drawVertical(Canvas c, RecyclerView parent) {
//左侧,就是RecyclerView左侧值,如果有paddingLeft,那么需要减去
int left = parent.getPaddingLeft();
//右侧,RecyclerView右侧值,如果有paddingRight值,那么需要减去
int right = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++){
View child = parent.getChildAt(i);
//计算top和bottom,
RecyclerView.LayoutParams params
= (RecyclerView.LayoutParams) child.getLayoutParams();
//top child.getBottom()获取到该条目,距离顶部的位置, + 该条目的margin值, + 可能使用动画效果,产生的偏移量
int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child));
//bottom 算好了top bottom只需要在top的基础上添加一个分割线的宽度即可。
int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left,top,right,bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//1。首先调用该方法,会获取条目之间的间隙宽度,即Rect矩形区域,
//每一个item都会调用一次该方法
if(orientation == LinearLayoutManager.VERTICAL){
//垂直
//这个上下左右分割线的偏移值
outRect.set(0,0,0,mDivider.getIntrinsicHeight());
}else{
//水平
outRect.set(0,0,mDivider.getIntrinsicWidth(),0);
}
}
}
这里需要注意一点:
2.GridLayoutManager绘制分割线。
GridLayoutManager是网格样式的列表,因此,item不可能绘制4边的分割线,如果绘制了的话,会出现重叠的情况。 只需要绘制右下的网格线即可。方法和LinearLayoutManager一样,直接贴出代码,关键点可以看代码上的注释。
public class GridViewItemDecoration extends RecyclerView.ItemDecoration {
private final Drawable mDivider;
public GridViewItemDecoration(Context context) {
//获取到自定义的分割线
mDivider = context.getResources().getDrawable(R.drawable.item_divier);
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
drawVertical(c,parent);
drawHorizontal(c,parent);
}
/**
* 绘制水平分割线
* @param c
* @param parent
*/
private void drawHorizontal(Canvas c, RecyclerView parent) {
int childCount = parent.getChildCount();
int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
for(int i = 0; i < childCount ; i++){
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params
= (RecyclerView.LayoutParams) child.getLayoutParams();
if(i < spanCount){
//第一行
int top = child.getTop() + params.topMargin;
int bottom = top + mDivider.getIntrinsicHeight();
int left = child.getLeft() + params.leftMargin;
int right = child.getRight() + params.rightMargin;
mDivider.setBounds(left,top,right,bottom);
mDivider.draw(c);
}
int top = child.getBottom() + params.bottomMargin;
int bottom = top + mDivider.getIntrinsicHeight();
int left = child.getLeft() + params.leftMargin;
int right = child.getRight() + params.rightMargin;
mDivider.setBounds(left,top,right,bottom);
mDivider.draw(c);
}
}
/**
* 绘制垂直分割线
* @param c
* @param parent
*/
private void drawVertical(Canvas c, RecyclerView parent) {
int childCount = parent.getChildCount();
int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
int cloum = childCount / spanCount;//行数
Log.i("行数", "drawVertical: "+cloum);
for(int i = 0; i < childCount ; i++){
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params
= (RecyclerView.LayoutParams) child.getLayoutParams();
for(int j = 0; j <= cloum ; j++){
if(i == spanCount*j){
/*
算出每一行的第一列: spanCount是用户输入的列数,如果spanCount=4
那么每一行的第一列就是0 4 8 。。。。。即spanCount的整数倍,
如果cloum=5 5行,那么最后一行的第一列就是spanCount*cloum
*/
//top
int top = child.getTop() - params.topMargin;
//bottom不以child为标准,用整个recyclerView的bottom为标准
int bottom = child.getBottom() + params.bottomMargin;
int left = child.getLeft() + params.leftMargin;
int right = left + mDivider.getIntrinsicWidth();
mDivider.setBounds(left,top,right,bottom);
mDivider.draw(c);
}
}
int top = child.getTop() - params.topMargin;
int bottom = child.getBottom() + params.bottomMargin;
int left = child.getRight() + params.rightMargin;
int right = left + mDivider.getIntrinsicWidth();
mDivider.setBounds(left,top,right,bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
//在这里,获取矩形区域的左上右下的偏移量
int right = mDivider.getIntrinsicWidth();
int bottom = mDivider.getIntrinsicHeight();
outRect.set(0,0,right,bottom);
}
}
这里要注意的就是,算出第一行的item和每一行第一列的item,具体怎么算已经在注释中给出。
除了第一行的上部和第一列的左侧添加上分割线之外,还有一种设计是最后一行的bottom和最后一列的右侧都不要添加上分割线。如果要实现这样的效果,只需要在getItemOffsets()中做文章就行了,如果是最后一行,下侧偏移量设置为0,如果是最后一列,右侧偏移量设置为0,代码如下所示,注释在代码中写清楚了。
@Override
public void getItemOffsets(@NonNull Rect outRect, int itemPosition, @NonNull RecyclerView parent) {
super.getItemOffsets(outRect, itemPosition, parent);
int right = mDivider.getIntrinsicWidth();
int bottom = mDivider.getIntrinsicHeight();
if(isLastColum(itemPosition,parent)){
//如果是最后一列,右侧偏移量设置为0
right = 0;
}
if (isLastRow(itemPosition,parent)){
//如果是最后一行,下侧偏移量设置为0
bottom = 0;
}
outRect.set(0,0,right,bottom);
}
/**
* 是否是最后一行
* @param itemPosition
* @param parent
* @return
*/
private boolean isLastRow(int itemPosition, RecyclerView parent) {
int spanCount = getSpanCount(parent);
if(spanCount!=-1){
//最后一行
int childCount = parent.getChildCount();
int lastRowCount = childCount % spanCount;
if( lastRowCount == 0 || lastRowCount < spanCount){
return true;
}
}else{
Snackbar.make(parent,"请传入GridLayoutManager",Snackbar.LENGTH_SHORT).show();
}
return false;
}
private int getSpanCount(RecyclerView parent) {
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if(layoutManager instanceof GridLayoutManager){
GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
int spanCount = gridLayoutManager.getSpanCount();
return spanCount;
}
return -1;
}
/**
* 是否是最后一列
* @param itemPosition
* @param parent
* @return
*/
private boolean isLastColum(int itemPosition, RecyclerView parent) {
int spanCount = getSpanCount(parent);
if(spanCount!=-1){
if((itemPosition+1)%spanCount == 0){
return true;
}
}else{
Snackbar.make(parent,"请传入GridLayoutManager",Snackbar.LENGTH_SHORT).show();
}
return false;
}
另外可以通过修改Theme.Appcompat主题样式里面的android:listSelector或者 android:listDivider属性达到改变间隔线的大小和颜色。
<style name="AppTheme" parent="AppBaseTheme">
<item name="android:listDivider">@drawable/item_divider</item>
</style>
二.RecyclerView条目拖拽动画。
这里主要是使用ItemTouchHelper进行条目的拖拽。先来看下效果。
可拖拽RecyclerView效果图
看看效果,是不是非常的炫酷,其实实现这个效果非常的简单,只需要使用Android提供的ItemTouchHelper即可,那么什么是ItemTouchHelper呢,官方文档是这么解释的:
This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.It works with a RecyclerView and a Callback class, which configures what type of interactions are enabled and also receives events when user performs these actions.Depending on which functionality you support, you should override onMove(RecyclerView, ViewHolder, ViewHolder) and / or onSwiped(ViewHolder, int).
ItemTouchHelper是一个工具类,可实现侧滑删除和拖拽移动,使用这个工具类需要RecyclerView和Callback。同时根据需要重写onMove和onSwiped方法。
根据官方文档的指导,我们来写一个类继承ItemTouchHelper.Callback,并重写其中的方法,我们看到源码中,ItemTouchHelper.Callback中有几个必须要重写的方法
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction)
-
getMovementFlags()
用于设置是否处理拖拽事件和滑动事件,以及拖拽和滑动操作的方向,有以下两种情况:
1.如果是列表类型的,拖拽只有ItemTouchHelper.UP、ItemTouchHelper.DOWN两个方向
2.如果是网格类型的,拖拽则有UP、DOWN、LEFT、RIGHT四个方向
另外,滑动方向列表类型的,有START和END两个方法,如果是网格类型的一般不设置支持滑动操作可以将swipeFlags 置为0,表示不支持swipe,
最后,需要调用return makeMovementFlags(dragFlags, swipeFlags);将设置的标志位return回去! -
onMove()
如果我们设置了相关的dragFlags ,那么当我们长按item的时候就会进入拖拽并在拖拽过程中不断回调onMove()方法,我们就在这个方法里获取当前拖拽的item和已经被拖拽到所处位置的item的ViewHolder。 -
onSwipe()
如果我们设置了相关的swipeFlags,那么当我们滑动item的时候就会调用onSwipe()方法,一般的话在使用LinearLayoutManager的时候,在这个方法里可以删除item,来实现滑动删除!
另外还有4个方法:
//开启长按拖拽功能,默认为true
//如果设置为false,手动开启,调用startDrag()
@Override
public boolean isLongPressDragEnabled() {
return true;
}
//开始滑动功能,默认为true
//如果设置为false,手动开启,调用startSwipe()
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState)
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
-
onSelectedChanged()
这个方法在选中Item的时候(拖拽或者滑动开始的时候)调用,通常这个方法里我们可以改变选中item的背景颜色等,高亮表示选中来提高用户体验。
需要注意的是,这里的第二个参数int actionState,它有以下3个值,分别表示3种状态:
ACTION_STATE_IDLE:闲置状态
ACTION_STATE_SWIPE:滑动状态
ACTION_STATE_DRAG:拖拽状态 -
clearView()
这个方法在当手指松开的时候(拖拽或滑动完成的时候)调用,这时候我们可以将item恢复为原来的状态。
学习完了理论知识,我们来写第二部分开头的效果。
1. 编写一个类继承ItemTouchHelper.Callback,重写其中的方法。
public class MyItemTouchHelperCallBack extends ItemTouchHelper.Callback {
private boolean isMove;
//在CallBack回调时首先调用,用来判断当前是什么动作(比如方向)
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
int up = ItemTouchHelper.UP;//向上
int down = ItemTouchHelper.DOWN;//向下
int right = ItemTouchHelper.RIGHT;//向右
int left = ItemTouchHelper.LEFT;//向左
int dragFlags = up | down;
int swipeFlags = right | left;//如果设置为0 ,那么禁止侧滑
int flag = makeMovementFlags(dragFlags, swipeFlags);
return flag ;
}
//上下拖拽的时候调用
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder srcHolder, @NonNull RecyclerView.ViewHolder targetHolder) {
return true;
}
// 侧滑时调用
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
}
//返回true 长按可以拖拽 返回false 长按不可以拖拽
@Override
public boolean isLongPressDragEnabled() {
return true;
}
//拖拽或者侧滑时调用
@Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){
//当不为空闲时,更改条目颜色
viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext()
.getResources().getColor(R.color.lightgray));
}
super.onSelectedChanged(viewHolder, actionState);
}
/**
* 拖拽完成时调用
* @param recyclerView
* @param viewHolder
*/
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
// 拖拽完成时,设置条目颜色为白色 viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext()
.getResources().getColor(R.color.white));
super.clearView(recyclerView, viewHolder);
}
}
2. 在Activity中,将ItemTouchHelper设置给RecyclerView。
ItemTouchHelper.Callback callback = new MyItemTouchHelperCallBack(adapter);
itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(recyclerView);
到这里,可以实现RecyclerView条目的拖拽和侧滑。但是要实现条目的交换和删除,需要与Adapter进行操作,那么我们就需要用到接口回调,在拖拽时Adapter中需要进行条目的交换,在侧滑时在Adapter中进行条目的删除。
3. 接口的编写
public interface ItemChangeListener {
/**
* 上下拖拽时调用 item交换
* @param fromPosition
* @param toPosition
* @return 是否交换
*/
boolean ItemChange(int fromPosition,int toPosition);
/**
* 条目删除时调用
* @param position
*/
void ItemRemoved(int position);
}
编写了接口,就得在该调用的时候调用其中的方法, 分析一下,在什么时候需要交换条目呢?当然是在拖拽的时候。什么时候需要删除条目呢?当然是在侧滑的时候,有了这个思路,我们就需要在onMove()和onSwipe()中调用接口中的两个方法,那么就需要在构造方法中传入ItemChangeListener。
改写后的MyItemTouchHelperCallBack:
public class MyItemTouchHelperCallBack extends ItemTouchHelper.Callback {
private ItemChangeListener listener;
private boolean isMove;
//1.在构造方法中传入ItemChangeListener
public MyItemTouchHelperCallBack(ItemChangeListener listener){
this.listener = listener;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
int up = ItemTouchHelper.UP;
int down = ItemTouchHelper.DOWN;
int right = ItemTouchHelper.RIGHT;
int left = ItemTouchHelper.LEFT;
int dragFlags = up | down;
int swipeFlags = right | left;
int i = makeMovementFlags(dragFlags, swipeFlags);
return I;
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder srcHolder, @NonNull RecyclerView.ViewHolder targetHolder) {
if(srcHolder.getItemViewType() != targetHolder.getItemViewType()){
//如果条目是不同类型,那么就不让拖拽
return false;
}
if(listener != null){
//在move的时候,调用ItemChange
isMove = listener.ItemChange(srcHolder.getAdapterPosition(), targetHolder.getAdapterPosition());
}
return isMove;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
if(listener != null){
//在swipe时,调用ItemRemoved方法
listener.ItemRemoved(viewHolder.getAdapterPosition());
}
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){
viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext()
.getResources().getColor(R.color.lightgray));
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext()
.getResources().getColor(R.color.white));
super.clearView(recyclerView, viewHolder);
}
}
4. adapter需要实现ItemChangeListener,重写其中的方法,在方法中实现条目的交换与删除。
//1.实现ItemChangeListener 并重写其中的方法
public class QQAdapter extends RecyclerView.Adapter<QQAdapter.MyViewHolder> implements ItemChangeListener{
private StartDragListener listener;
private List<QQMessage> list;
public QQAdapter(List<QQMessage> list,StartDragListener listener) {
this.list = list;
this.listener = listener;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View view = KimLiuUtils.RvInflate(viewGroup, R.layout.listitem);
MyViewHolder myViewHolder = new MyViewHolder(view);
return myViewHolder;
}
@Override
public void onBindViewHolder(@NonNull final MyViewHolder myViewHolder, int i) {
QQMessage qqMessage = list.get(i);
myViewHolder.iv_logo.setImageResource(qqMessage.getLogo());
myViewHolder.tv_lastMsg.setText(qqMessage.getLastMsg()+"");
myViewHolder.tv_name.setText(qqMessage.getName()+"");
myViewHolder.tv_time.setText(qqMessage.getTime()+"");
myViewHolder.iv_logo.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if(listener != null){
listener.startDrag(myViewHolder);
}
return false;
}
});
}
@Override
public int getItemCount() {
return list.size();
}
//2.在这里实现条目的交换
@Override
public boolean ItemChange(int fromPosition, int toPosition) {
//1. 交换数据 2. 交换位置 刷新
Collections.swap(list,fromPosition,toPosition);
notifyItemMoved(fromPosition,toPosition);
return true;
}
//3.在这里实现条目的删除
@Override
public void ItemRemoved(int position) {
//1.删除数据 2.刷新
list.remove(position);
notifyItemRemoved(position);
}
class MyViewHolder extends RecyclerView.ViewHolder{
private final TextView tv_lastMsg;
private final TextView tv_name;
private final ImageView iv_pop;
private final TextView tv_time;
private ImageView iv_logo;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
iv_logo = itemView.findViewById(R.id.iv_logo);
tv_lastMsg = itemView.findViewById(R.id.tv_lastMsg);
tv_name = itemView.findViewById(R.id.tv_name);
iv_pop = itemView.findViewById(R.id.iv_pop);
tv_time = itemView.findViewById(R.id.tv_time);
}
}
}
至此,我们已经实现了上面的效果,但是要在触摸头像时实现条目的拖拽,可以提高用户的体验,当然也可以不实现,如果要实现,还是使用方法回调,在touch头像的时候调用接口中的方法,在方法的实现中,开始拖拽(调用itemTouchHelper.startDrag(viewHolder))。
实现这样一个功能是不是很简单呢,只要能用好ItemTouchHelper就行,通常我们在App中总是可以看到网格状的RecyclerView要进行拖拽,其实原理和这个相同,这里就不再赘述。
如果对我的内容感兴趣,可以关注我的公众号:平头哥写代码。内容不多,但都是精选。
平头哥写代码
网友评论