1. RecyclerView概述
从Android 5.0开始,谷歌公司推出了一个用于大量数据展示的新控件RecylerView,可以用来代替传统的ListView,更加强大和灵活。
在ListView中 改变列表某一个item数据,然后刷新列表,会回到最顶部,而RecyclerView可以保持原来滑动的位置不变。
在使用RecyclerView时,可能会涉及到如下内容:
- 想要控制其Item们的排列方式,使用布局管理器
LayoutManager
- 如果要创建一个适配器,请使用
RecyclerView.Adapter
- 想要控制Item间的间隔,请使用
RecyclerView.ItemDecoration
- 想要控制Item增删的动画,请使用
RecyclerView.ItemAnimator
- CardView扩展
FrameLayout
类并能够显示卡片内信息,这些信息在整个平台中拥有一致的呈现方式。CardView小部件可拥有阴影和圆角。
上述功能主要涉及到的方法如下:
mRecyclerView = findView(R.id.id_recyclerview);
//设置布局管理器
mRecyclerView.setLayoutManager(layout);
//设置adapter
mRecyclerView.setAdapter(adapter)
//设置Item增加、移除动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//添加分割线
mRecyclerView.addItemDecoration(new DividerItemDecoration(
getActivity(), DividerItemDecoration.HORIZONTAL_LIST));
如上图所示,如果要使用RecyclerView,必须指定一个Adapter和LayoutManager
2. RecyclerView使用
2.1 使用流程
RecyclerView定义在support库当中,想要使用该控件,需要在项目的build.gradle中添加相应的依赖库。在app/build.gradle文件,在dependencies闭包中添加当下的版本:
implementation 'com.android.support:recyclerview-v7:28.0.0'
同步之后,就可以展开对RecyclerView的使用,其使用方式其实与ListView的三要素类似。
在具体演示RecyclerView使用之前,左中了解一下其适配器:RecyclerView.Adapter
2.2 RecyclerView.Adapter
该适配器是一个抽象类,并支持泛型
public abstract static class Adapter {
...
}
如果创建一个适配器继承RecyclerView.Adapter
,需要重写三个方法:
onCreateViewHolder()
onBindViewHolder()
getItemCount()
指定泛型后,方法一与方法二会根据泛型改变。
创建适配器时,一般会先定义一个ViewHolder内部类继承RecyclerView.ViewHolder,则指定的泛型即为该ViewHolder,可以在其内定义每个列表上的视图控件,并在onCreateViewHolder中初始化
-
onCreateViewHolder()
示例
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
该方法主要用来创建ViewHolder,可以根据需求的itemType
,创建出多个ViewHolder
。创建多个itemType
时,需要getItemViewType(int position)
方法配合
-
onBindViewHolder()
示例
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
该方法主要是将layout视图控件与ViewHolder中元件属性绑定。
-
getItemCount()
示例
@Override
public int getItemCount() {
return mFruitList.size();
}
这个方法的返回值,便是RecyclerView
中实际item
的数量。有些情况下,当增加了HeaderView
或者FooterView
后,需要注意考虑这个返回值
介绍了RecyclerView.Adapter的这三个方法,现在简单示例RecyclerView的使用
- 在xml中添加RecyclerView控件
- 新建RecyclerView需要展示的界面xml布局
- 定义数据类
public class Fruit {
private String name;
private int imageId;
public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}
public String getName() {
return name;
}
public int getImageId() {
return imageId;
}
}
- 新建适配器,将其绑定到RecyclerView
public class FruitAdapter extends RecyclerView.Adapter {
private List mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view) {
super(view);
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List fruitList) {
mFruitList = fruitList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
- 使用RecyclerView
public class MainActivity extends AppCompatActivity {
private List fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit(getRandomLengthName("Apple"), R.drawable.apple_pic);
fruitList.add(apple);
Fruit banana = new Fruit(getRandomLengthName("Banana"), R.drawable.banana_pic);
fruitList.add(banana);
Fruit orange = new Fruit(getRandomLengthName("Orange"), R.drawable.orange_pic);
fruitList.add(orange);
Fruit watermelon = new Fruit(getRandomLengthName("Watermelon"), R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pear = new Fruit(getRandomLengthName("Pear"), R.drawable.pear_pic);
fruitList.add(pear);
Fruit grape = new Fruit(getRandomLengthName("Grape"), R.drawable.grape_pic);
fruitList.add(grape);
Fruit pineapple = new Fruit(getRandomLengthName("Pineapple"), R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit(getRandomLengthName("Strawberry"), R.drawable.strawberry_pic);
fruitList.add(strawberry);
Fruit cherry = new Fruit(getRandomLengthName("Cherry"), R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit mango = new Fruit(getRandomLengthName("Mango"), R.drawable.mango_pic);
fruitList.add(mango);
}
}
private String getRandomLengthName(String name) {
Random random = new Random();
int length = random.nextInt(20) + 1;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++){
builder.append(name);
}
return builder.toString();
}
}
除了除了LinearLayoutManager之外,还提供了GridLayoutManager和StaggeredGridLayoutManager这两种内置的布局排列方式。前者实现网格布局,后者可以用于实现瀑布流布局。
备注:
在onCreateViewHolder()中,映射Layout必须为
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false);
而不能是
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, null);
2.3 万能适配器
创建RecyclerView的Adapter时,发现实现过程大同小异,可以创建一个万能适配器,快捷创建Adapter。
下面给出一个简易版万能适配器,学习了RecyclerView其他部分内容在给出完成版。
public abstract class QuickAdapter extends RecyclerView.Adapter{
private List mDatas;
public QuickAdapter(List datas){
this.mDatas = datas;
}
public abstract int getLayoutId(int viewType);
@Override
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
return VH.get(parent,getLayoutId(viewType));
}
@Override
public void onBindViewHolder(VH holder, int position) {
convert(holder, mDatas.get(position), position);
}
@Override
public int getItemCount() {
return mDatas.size();
}
public abstract void convert(VH holder, T data, int position);
static class VH extends RecyclerView.ViewHolder{...}
}
其中QuickAdapter.VH的实现如下:
static class VH extends RecyclerView.ViewHolder{
private SparseArray mViews;
private View mConvertView;
private VH(View v){
super(v);
mConvertView = v;
mViews = new SparseArray<>();
}
public static VH get(ViewGroup parent, int layoutId){
View convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
return new VH(convertView);
}
public T getView(int id){
View v = mViews.get(id);
if(v == null){
v = mConvertView.findViewById(id);
mViews.put(id, v);
}
return (T)v;
}
public void setText(int id, String value){
TextView view = getView(id);
view.setText(value);
}
}
其中:
- getLayoutId(viewType)是根据viewType返回布局ID
- convert()做具体Bind操作
设置完上述的万能适配器,如果要创建一个Adapter时,只需:
mAdapter = new QuickAdapter(data) {
@Override
public int getLayoutId(int viewType) {
switch(viewType){
case TYPE_1:
return R.layout.item_1;
case TYPE_2:
return R.layout.item_2;
}
}
public int getItemViewType(int position) {
if(position % 2 == 0){
return TYPE_1;
} else{
return TYPE_2;
}
}
@Override
public void convert(VH holder, Model data, int position) {
int type = getItemViewType(position);
switch(type){
case TYPE_1:
holder.setText(R.id.text, data.text);
break;
case TYPE_2:
holder.setImage(R.id.image, data.image);
break;
}
}
};
3. LayoutManager自定义与使用
3.1 概述
如果说Adapter负责提供View,而LayoutManger则负责它们在RecyclerView中摆放的位置以及在窗口中不可见之后的回收策略。
RecyclerView提供的布局管理器:
-
LinearLayoutManager
以垂直或水平滚动列表方式显示项目 -
GridLayoutManager
在网格中显示项目。 -
StaggeredGridLayoutManager
在分散对齐网格中显示项目。
这里简单介绍LinearLayoutManager的几个重要方法,分析一下LayoutManger的实现
-
onLayoutChildren()
: 对RecyclerView进行布局的入口方法。 -
fill()
: 负责填充RecyclerView。 -
scrollVerticallyBy()
:根据手指的移动滑动一定距离,并调用fill()填充。 -
canScrollVertically()
或canScrollHorizontally()
: 判断是否支持纵向滑动或横向滑动。
其中onLayoutChildren()
的核心实现如下:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler); //将原来所有的Item View全部放到Recycler的Scrap Heap或Recycle Pool
fill(recycler, mLayoutState, state, false); //填充现在所有的Item View
}
RecyclerView的回收机制有个重要的概念,即将回收站分为Scrap Heap
和Recycle Pool
,其中Scrap Heap的元素可以被直接复用,而不需要调用onBindViewHolder()。detachAndScrapAttachedViews()会根据情况,将原来的Item View放入Scrap Heap或Recycle Pool,从而在复用时提升效率
fill()
是对剩余空间不断地调用layoutChunk()
,直到填充完为止。layoutChunk()的核心实现如下:
public void layoutChunk() {
View view = layoutState.next(recycler); //调用了getViewForPosition()
addView(view); //加入View
measureChildWithMargins(view, 0, 0); //计算View的大小
layoutDecoratedWithMargins(view, left, top, right, bottom); //布局View
}
其中next()
调用了getViewForPosition(currentPosition),该方法是从RecyclerView的回收机制实现类Recycler中获取合适的View。
在具体演示如何自定义LayoutManager之前,先介绍一个常用的API
recycler.getViewForPosition(position)
获取位置为position的View。
getPosition(View view)
获取view的位置。
measureChildWithMargins(View child, int widthUsed, int heightUsed)
测量view的宽高,包括外边距。
layoutDecoratedWithMargins(View child, int left, int top, int right,int bottom)
将child显示在RecyclerView上面,
left,top,right,bottom
规定了显示的区域。detachView(View child)
临时回收view。
attachView(View child)
将
detachView(View child)
回收的view拿回来。detachAndScrapAttachedViews(RecyclerView.Recycler recycler)
用指定的recycler临时移除所有添加的views。
detachAndScrapView(View child, RecyclerView.Recycler recycler)
用指定的recycler临时回收view。
removeAndRecycleAllViews(RecyclerView.Recycler recycler)
移除所有的view并且用给的recycler回收。
removeAndRecycleView(View child, RecyclerView.Recycler recycler)
移除指定的view并且用给的recycler回收。
offsetChildrenHorizontal(int dx)
水平移动所有的view,同样也有
offsetChildrenVertical(int dy)
3.2 简单示例
这里我们通过演示一个简单示例来介绍一下自定义一个LayoutManager需要实现的核心方法。
- 首先生成一个类,例如CustomLayoutManager,派生自LayoutManager
public class CustomLayoutManager extends LayoutManager {
@Override
public LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
}
派生自LayoutManager时,会强制让我们生成generateDefaultLayoutParams()
方法,即RecyclerView Item的布局参数。无特殊要求,直接让子Item自己决定宽高即可
- 添加
onLayoutChild()
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//定义竖直方向的偏移量
int offsetY = 0;
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
layoutDecorated(view, 0, offsetY, width, offsetY + height);
offsetY += height;
}
}
这里做了两件事:
把所有Item对应的view加载进来
把所有Item摆在其应在的地方
这个方法是LayoutManager的主入口,在view需要初始化布局时调用(适配器数据改变或适配器替换时再次被调用)。
通常来说,在这个方法中你需要完成主要步骤如下:
- 在滚动事件结束后检查所有附加视图当前的偏移位置。
- 判断是否需要添加新视图填充由滚动屏幕产生的空白部分。并从 Recycler 中获取视图。
- 判断当前视图是否不再显示。移除它们并放置到 Recycler 中。
- 判断剩余视图是否需要整理。发生上述变化后可能 需要你修改视图的子索引来更好地和它们的适配器位置校准。
这个示例较为简单,只是通过measureChildWithMargins(view, 0, 0)
;函数测量这个view,并且通过getDecoratedMeasuredWidth(view)
得到测量出来的宽度,需要注意的是通过getDecoratedMeasuredWidth(view)得到的是item+decoration的总宽度。如果你只想得到view的测量宽度,通过view.getMeasuredWidth()就可以得到了
然后通过layoutDecorated();函数将每个item摆放在对应的位置,每个Item的左右位置都是相同的,从左侧x=0开始摆放,只是y的点需要计算。所以这里有一个变量offsetY,用以累加当前Item之前所有item的高度。
- 添加滑动效果
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
// 平移容器内的item
offsetChildrenVertical(-dy);
return dy;
}
通过在canScrollVertically()
中return true;使LayoutManager具有垂直滚动的功能。然后在scrollVerticallyBy()
中接收每次滚动的距离dy。
其中dy表示手指在屏幕上每次滑动的位移:
- 当手指由下往上滑时,dy>0
- 当手指由上往下滑时,dy<0
明显手指上滑,子Item上移,需要减去dy,让item移动-dy距离是合理的。可以通过offsetChildrenVertical()
移动所有item
- 添加异常判断
前述代码实现滚动,但是Item到顶或者底仍可以继续滚动,需要加以判断是否到顶或者到底
判断到顶了
判断到顶只需把所有dy相加,如果小于0,表示到顶,不再移动
private int mSumDy = 0;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
int travel = dy;
//如果滑动到最顶部
if (mSumDy + dy < 0) {
travel = -mSumDy;
}
mSumDy += travel;
// 平移容器内的item
offsetChildrenVertical(-travel);
return dy;
}
判断到底了
判断到底的方法是用总高度减去最后一屏高度,即到底的偏移值。如果大于这个偏移值,则说明超过底部。
首先要得到所有Item的总高度,在onLayoutChildren中会测量所有的item并对每一个item布局,只需在onLayoutChildren()
中将所有的item高度相加即可得到所有Item的总高度。
private int mTotalHeight = 0;
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//定义竖直方向的偏移量
int offsetY = 0;
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
layoutDecorated(view, 0, offsetY, width, offsetY + height);
offsetY += height;
}
//如果所有子View的高度和没有填满RecyclerView的高度,
// 则将高度设置为RecyclerView的高度
mTotalHeight = Math.max(offsetY, getVerticalSpace());
}
private int getVerticalSpace() {
return getHeight() - getPaddingBottom() - getPaddingTop();
}
其中getVerticalSpace()
函数可以得到RecyclerView用于显示item的真实高度。接着在srcollVerticallyBy中判断到底与否并处理:
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
int travel = dy;
//如果滑动到最顶部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
travel = mTotalHeight - getVerticalSpace() - mSumDy;
}
mSumDy += travel;
// 平移容器内的item
offsetChildrenVertical(-travel);
return dy;
}
mSumDy + dy 表示当前的移动距离,mTotalHeight - getVerticalSpace()表示当滑动到底时滚动的总距离;
3.3 LayoutManager附加特性
LayoutManager中提供了一些辅助方法操作decoration(关于ItemDecoration在后面详细介绍):
- 用
getDecoratedLeft()
代替child.getLeft()获取子视图的 left 边缘。 - 用
getDecoratedTop()
代替getTop()获取子视图的 top 边缘。 - 用
getDecoratedRight()
代替getRight()获取子视图的 right 边缘。 - 用
getDecoratedBottom()
代替getBottom()获取子视图的 bottom 边缘。 - 使用
measureChild()
或measureChildWithMargins()
代替child.measure() 测量来自 Recycler 的新视图。 - 使用
layoutDecorated()
代替 child.layout() 布局来自 Recycler 的新视图。 - 使用
getDecoratedMeasuredWidth()
或getDecoratedMeasuredHeight()
代替 child.getMeasuredWidth()或 child.getMeasuredHeight()获取 子视图的测量数据
3.3.1 数据集改变
当使用 notifyDataSetChanged()
触发 RecyclerView.Adapter 的更新操作时, LayoutManager 负责更新布局中的视图。这时,onLayoutChildren()
会被再次调用。实现这个功能需要我们在onLayoutChildre()
方法中判断出当前状态是生成一个新的视图还是adapter更新期间视图改变。
。。。。。
3.3.2 onAdapterChanged()
这个方法提供了另一个重置布局的场所。设置新的 adapter 会触发这个事件。
示例:
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
//Completely scrap the existing layout
removeAllViews();
}
移除视图会触发一个新的布局过程,当 onLayoutChildren()
被再次调用时, 我们的代码会执行创建新视图的布局过程,因为现在没有 attched 的子视图。
3.3.3 Scroll to Position
其一个重要的特性就是给LayoutManager添加滚动到特定位置的功能。可以带有动画效果,也可以没有
-
scrollToPosition()
layout将当前位置设为第一个可见Item时,调用
RecyclerView的scrollToPosition()
。
示例:
@Override
public void scrollToPosition(int position) {
if (position >= getItemCount()) {
Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
return;
}
//Ignore current scroll offset, snap to top-left
mForceClearOffsets = true;
//Set requested position as first visible
mFirstVisiblePosition = position;
//Trigger a new view layout
requestLayout();
}
smoothScrollToPosition()
在带有动画的情况下,需要在该方法中创建一个RecyclerView.SmoothScroller
实例,在方法返回前请求startSmoothScroll()
启动动画
RecyclerView.SmoothScroller
是提供 API 的抽象类,含有四个方法:
-
onStart()
当滑动动画开始时被触发
-
onStop()
当滑动动画停止时被触发
-
onSeekTargetStep()
当 scroller 搜索目标 view 时被重复调用,这个方法负责读取提供的 dx/dy ,然后更新应该在这两个方向移动的距离。
-
onTragetFound()
只在目标视图被 attach 后调用一次。 这是将目标视图要通过动画移动到准确位置最后的场所。
4. ItemDecoration
RecyclerView并没有支持divider这样的属性,我们可以自己定制分割线。
RecyclerView添加分割线的方法是:mRecyclerView.addItemDecoration()
,该方法的参数为RecyclerView.ItemDecoration
,该类为抽象类
查看一下其源码:
public abstract static class ItemDecoration {
public ItemDecoration() {
}
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
this.onDraw(c, parent);
}
/** @deprecated */
@Deprecated
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
}
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
this.onDrawOver(c, parent);
}
/** @deprecated */
@Deprecated
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
}
/** @deprecated */
@Deprecated
public void getItemOffsets(@NonNull Rect outRect, int itemPosition, @NonNull RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
this.getItemOffsets(outRect, ((RecyclerView.LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent);
}
}
调用addItemDecoration()
方法添加decoration的时候,会调用该类的onDraw()
和onDrawOver()
方法
-
onDraw
方法先于drawChildren -
onDrawOver
在drawChildren之后,一般我们选择复写其中一个即可。 -
getItemOffsets
为每一个item设置一定的偏移量,主要用于绘制Decorator,设置分割线宽、高等
简单示例:(这里LayoutManager为LinearLayoutManager)
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider
};
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
private Drawable mDivider;
private int mOrientation;
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("invalid orientation");
}
mOrientation = orientation;
}
@Override
public void onDraw(Canvas c, RecyclerView parent) {
if (mOrientation == VERTICAL_LIST) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
public void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
android.support.v7.widget.RecyclerView v = new android.support.v7.widget.RecyclerView(parent.getContext());
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
public void drawHorizontal(Canvas c, RecyclerView parent) {
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}
该实现类可以看到通过读取系统主题中的 android.R.attr.listDivider
作为Item间的分割线,并且支持横向和纵向
然后在原来的代码中添加一句:
mRecyclerView.addItemDecoration(new DividerItemDecoration(this,
DividerItemDecoration.VERTICAL_LIST));
4.1 ItemDecoration解析
-
前述代码分割线是系统默认,你可以在theme.xml中找到该属性的使用情况,使用系统的listDivider可以方便我们随意改变,其属性声明:
当然可以自己写个Drawable,例如:
-
要使用ItemDecoration,必须先自定义,继承ItemDecoration,重写
getItemOffsets()
和onDraw()
这里先解释ItemDecoration含义。
ItemDecoration即对Item起装饰作用。
getItemOffsets()
就是设置item周边的偏移量(也就是装饰区域的宽度),而onDraw()
才是真正实现装饰的回调方法,该方法可以在装饰区域任意画画。前面ItemDecoration源码中可以看出,其主要有三个方法:
OnDraw() onDrawOver() getItemOffsets()
这三个方法的作用可以简单理解如下图
其中绿色表示内容,红色表示装饰:
- 图1表示
getItemOffsets()
,实现类似padding效果 - 图2表示
onDraw()
,实现类似绘制背景效果,内容在上 - 图3表示
onDrawOver()
,可以绘制在内容上面,覆盖内容
假设我们要实现线性列表的分割线:
- 当线性列表是水平方向时,分割线竖直的;当线性列表是竖直方向时,分割线是水平的
- 当画竖直分割线时,需要在item的右边偏移出一条线的宽度;当画水平分割线时,需要在item的下边偏移出一条线的高度
/** * 画线 */ @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); if (orientation == RecyclerView.HORIZONTAL) { drawVertical(c, parent, state); } else if (orientation == RecyclerView.VERTICAL) { drawHorizontal(c, parent, state); } } /** * 设置条目周边的偏移量 */ @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); if (orientation == RecyclerView.HORIZONTAL) { //画垂直线 outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); } else if (orientation == RecyclerView.VERTICAL) { //画水平线 outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); } }
getItemOffsets()
是相对每个item而言的,即每个item都会偏移出相同的装饰区域,而onDraw
则不同,它是相对Canvas来说的,通俗的说就是要自己找到要画的线的位置/** * 在构造方法中加载系统自带的分割线(就是ListView用的那个分割线) */ public MyDecorationOne(Context context, int orientation) { this.orientation = orientation; int[] attrs = new int[]{android.R.attr.listDivider}; TypedArray a = context.obtainStyledAttributes(attrs); mDivider = a.getDrawable(0); a.recycle(); } /** * 画竖直分割线 */ private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state) { int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); int left = child.getRight() + params.rightMargin; int top = child.getTop() - params.topMargin; int right = left + mDivider.getIntrinsicWidth(); int bottom = child.getBottom() + params.bottomMargin; mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } /** * 画水平分割线 */ private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state) { int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); int left = child.getLeft() - params.leftMargin; int top = child.getBottom() + params.bottomMargin; int right = child.getRight() + params.rightMargin; int bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } }
- 图1表示
简单示例ItemDecoration的三个主要方法:点击查看原文阅读
下面是没有添加任何ItemDecoration的界面:
-
预计实现效果如下时:
从效果图看实现了分割线效果。根据前述知识,只需先用
getItemOffset()
方法在Item下方空出一定的高度控件,然后用onDraw()
绘制这个空间即可public class SimpleDividerDecoration extends RecyclerView.ItemDecoration { private int dividerHeight; private Paint dividerPaint; public SimpleDividerDecoration(Context context) { dividerPaint = new Paint(); dividerPaint.setColor(context.getResources().getColor(R.color.colorAccent)); dividerHeight = context.getResources().getDimensionPixelSize(R.dimen.divider_height); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); outRect.bottom = dividerHeight; //item下方空出一块空间 } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { int childCount = parent.getChildCount(); int left = parent.getPaddingLeft(); int right = parent.getWidth() - parent.getPaddingRight(); //遍历item for (int i = 0; i < childCount - 1; i++) { View view = parent.getChildAt(i); float top = view.getBottom(); float bottom = view.getBottom() + dividerHeight; c.drawRect(left, top, right, bottom, dividerPaint); } } }
-
预计实现效果如下图时:
这种效果有点像商品标签,覆盖在内容之上可以通过
onDrawOver()
实现public class LeftAndRightTagDecoration extends RecyclerView.ItemDecoration { private int tagWidth; private Paint leftPaint; private Paint rightPaint; public LeftAndRightTagDecoration(Context context) { leftPaint = new Paint(); leftPaint.setColor(context.getResources().getColor(R.color.colorAccent)); rightPaint = new Paint(); rightPaint.setColor(context.getResources().getColor(R.color.colorPrimary)); tagWidth = context.getResources().getDimensionPixelSize(R.dimen.tag_width); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); int pos = parent.getChildAdapterPosition(child); boolean isLeft = pos % 2 == 0; if (isLeft) { float left = child.getLeft(); float right = left + tagWidth; float top = child.getTop(); float bottom = child.getBottom(); c.drawRect(left, top, right, bottom, leftPaint); } else { float right = child.getRight(); float left = right - tagWidth; float top = child.getTop(); float bottom = child.getBottom(); c.drawRect(left, top, right, bottom, rightPaint); } } } }
ItemDecoration效果是可以叠加的
recyclerView.addItemDecoration(new LeftAndRightTagDecoration(this)); recyclerView.addItemDecoration(new SimpleDividerDecoration(this));
将上面两个ItemDecoration同时添加到RecyclerView时,效果如下:
-
预计实现效果如下图时:
效果有点类似手机通讯录分组,这种效果就是有些Item添加分割线,有些没有而已。
-
定义一个接口给Activity进行回调用来进行数据分组和获取首字母
public interface DecorationCallback { long getGroupId(int position); String getGroupFirstLine(int position); }
-
实现ItemDecoration
public class SectionDecoration extends RecyclerView.ItemDecoration { private static final String TAG = "SectionDecoration"; private DecorationCallback callback; //回调接口 private TextPaint textPaint; private Paint paint; private int topGap; private Paint.FontMetrics fontMetrics; //初始化 public SectionDecoration(Context context, DecorationCallback decorationCallback) { Resources res = context.getResources(); this.callback = decorationCallback; paint = new Paint(); paint.setColor(res.getColor(R.color.colorAccent)); textPaint = new TextPaint(); textPaint.setTypeface(Typeface.DEFAULT_BOLD); textPaint.setAntiAlias(true); textPaint.setTextSize(80); textPaint.setColor(Color.BLACK); textPaint.getFontMetrics(fontMetrics); textPaint.setTextAlign(Paint.Align.LEFT); fontMetrics = new Paint.FontMetrics(); topGap = res.getDimensionPixelSize(R.dimen.sectioned_top);//32dp } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); int pos = parent.getChildAdapterPosition(view); Log.i(TAG, "getItemOffsets:" + pos); long groupId = callback.getGroupId(pos); if (groupId < 0) return; if (pos == 0 || isFirstInGroup(pos)) {//同组的第一个才添加padding outRect.top = topGap; } else { outRect.top = 0; } } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); int left = parent.getPaddingLeft(); int right = parent.getWidth() - parent.getPaddingRight(); int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View view = parent.getChildAt(i); int position = parent.getChildAdapterPosition(view); long groupId = callback.getGroupId(position); if (groupId < 0) return; String textLine = callback.getGroupFirstLine(position).toUpperCase(); //判断是否需要绘制分割线 if (position == 0 || isFirstInGroup(position)) { float top = view.getTop() - topGap; float bottom = view.getTop(); c.drawRect(left, top, right, bottom, paint);//绘制红色矩形 c.drawText(textLine, left, bottom, textPaint);//绘制文本 } } } private boolean isFirstInGroup(int pos) { if (pos == 0) { return true; } else { long prevGroupId = callback.getGroupId(pos - 1); long groupId = callback.getGroupId(pos); return prevGroupId != groupId; } } public interface DecorationCallback { long getGroupId(int position); String getGroupFirstLine(int position); } }
-
在Activity中使用
recyclerView.addItemDecoration(new SectionDecoration(this, new SectionDecoration.DecorationCallback() { @Override public long getGroupId(int position) { return Character.toUpperCase(dataList.get(position).getName().charAt(0)); } @Override public String getGroupFirstLine(int position) { return dataList.get(position).getName().substring(0, 1).toUpperCase(); } }));
-
-
预计实现效果如下时:
[图片上传失败...(image-ca730d-1565686110808)]
这种效果叫做粘性头部。
Header不动则一定要绘制在Item内容之上,需要重写
onDrawOver()
,其他与前述代码一致public class PinnedSectionDecoration extends RecyclerView.ItemDecoration { private static final String TAG = "PinnedSectionDecoration"; private DecorationCallback callback; private TextPaint textPaint; private Paint paint; private int topGap; private Paint.FontMetrics fontMetrics; public PinnedSectionDecoration(Context context, DecorationCallback decorationCallback) { Resources res = context.getResources(); this.callback = decorationCallback; paint = new Paint(); paint.setColor(res.getColor(R.color.colorAccent)); textPaint = new TextPaint(); textPaint.setTypeface(Typeface.DEFAULT_BOLD); textPaint.setAntiAlias(true); textPaint.setTextSize(80); textPaint.setColor(Color.BLACK); textPaint.getFontMetrics(fontMetrics); textPaint.setTextAlign(Paint.Align.LEFT); fontMetrics = new Paint.FontMetrics(); topGap = res.getDimensionPixelSize(R.dimen.sectioned_top); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); int pos = parent.getChildAdapterPosition(view); long groupId = callback.getGroupId(pos); if (groupId < 0) return; if (pos == 0 || isFirstInGroup(pos)) { outRect.top = topGap; } else { outRect.top = 0; } } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); int itemCount = state.getItemCount(); int childCount = parent.getChildCount(); int left = parent.getPaddingLeft(); int right = parent.getWidth() - parent.getPaddingRight(); float lineHeight = textPaint.getTextSize() + fontMetrics.descent; long preGroupId, groupId = -1; for (int i = 0; i < childCount; i++) { View view = parent.getChildAt(i); int position = parent.getChildAdapterPosition(view); preGroupId = groupId; groupId = callback.getGroupId(position); if (groupId < 0 || groupId == preGroupId) continue; String textLine = callback.getGroupFirstLine(position).toUpperCase(); if (TextUtils.isEmpty(textLine)) continue; int viewBottom = view.getBottom(); float textY = Math.max(topGap, view.getTop()); if (position + 1 < itemCount) { //下一个和当前不一样移动当前 long nextGroupId = callback.getGroupId(position + 1); if (nextGroupId != groupId && viewBottom < textY ) {//组内最后一个view进入了header textY = viewBottom; } } c.drawRect(left, textY - topGap, right, textY, paint); c.drawText(textLine, left, textY, textPaint); } } }
5. Item Animator
6. Item监听
RecyclerView默认没有像ListView一样提供setOnItemClickListener()
接口,需要我们自己去改造实现。
6.1 常规实现
关于RecyclerView的Item点击事件常规做法就是在Adapter中增加OnItemClickListener接口和setOnItemClickListener接口,为每个Item添加点击监听。
代码示例:
- 自定义Adapter适配器,添加item click接口,设置点击事件
public class MyAdapter extends RecyclerView.Adapter {
private LayoutInflater mInflater;
private Context mContext;
private List mDatas;
private int[] imgIds;
private OnItemClickListener mOnItemClickListener;
public MyAdapter(Context context, List datas, int[] imgIds) {
this.mContext = context;
this.mDatas = datas;
this.imgIds = imgIds;
mInflater = LayoutInflater.from(context);
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = mInflater.inflate(R.layout.item_single_textview, parent, false);
MyViewHolder viewHolder = new MyViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
holder.textView.setText(mDatas.get(position));
holder.imageView.setBackgroundResource(imgIds[ position % imgIds.length]);
// item click
if (mOnItemClickListener != null) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mOnItemClickListener.onItemClick(holder.itemView, position);
}
});
// item long click
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
mOnItemClickListener.onItemLongClick(holder.itemView, position);
return true;
}
});
}
}
@Override
public int getItemCount() {
return mDatas.size();
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.mOnItemClickListener = listener;
}
public void addData(int pos) {
mDatas.add(pos, "Add one");
notifyItemInserted(pos);
}
public void deleteData(int pos) {
mDatas.remove(pos);
notifyItemRemoved(pos);
}
public interface OnItemClickListener {
void onItemClick(View view, int position);
void onItemLongClick(View view, int position);
}
}
class MyViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
TextView textView;
public MyViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.textView);
imageView = itemView.findViewById(R.id.imageView);
}
}
- 修改MainActivity
mAdapter = new MyAdapter(this, mDatas, imgIds);
recyclerView.setAdapter(mAdapter);
//设置RecyclerView的布局管理
LinearLayoutManager manager = new LinearLayoutManager(this,
LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(manager);
recyclerView.setItemAnimator(new DefaultItemAnimator());
mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
Toast.makeText(MainActivity.this, "clicked " + position,
Toast.LENGTH_SHORT).show();
}
@Override
public void onItemLongClick(View view, int position) {
Toast.makeText(MainActivity.this, "long clicked " + position,
Toast.LENGTH_SHORT).show();
}
});
}