小甜点,RecyclerView 之 ItemDecoration 讲解及高级特性实践
2017-04-13 20:46
381 查看
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
毫无疑问,RecyclerView 是现在 Android 世界中最重要的系统组件之一,它的出现就是为了高效代替 ListView 和 GridView。当时它的出现解决了我一个大的需求,这个需求就是在电视盒子界面上横向加载应用列表,由于 ListView 没有横向加载的功能,而网络上开源的那些 HorizontalListView 又不满足需求,所以我们只能自定义 ViewGroup 来实现需求,但是回收机制不是很完善,所以性能并不好,所以当 RecyclerView 横空出世时,我第一时间拥抱了它,并推荐 Android 开发小组成员们去了解它。
但后来,我发现 RecyclerView 除了比 ListView 好用外,某些地方它却更复杂了,它将更多的权力交给了开发者自己,比如布局,比如 ITEM 的分割线,比如点击监听等等。但总归它是好东西,所以我们得多花些时间来学习,平常开发我们一般按照 RecyclerView 的基本用法便可以实现绝大多数需求,但是某些场景下却远远不够,比如我们不想局限于 LinearLayoutManager 想自己定义 LayoutManager,我们需要定义时光轴的效果,我们想实现美妙的添加删除动画等等,这些情况下解决问题的话需要我们对 RecyclerView 本身有足够的了解。
今天,这篇文章不讲 RecyclerView 基本的知识和用法,讲它一个有趣的知识点 ItemDecoration。
我们在使用 ListView 的时候只要在 xml 文件中,使用
我们新建一个工程,然后在一个页面里面添加一个 RecyclerView。创建相关的 Adapter,加载布局文件,这里布局文件很简单,就是一个 TextView,再之后在 Activity 初始化它。
可以看到所有的选项都混在一起,为了美观应该需要 1 px 的分割线,之前我一般在 Item 的布局文件中设置它的 topMargin 或者是 bottomMargin,所以我们可以在相关的 Adapter 中这样修改。
效果如下:
现在我们同样可以通过给 RecyclerView 添加 ItemDecoration 来实现它。
首先,我们需要自定义一个 ItemDecoration,按照目前的需求,我们只需要实现它的一个方法就可以了。
然后在 Activity 中添加它到 RecyclerView 就可以了。
效果如图:
Rect outRect
View view
RecyclerView parent
RecyclerView.State state
这四个参数分别干什么的呢?我们不妨在 AndroidStudio 中按 Ctrl 键点击方法名,就可以到了它被调用的位置。
我们注意这一行代码
我们可以看下面的这张图。
绿色区域代表 RecyclerView 中的一个 ItemView,而外面橙色区域也就是相应的 outRect,也就是 ItemView 与其它组件的偏移区域,等同于 margin 属性,通过复写 getItemOffsets() 方法,然后指定 outRect 中的 top、left、right、bottom 就可以控制各个方向的间隔了。注意的是这些属性都是偏移量,是指偏移 ItemView 各个方向的数值。在上面的例子中我设置了
可以看到它传递了一个 Canvas 参数对象,所以它拥有了绘制的能力。但是怎么绘制呢?
其实它是配合了前面的 getItemOffsets 方法一起使用的,getItemOffsets 撑开了 ItemView 的上下左右间隔区域,而 onDraw 方法通过计算每个 ItemView 的坐标位置与它的 outRect 值来确定它要绘制内容的区间。
假设,我们要设计一个高度为 2 px 的红色分割线,那么我们就需要在每个 ItemView top位置上方画一个 2 px 高度的矩形,然后填充颜色为红色。
需要注意的一点是 getItemOffsets 是针对每一个 ItemView,而 onDraw 方法却是针对 RecyclerView 本身,所以在 onDraw 方法中需要遍历屏幕上可见的 ItemView,分别获取它们的位置信息,然后分别的绘制对应的分割线。
我们看下面的这张示意图
为了便于观察我将第一条分割线的颜色透明化了,我们可以看到每条分割线绘制的区域其实就是 outRect.top 至 ItemView.top 之间的区域,所以我们就需要在当初 getOffsets 方法进行位置偏移时就记录下每个 itemView 向上的间隔距离,之后的逻辑就是遍历屏幕上的 View,然后描绘分割线。
然后我们在 Activity 将 ColorDividerItemDecoration 添加到相应的 RecyclerView 中就可以了。
效果如下图:
至此,红色的分割线就搞定了。
但一定要注意的是,onDraw 方法可不只能绘制简单的线条,它可是拥有 Canvas 的,所以画圆、画矩形、画弧形、绘制图片都不在话下。为了提高本篇代码的技术含量,下面我们通过 ItemDecoration 来实现一个时光轴的效果。
我们可以看到左边白色的图案就大概是我们时光轴要绘制的图形。我们通过 getItemOffsets 方法来对 ItemView 进行 left 和 top 的间距设置。然后确定好轴线的起始坐标,中间轴结点的图形或者是图案。我们可以通过 ItemView 将相应的时光轴片断分解,如下图。
主要是一些参数的确定,例如 DividerHeight,注意这个 DividerHeight 不是指 ItemView 向上的间隔值,而是相应的 ItemDecoration 的高度。中心坐标 (centerX,centerY),还有上下两段轴线的起始坐标。有了这些参数后,我们就能轻松地编码了。
然后效果如下图:
感觉不怎么美观,我们尝试将结点的实心圆改成空心圆。
效果如下:
感觉美观了许多。
同时,我们可以将轴结点用图标代替圆或者圆圈。
上面的图标感觉更好看了。
需要注意的是 onDraw 方法,ItemDecoration 是在 ItemView 的下方绘制的,也就是 ItemView 可能会覆盖 ItemDecoration 的内容。我们可以验证一下,在时间轴与 ItemView 的边界画一个完整的圆,观察它的效果。
可以看到,在重合的地方,圆圈确实被 ItemView 内容覆盖了。
大家可能会想到,ItemDecoration 内容能不能覆盖在 ItemView 内容之上呢?
答案是肯定的,但不是在 onDraw() 方法实现,而是另外一个方法 onDrawOver()。
或者这样。
这些角标都是绘制在 ItemView 之上的,现在有了 ItemDecoration 我们也可以轻松而优雅地实现它。
比如我们要实现一个图书销量排行榜。我们有大概的草图。
然后我们就可以编码了。
布局文件:
相应的 Adapter 代码:
自定义 FlagItemDecoration
然后在 Activity 进行相应的数据处理,这里的数据都是为了测试用的,所以比较随意。
最终效果如下图:
有人在想,通过 ItemView 中的布局文件不就可以完成这样的操作吗?是的,确实是可以的,将 Flag 角标定义在每一个 ItemView 布局文件中,然后在 Adapter 的 onBindViewHolder 方法中根据 postion 的值来决定是否加载角标。
但是这里是为了说明 ItemDecoration 中的 onDrawOver 方法,为了说明它确实能让 ItemDecoration 图像绘制在 ItemView 内容之上。事实上,ItemDecoration 的妙处还有好多好多。
* getItemOffsets 撑开 ItemView 上、下、左、右四个方向的空间
* onDraw 在 ItemView 内容之下绘制图形
* onDrawOver 在 ItemView 内容之上绘制图形。
好了,文章就到这里结束。
完整源码地址
github地址
更有意思的内容请看这篇《RecyclerView探索之通过ItemDecoration实现StickyHeader效果》
毫无疑问,RecyclerView 是现在 Android 世界中最重要的系统组件之一,它的出现就是为了高效代替 ListView 和 GridView。当时它的出现解决了我一个大的需求,这个需求就是在电视盒子界面上横向加载应用列表,由于 ListView 没有横向加载的功能,而网络上开源的那些 HorizontalListView 又不满足需求,所以我们只能自定义 ViewGroup 来实现需求,但是回收机制不是很完善,所以性能并不好,所以当 RecyclerView 横空出世时,我第一时间拥抱了它,并推荐 Android 开发小组成员们去了解它。
但后来,我发现 RecyclerView 除了比 ListView 好用外,某些地方它却更复杂了,它将更多的权力交给了开发者自己,比如布局,比如 ITEM 的分割线,比如点击监听等等。但总归它是好东西,所以我们得多花些时间来学习,平常开发我们一般按照 RecyclerView 的基本用法便可以实现绝大多数需求,但是某些场景下却远远不够,比如我们不想局限于 LinearLayoutManager 想自己定义 LayoutManager,我们需要定义时光轴的效果,我们想实现美妙的添加删除动画等等,这些情况下解决问题的话需要我们对 RecyclerView 本身有足够的了解。
今天,这篇文章不讲 RecyclerView 基本的知识和用法,讲它一个有趣的知识点 ItemDecoration。
ItemDecoration
Decoration 的英文意思是装饰物的意思,引申到这里来,肯定也是与 RecyclerView 的界面装饰有关。我们常见的就是分割线了。我们在使用 ListView 的时候只要在 xml 文件中,使用
android:divider就可以,但是很遗憾 RecyclerView 却没有相应的控制。
我们新建一个工程,然后在一个页面里面添加一个 RecyclerView。创建相关的 Adapter,加载布局文件,这里布局文件很简单,就是一个 TextView,再之后在 Activity 初始化它。
public class DividerActivity extends AppCompatActivity { RecyclerView mRecyclerView; List<String> data; TestAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_divider); mRecyclerView = (RecyclerView) findViewById(R.id.divider_recyclerview); initDatas(); mAdapter = new TestAdapter(data); mRecyclerView.setAdapter(mAdapter); LinearLayoutManager layoutmanager = new LinearLayoutManager(this); layoutmanager.setOrientation(LinearLayoutManager.VERTICAL); mRecyclerView.setLayoutManager(layoutmanager); } private void initDatas() { data = new ArrayList<>(); for (int i = 0; i < 56;i++) { data.add(i+" test "); } } }
可以看到所有的选项都混在一起,为了美观应该需要 1 px 的分割线,之前我一般在 Item 的布局文件中设置它的 topMargin 或者是 bottomMargin,所以我们可以在相关的 Adapter 中这样修改。
public TestHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item,parent,false); RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams(); layoutParams.topMargin = 1; view.setLayoutParams(layoutParams); TestHolder holder = new TestHolder(view); return holder; }
效果如下:
现在我们同样可以通过给 RecyclerView 添加 ItemDecoration 来实现它。
首先,我们需要自定义一个 ItemDecoration,按照目前的需求,我们只需要实现它的一个方法就可以了。
public class TestDividerItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); // //如果不是第一个,则设置top的值。 if (parent.getChildAdapterPosition(view) != 0){ //这里直接硬编码为1px outRect.top = 1; } } }
然后在 Activity 中添加它到 RecyclerView 就可以了。
mRecyclerView = (RecyclerView) findViewById(R.id.divider_recyclerview); initDatas(); mAdapter = new TestAdapter(data); mRecyclerView.setAdapter(mAdapter); LinearLayoutManager layoutmanager = new LinearLayoutManager(this); layoutmanager.setOrientation(LinearLayoutManager.VERTICAL); mRecyclerView.setLayoutManager(layoutmanager); mRecyclerView.addItemDecoration(new TestDividerItemDecoration());
效果如图:
getItemOffsets()
我们可以看到自定义的 TestDividerItemDeoration 只实现了一个方法 getItemOffsets()。方法里面有四个参数。Rect outRect
View view
RecyclerView parent
RecyclerView.State state
这四个参数分别干什么的呢?我们不妨在 AndroidStudio 中按 Ctrl 键点击方法名,就可以到了它被调用的位置。
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); //在这里被调用 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; return insets; }
我们注意这一行代码
java mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);很容易得知,outRect 是一个全为 0 的 Rect。view 指 RecyclerView 中的 Item。parent 就是 RecyclerView 本身,state 就是一个状态。
我们可以看下面的这张图。
绿色区域代表 RecyclerView 中的一个 ItemView,而外面橙色区域也就是相应的 outRect,也就是 ItemView 与其它组件的偏移区域,等同于 margin 属性,通过复写 getItemOffsets() 方法,然后指定 outRect 中的 top、left、right、bottom 就可以控制各个方向的间隔了。注意的是这些属性都是偏移量,是指偏移 ItemView 各个方向的数值。在上面的例子中我设置了
outRect.top = 1;所以每个 ItemView 之间有 1 px 的空隙,而这 1 px 空隙透露了下面背景色,所以看起来就像是分隔线,这实现了简单的分隔线效果,但这种方法分隔线的效果只能取决于背景色,如果我要定制分割线的颜色呢?这个时候就要讲到一个新的方法名 onDraw()。
onDraw()
在 Android 中的每一个 View 中 onDraw() 是很重要的一个方法,用来绘制组件的UI效果,所以在 ItemDecocration 中它自然也是用来绘制外观的。我们来看它的方法声明。java public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state);
可以看到它传递了一个 Canvas 参数对象,所以它拥有了绘制的能力。但是怎么绘制呢?
其实它是配合了前面的 getItemOffsets 方法一起使用的,getItemOffsets 撑开了 ItemView 的上下左右间隔区域,而 onDraw 方法通过计算每个 ItemView 的坐标位置与它的 outRect 值来确定它要绘制内容的区间。
假设,我们要设计一个高度为 2 px 的红色分割线,那么我们就需要在每个 ItemView top位置上方画一个 2 px 高度的矩形,然后填充颜色为红色。
需要注意的一点是 getItemOffsets 是针对每一个 ItemView,而 onDraw 方法却是针对 RecyclerView 本身,所以在 onDraw 方法中需要遍历屏幕上可见的 ItemView,分别获取它们的位置信息,然后分别的绘制对应的分割线。
我们看下面的这张示意图
为了便于观察我将第一条分割线的颜色透明化了,我们可以看到每条分割线绘制的区域其实就是 outRect.top 至 ItemView.top 之间的区域,所以我们就需要在当初 getOffsets 方法进行位置偏移时就记录下每个 itemView 向上的间隔距离,之后的逻辑就是遍历屏幕上的 View,然后描绘分割线。
public class ColorDividerItemDecoration extends RecyclerView.ItemDecoration { private float mDividerHeight; private Paint mPaint; public ColorDividerItemDecoration() { mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(Color.RED); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); // //第一个ItemView不需要在上面绘制分割线 if (parent.getChildAdapterPosition(view) != 0){ //这里直接硬编码为1px outRect.top = 1; mDividerHeight = 1; } } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); int childCount = parent.getChildCount(); for ( int i = 0; i < childCount; i++ ) { View view = parent.getChildAt(i); int index = parent.getChildAdapterPosition(view); //第一个ItemView不需要绘制 if ( index == 0 ) { continue; } float dividerTop = view.getTop() - mDividerHeight; float dividerLeft = parent.getPaddingLeft(); float dividerBottom = view.getTop(); float dividerRight = parent.getWidth() - parent.getPaddingRight(); c.drawRect(dividerLeft,dividerTop,dividerRight,dividerBottom,mPaint); } } }
然后我们在 Activity 将 ColorDividerItemDecoration 添加到相应的 RecyclerView 中就可以了。
mRecyclerView.addItemDecoration(new ColorDividerItemDecoration());
效果如下图:
至此,红色的分割线就搞定了。
但一定要注意的是,onDraw 方法可不只能绘制简单的线条,它可是拥有 Canvas 的,所以画圆、画矩形、画弧形、绘制图片都不在话下。为了提高本篇代码的技术含量,下面我们通过 ItemDecoration 来实现一个时光轴的效果。
通过 ItemDecoration 实现时光轴的效果
编码的开始先做设计,或者说先思考。思考我们要做什么,或者说要怎么做。我们可以看到左边白色的图案就大概是我们时光轴要绘制的图形。我们通过 getItemOffsets 方法来对 ItemView 进行 left 和 top 的间距设置。然后确定好轴线的起始坐标,中间轴结点的图形或者是图案。我们可以通过 ItemView 将相应的时光轴片断分解,如下图。
主要是一些参数的确定,例如 DividerHeight,注意这个 DividerHeight 不是指 ItemView 向上的间隔值,而是相应的 ItemDecoration 的高度。中心坐标 (centerX,centerY),还有上下两段轴线的起始坐标。有了这些参数后,我们就能轻松地编码了。
public class TimelineItemDecoration extends RecyclerView.ItemDecoration { private Paint mPaint; //ItemView左边的间距 private float mOffsetLeft; //ItemView右边的间距 private float mOffsetTop; //时间轴结点的半径 private float mNodeRadius; public TimelineItemDecoration(Context context) { mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(Color.RED); mOffsetLeft = context.getResources().getDimension(R.dimen.timeline_item_offset_left); mNodeRadius = context.getResources().getDimension(R.dimen.timeline_item_node_radius); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); // //第一个ItemView不需要在上面绘制分割线 if (parent.getChildAdapterPosition(view) != 0){ //这里直接硬编码为1px outRect.top = 1; mOffsetTop = 1; } outRect.left = (int) mOffsetLeft; } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); int childCount = parent.getChildCount(); for ( int i = 0; i < childCount; i++ ) { View view = parent.getChildAt(i); int index = parent.getChildAdapterPosition(view); float dividerTop = view.getTop() - mOffsetTop; //第一个ItemView 没有向上方向的间隔 if ( index == 0 ) { dividerTop = view.getTop(); } float dividerLeft = parent.getPaddingLeft(); float dividerBottom = view.getBottom(); float dividerRight = parent.getWidth() - parent.getPaddingRight(); float centerX = dividerLeft + mOffsetLeft / 2; float centerY = dividerTop + (dividerBottom - dividerTop) / 2; float upLineTopX = centerX; float upLineTopY = dividerTop; float upLineBottomX = centerX; float upLineBottomY = centerY - mNodeRadius; //绘制上半部轴线 c.drawLine(upLineTopX,upLineTopY,upLineBottomX,upLineBottomY,mPaint); //绘制时间轴结点 c.drawCircle(centerX,centerY,mNodeRadius,mPaint); float downLineTopX = centerX; float downLineTopY = centerY + mNodeRadius; float downLineBottomX = centerX; float downLineBottomY = dividerBottom; //绘制上半部轴线 c.drawLine(downLineTopX,downLineTopY,downLineBottomX,downLineBottomY,mPaint); } } }
然后效果如下图:
感觉不怎么美观,我们尝试将结点的实心圆改成空心圆。
//绘制时间轴结点 mPaint.setStyle(Paint.Style.STROKE); c.drawCircle(centerX,centerY,mNodeRadius,mPaint); mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
效果如下:
感觉美观了许多。
同时,我们可以将轴结点用图标代替圆或者圆圈。
上面的图标感觉更好看了。
需要注意的是 onDraw 方法,ItemDecoration 是在 ItemView 的下方绘制的,也就是 ItemView 可能会覆盖 ItemDecoration 的内容。我们可以验证一下,在时间轴与 ItemView 的边界画一个完整的圆,观察它的效果。
mPaint.setStyle(Paint.Style.STROKE); c.drawCircle(view.getLeft(),centerY,mNodeRadius,mPaint); mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
可以看到,在重合的地方,圆圈确实被 ItemView 内容覆盖了。
大家可能会想到,ItemDecoration 内容能不能覆盖在 ItemView 内容之上呢?
答案是肯定的,但不是在 onDraw() 方法实现,而是另外一个方法 onDrawOver()。
onDrawOver 和角标。
现实中的APP或者网站经常有一些排行榜比如下面:或者这样。
这些角标都是绘制在 ItemView 之上的,现在有了 ItemDecoration 我们也可以轻松而优雅地实现它。
比如我们要实现一个图书销量排行榜。我们有大概的草图。
然后我们就可以编码了。
布局文件:
<?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="wrap_content" android:background="@android:color/white"> <TextView android:id="@+id/tv_rank_oder" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:layout_marginLeft="24dp" android:gravity="center" android:textColor="#6c6c6c"/> <ImageView android:id="@+id/iv_cover" android:layout_width="80dp" android:layout_height="80dp" android:layout_centerVertical="true" android:layout_marginLeft="60dp"/> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/iv_cover" android:layout_marginLeft="12dp" android:layout_marginTop="12dp" android:textColor="@android:color/black"/> <TextView android:id="@+id/tv_price" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/iv_cover" android:layout_below="@id/tv_title" android:layout_marginLeft="12dp" android:layout_marginTop="12dp" android:textColor="@android:color/holo_red_dark"/> </RelativeLayout>
相应的 Adapter 代码:
public class BookRankAdapter extends RecyclerView.Adapter<BookRankAdapter.TestHolder> { List<String> data; int[] mIconResouces; public BookRankAdapter(List<String> data,int[] ids) { this.data = data; this.mIconResouces = ids; } public void setData(List<String> data,int[] ids) { this.data = data; mIconResouces = ids; notifyDataSetChanged(); } @Override public TestHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_ranklist_item,parent,false); TestHolder holder = new TestHolder(view); return holder; } @Override public void onBindViewHolder(TestHolder holder, int position) { if (data != null && data.size() > 0 ) { String text = data.get(position); String[] infos = text.split("-"); holder.tvOrder.setText(position+""); holder.tvTitle.setText(infos[0]); holder.tvPrice.setText(infos[1]); holder.ivCover.setImageResource(mIconResouces[position]); } } @Override public int getItemCount() { return data == null ? 0 : data.size(); } static class TestHolder extends RecyclerView.ViewHolder{ public TextView tvOrder; public TextView tvTitle; public TextView tvPrice; public ImageView ivCover; public TestHolder(View itemView) { super(itemView); tvOrder = (TextView) itemView.findViewById(R.id.tv_rank_oder); tvTitle = (TextView) itemView.findViewById(R.id.tv_title); tvPrice = (TextView) itemView.findViewById(R.id.tv_price); ivCover = (ImageView) itemView.findViewById(R.id.iv_cover); } } }
自定义 FlagItemDecoration
public class FlagItemDecoration extends RecyclerView.ItemDecoration { private Paint mPaint; private Bitmap mIcon; private float mFlagLeft; public FlagItemDecoration(Context context) { mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(Color.RED); mIcon = BitmapFactory.decodeResource(context.getResources(),R.drawable.hotsale); mFlagLeft = context.getResources().getDimension(R.dimen.flag_left); } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { // super.getItemOffsets(outRect, view, parent, state); //第一个ItemView不需要在上面绘制分割线 if (parent.getChildAdapterPosition(view) == 0){ outRect.top = 0; } else { outRect.top = 2; } } @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 view = parent.getChildAt(i); int index = parent.getChildAdapterPosition(view); float top = view.getTop(); if ( index < 3 ) { c.drawBitmap(mIcon,mFlagLeft,top,mPaint); } } } }
然后在 Activity 进行相应的数据处理,这里的数据都是为了测试用的,所以比较随意。
public class BookRankActivity extends AppCompatActivity { RecyclerView mRecyclerView; List<String> data; BookRankAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_bookrank); mRecyclerView = (RecyclerView) findViewById(R.id.bookrank_recyclerview); initDatas(); int resouces[] = new int[] {R.drawable.book_renmin,R.drawable.book_huochetou, R.drawable.book_jieyouzahuodian,R.drawable.book_tensoflow,R.drawable.book_wangyangming ,R.drawable.book_renmin,R.drawable.book_huochetou, R.drawable.book_jieyouzahuodian,R.drawable.book_tensoflow,R.drawable.book_wangyangming ,R.drawable.book_renmin,R.drawable.book_huochetou, R.drawable.book_jieyouzahuodian,R.drawable.book_tensoflow,R.drawable.book_wangyangming }; mAdapter = new BookRankAdapter(data,resouces); mRecyclerView.setAdapter(mAdapter); LinearLayoutManager layoutmanager = new LinearLayoutManager(this); layoutmanager.setOrientation(LinearLayoutManager.VERTICAL); mRecyclerView.setLayoutManager(layoutmanager); mRecyclerView.addItemDecoration(new FlagItemDecoration(this)); } private void initDatas() { data = new ArrayList<>(); data.add("人民的名义- ¥ 33.5"); data.add("火车头 - ¥ 27.5"); data.add("解忧杂货店- ¥ 19.9"); data.add("TensorFlow - ¥ 102.5"); data.add("王阳明心学 - ¥ 60"); data.add("人民的名义1- ¥ 33.5"); data.add("火车头1 - ¥ 27.5"); data.add("解忧杂货店1- ¥ 19.9"); data.add("TensorFlow1 - ¥ 102.5"); data.add("王阳明心学1 - ¥ 60"); data.add("人民的名义2 - ¥ 33.5"); data.add("火车头2 - ¥ 27.5"); data.add("解忧杂货店2- ¥ 19.9"); data.add("TensorFlow2 - ¥ 102.5"); data.add("王阳明心学2 - ¥ 60"); } }
最终效果如下图:
有人在想,通过 ItemView 中的布局文件不就可以完成这样的操作吗?是的,确实是可以的,将 Flag 角标定义在每一个 ItemView 布局文件中,然后在 Adapter 的 onBindViewHolder 方法中根据 postion 的值来决定是否加载角标。
但是这里是为了说明 ItemDecoration 中的 onDrawOver 方法,为了说明它确实能让 ItemDecoration 图像绘制在 ItemView 内容之上。事实上,ItemDecoration 的妙处还有好多好多。
总结
自定义一个 ItemDecoration 通常要根据需要,复写它的 3 个方法。* getItemOffsets 撑开 ItemView 上、下、左、右四个方向的空间
* onDraw 在 ItemView 内容之下绘制图形
* onDrawOver 在 ItemView 内容之上绘制图形。
提醒
由于文章篇幅,ItemDecoration 最让我兴奋的内容我需要另写一篇文章,那就是通过 ItemDecoration 自定义 RecyclerView 中的头部或者是粘性头部。相信大家对头部这个概念比较了解,现在通过 ItemDecoration 就可以优雅地实现它,记住优雅两个字,条条大路通罗马,但是有人就优雅、有人就显得手忙脚乱。所以,我将文章标题定了一个词,叫小甜点,吃了让人舒心,ItemDecoration 用了也让人舒心。好了,文章就到这里结束。
完整源码地址
github地址
更有意思的内容请看这篇《RecyclerView探索之通过ItemDecoration实现StickyHeader效果》
相关文章推荐
- 小甜点,RecyclerView 之 ItemDecoration 讲解及高级特性实践
- RecyclerView之ItemDecoration讲解及高级特性实践
- RecyclerView之ItemDecoration讲解及高级特性实践
- [置顶] RecyclerView高级应用——自定义ItemDecoration
- Material Design 实现之 RecyclerView高级特性
- 5.0新特性RecyclerView
- Android开发--RecyclerView使用,看AndroidL新特性
- Android TextView高级特性使用
- Android TextView高级特性使用
- 深入理解Java虚拟机:JVM高级特性与最佳实践
- JVM高级特性与最佳实践-虚拟机性能监控与故障处理
- Android开发--RecyclerView使用,看AndroidL新特性
- 深入理解Java虚拟机(JVM高级特性与最佳实践java虚拟机)的一些知识总结
- RecyclerView的高级用法——定制动画
- Android新特性之RecyclerView的简单使用
- JVM高级特性与最佳实践-JConsole的使用
- Android TextView高级特性使用
- 深入理解Java虚拟机:JVM高级特性与最佳实践
- 读书_JVM高级特性与最佳实践
- android 5.0新特性 RecyclerView使用初级