android getchild view,教你玩转 Android RecyclerView:深入解析 RecyclerView.ItemDecoration类(含实例讲解)...

本文详细解读了RecyclerView.ItemDecoration的使用,涵盖 getItemOffsets()、onDraw() 和 onDrawOver() 方法,以及如何结合自定义View实现时间轴效果。通过实例演示,学习如何添加分割线、角标和创建美观的时间线UI。

9a796bb23a47

前言

RecyclerView在 Android开发中非常常用,如果能结合ItemDecoration类使用,那么将大大提高RecyclerView的表现效果

本文全面解析了ItemDecoration类,包括ItemDecoration类简介、使用方法 & 实例讲解,最终结合 自定义View实现 时间轴UI开发,希望你们会喜欢。

ItemDecoration类属于RecyclerView的高级用法

目录

9a796bb23a47

目录

1. ItemDecoration类 简介

1.1 定义

RecyclerView类的静态内部类

1.2 作用

向 RecyclerView中的 ItemView 添加装饰

即绘制更多内容,丰富ItemView的UI效果

2. 具体使用

ItemDecoration类中仅有3个方法,具体如下:

public class TestDividerItemDecoration extends RecyclerView.ItemDecoration {

// 方法1:getItemOffsets()

// 作用:设置ItemView的内嵌偏移长度(inset)

@Override

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {

...

}

// 方法2:onDraw()

// 作用:在子视图上设置绘制范围,并绘制内容

// 类似平时自定义View时写onDraw()一样

// 绘制图层在ItemView以下,所以如果绘制区域与ItemView区域相重叠,会被遮挡

@Override

public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {

...

}

// 方法3:onDrawOver()

// 作用:同样是绘制内容,但与onDraw()的区别是:绘制在图层的最上层

@Override

public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {

...

}

下面,我将详细介绍这3个方法。

2.1 getItemOffsets()

2.1.1 作用

设置ItemView的内嵌偏移长度(inset)

如图,其实RecyclerView 中的 ItemView 外面会包裹着一个矩形(outRect)

内嵌偏移长度 是指:该矩形(outRect)与 ItemView的间隔

9a796bb23a47

示意图

内嵌偏移长度分为4个方向:上、下、左、右,并由outRect 中的 top、left、right、bottom参数 控制

top、left、right、bottom参数默认 = 0,即矩形和Item重叠,所以看起来矩形就消失了

9a796bb23a47

示意图

2.1.2 具体使用

@Override

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {

// 参数说明:

// 1. outRect:全为 0 的 Rect(包括着Item)

// 2. view:RecyclerView 中的 视图Item

// 3. parent:RecyclerView 本身

// 4. state:状态

outRect.set(50, 0, 0,50);

// 4个参数分别对应左(Left)、上(Top)、右(Right)、下(Bottom)

// 上述语句代表:左&下偏移长度=50px,右 & 上 偏移长度 = 0

...

}

9a796bb23a47

示意图

2.1.3 源码分析

RecyclerView本质上是一个自定义ViewGroup,子视图child = 每个ItemView

其通过 LayoutManager测量并布局 ItemView

public void measureChild(View child, int widthUsed, int heightUsed) {

// 参数说明:

// 1. child:要测量的子view(ItemView)

// 2. widthUsed: 一个ItemView的所有ItemDecoration占用的宽度(px)

// 3. heightUsed:一个ItemView的所有ItemDecoration占用的高度(px)

final LayoutParams lp = (LayoutParams) child.getLayoutParams();

final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);

// 累加当前ItemDecoration 4个属性值->>分析1

widthUsed += insets.left + insets.right;

// 计算每个ItemView的所有ItemDecoration的宽度

heightUsed += insets.top + insets.bottom;

// 计算每个ItemView的所有ItemDecoration的高度

final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),

getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,

canScrollHorizontally());

// 测量child view(ItemView)的宽度

// 第三个参数设置 child view 的 padding,即ItemView的Padding

// 而该参数把 insets 的值算进去,所以insets 值影响了每个 ItemView 的 padding值

// 高度同上

final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),

getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,

canScrollVertically());

if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {

child.measure(widthSpec, heightSpec);

}

}

// 分析完毕,请跳出

Rect getItemDecorInsetsForChild(View child) {

final LayoutParams lp = (LayoutParams) child.getLayoutParams();

insets.set(0, 0, 0, 0);

for (int i = 0; i < decorCount; i++) {

mTempRect.set(0, 0, 0, 0);

// 获取getItemOffsets() 中设置的值

mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);

// 将getItemOffsets() 中设置的值添加到insets 变量中

insets.left += mTempRect.left;

insets.top += mTempRect.top;

insets.right += mTempRect.right;

insets.bottom += mTempRect.bottom;

}

// 最终返回

return insets;

}

// insets介绍

// 1. 作用:

// a. 把每个ItemView的所有 ItemDecoration 的 getItemOffsets 中设置的值累加起来,(每个ItemView可添加多个ItemDecoration)

// 即把每个ItemDecoration的left, top, right, bottom 4个属性分别累加

// b. 记录上述结果

// c. inset就像padding和margin一样,会影响view的尺寸和位置

// 2. 使用场景:设置View的边界大小,使得其大小>View的背景大小

// 如 按钮图标(View的背景)较小,但是我们希望按钮有较大的点击热区(View的边界大小)

// 返回到分析1进来的原处

总结

结论:outRect4个属性值影响着ItemView的Padding值

具体过程:在RecyclerView进行子View宽高测量时(measureChild()),会将getItemOffsets()里设置的 outRect4个属性值(Top、Bottom、Left、Right)通过insert值累加 ,并最终添加到子View的 Padding属性中

2.2 onDraw()

2.2.1 作用

通过 Canvas 对象绘制内容

2.2.2 具体使用

使用方法类似自定义View时的onDraw()

@Override

public void onDraw(Canvas c, RecyclerView parent,

RecyclerView.State state) {

....

// 使用类似自定义View时的 onDraw()

}

2.2.3 特别注意

注意点1:Itemdecoration的onDraw()绘制会先于ItemView的onDraw()绘制,所以如果在Itemdecoration的onDraw()中绘制的内容在ItemView边界内,就会被ItemView遮挡住。如下图:

此现象称为onDraw()的 OverDraw现象

9a796bb23a47

示意图

解决方案:配合前面的 getItemOffsets() 一起使用在outRect矩形 与 ItemView的间隔区域 绘制内容

即:通过getItemOffsets() 设置与 Item 的间隔区域,从而获得与ItemView不重叠的绘制区域

9a796bb23a47

示意图

注意点2: getItemOffsets() 针对是每一个 ItemView的,而 onDraw() 针对 RecyclerView 本身

解决方案:在 使用onDraw()绘制时,需要先遍历RecyclerView 的所有ItemView分别获取它们的位置信息,然后再绘制内容

此处遍历的RecyclerView的ItemView(即Child view),并不是 Adapter 设置的每一个 item,而是可见的 item

因为只有可见的Item 才是RecyclerView的 Child view

@Override

public void onDraw(Canvas c, RecyclerView parent,

RecyclerView.State state) {

// RecyclerView 的左边界加上 paddingLeft距离 后的坐标位置

final int left = parent.getPaddingLeft();

// RecyclerView 的右边界减去 paddingRight 后的坐标位置

final int right = parent.getWidth() - parent.getPaddingRight();

// 即左右边界就是 RecyclerView 的 ItemView区域

// 获取RecyclerView的Child view的个数

final int childCount = parent.getChildCount();

// 设置布局参数

final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

.getLayoutParams();

// 遍历每个RecyclerView的Child view

// 分别获取它们的位置信息,然后再绘制内容

for (int i = 0; i < childCount; i++) {

final View child = parent.getChildAt(i);

int index = parent.getChildAdapterPosition(view);

// 第一个Item不需要绘制

if ( index == 0 ) {

continue;

}

// ItemView的下边界:ItemView 的 bottom坐标 + 距离RecyclerView底部距离 +translationY

final int top = child.getBottom() + params.bottomMargin +

Math.round(ViewCompat.getTranslationY(child));

// 绘制分割线的下边界 = ItemView的下边界+分割线的高度

final int bottom = top + mDivider.getIntrinsicHeight();

mDivider.setBounds(left, top, right, bottom);

mDivider.draw(c);

}

}

}

2.2.4 应用场景

在丰富 ItemView 的显示效果,即在ItemView 的基础上绘制内容

如分割线等等

2.2.5 实例讲解

实例说明:在ItemView设计一个高度为 10 px 的红色分割线

思路

通过getItemOffsets()设置与 Item 的下间隔区域 = 10 px

设置好onDraw()可绘制的区域

通过onDraw()绘制一个高度 = 10px的矩形(填充颜色=红色)

9a796bb23a47

示意图

具体实现

步骤1:自定义ItemDecoration类

ItemDecoration.java

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

private Paint mPaint;

// 在构造函数里进行绘制的初始化,如画笔属性设置等

public DividerItemDecoration() {

mPaint = new Paint();

mPaint.setColor(Color.RED);

// 画笔颜色设置为红色

}

// 重写getItemOffsets()方法

// 作用:设置矩形OutRect 与 Item 的间隔区域

@Override

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {

super.getItemOffsets(outRect, view, parent, state);

int itemPosition = parent.getChildAdapterPosition(view);

// 获得每个Item的位置

// 第1个Item不绘制分割线

if (itemPosition != 0) {

outRect.set(0, 0, 0, 10);

// 设置间隔区域为10px,即onDraw()可绘制的区域为10px

}

}

// 重写onDraw()

// 作用:在间隔区域里绘制一个矩形,即分割线

@Override

public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {

super.onDraw(c, parent, state);

// 获取RecyclerView的Child view的个数

int childCount = parent.getChildCount();

// 遍历每个Item,分别获取它们的位置信息,然后再绘制对应的分割线

for ( int i = 0; i < childCount; i++ ) {

// 获取每个Item的位置

final View child = parent.getChildAt(i);

int index = parent.getChildAdapterPosition(child);

// 第1个Item不需要绘制

if ( index == 0 ) {

continue;

}

// 获取布局参数

final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

.getLayoutParams();

// 设置矩形(分割线)的宽度为10px

final int mDivider = 10;

// 根据子视图的位置 & 间隔区域,设置矩形(分割线)的2个顶点坐标(左上 & 右下)

// 矩形左上顶点 = (ItemView的左边界,ItemView的下边界)

// ItemView的左边界 = RecyclerView 的左边界 + paddingLeft距离 后的位置

final int left = parent.getPaddingLeft();

// ItemView的下边界:ItemView 的 bottom坐标 + 距离RecyclerView底部距离 +translationY

final int top = child.getBottom() + params.bottomMargin +

Math.round(ViewCompat.getTranslationY(child));

// 矩形右下顶点 = (ItemView的右边界,矩形的下边界)

// ItemView的右边界 = RecyclerView 的右边界减去 paddingRight 后的坐标位置

final int right = parent.getWidth() - parent.getPaddingRight();

// 绘制分割线的下边界 = ItemView的下边界+分割线的高度

final int bottom = top + mDivider;

// 通过Canvas绘制矩形(分割线)

c.drawRect(left,top,right,bottom,mPaint);

}

}

}

步骤2:在设置RecyclerView时添加该分割线即可

Rv = (RecyclerView) findViewById(R.id.my_recycler_view);

//使用线性布局

LinearLayoutManager layoutManager = new LinearLayoutManager(this);

Rv.setLayoutManager(layoutManager);

Rv.setHasFixedSize(true);

// 通过自定义分割线类 添加分割线

Rv.addItemDecoration(new DividerItemDecoration());

//为ListView绑定适配器

myAdapter = new MyAdapter(this,listItem);

Rv.setAdapter(myAdapter);

myAdapter.setOnItemClickListener(this);

2.2.6 结果展示

9a796bb23a47

示意图

2.2.7 源码地址

2.3 onDrawOver()

2.3.1 作用

与onDraw()类似,都是绘制内容

但与onDraw()的区别是:Itemdecoration的onDrawOver()绘制 是后于 ItemView的onDraw()绘制

即不需要考虑绘制内容被ItemView遮挡的问题,反而 ItemView会被onDrawOver()绘制的内容遮挡

绘制时机比较:

Itemdecoration.onDraw()> ItemView.onDraw() > Itemdecoration.onDrawOver()

9a796bb23a47

示意图

2.3.2 具体使用

使用方法类似自定义View时的onDraw()

@Override

public void onDrawOver(Canvas c, RecyclerView parent,

RecyclerView.State state) {

....

// 使用类似自定义View时的 onDraw()

}

2.3.3 应用场景

在 RecyclerView / 特定的 ItemView 上绘制内容,如蒙层、重叠内容等等

2.3.4 实例讲解

实例说明:在 RecyclerView 上每个 ItemView 上叠加一个角标

9a796bb23a47

角度示意图

具体代码实现

** 步骤1:自定义 ItemDecoration类**

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

private Paint mPaint;

private Bitmap mIcon;

// 在构造函数里进行绘制的初始化,如画笔属性设置等

public DividerItemDecoration(Context context) {

mPaint = new Paint();

mPaint.setColor(Color.RED);

// 画笔颜色设置为红色

// 获取图片资源

mIcon = BitmapFactory.decodeResource(context.getResources(), R.mipmap.logo);

}

// 重写onDrawOver()

// 将角度绘制到ItemView上

@Override

public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {

super.onDrawOver(c, parent, state);

// 获取Item的总数

int childCount = parent.getChildCount();

// 遍历Item

for ( int i = 0; i < childCount; i++ ) {

// 获取每个Item的位置

View view = parent.getChildAt(i);

int index = parent.getChildAdapterPosition(view);

// 设置绘制内容的坐标(ItemView的左边界,ItemView的上边界)

// ItemView的左边界 = RecyclerView 的左边界 = paddingLeft距离 后的位置

final int left = parent.getWidth()/2;

// ItemView的上边界

float top = view.getTop();

// 第1个ItemView不绘制

if ( index == 0 ) {

continue;

}

// 通过Canvas绘制角标

c.drawBitmap(mIcon,left,top,mPaint);

}

}

}

** 步骤2:在设置RecyclerView时添加即可 **

Rv = (RecyclerView) findViewById(R.id.my_recycler_view);

//使用线性布局

LinearLayoutManager layoutManager = new LinearLayoutManager(this);

Rv.setLayoutManager(layoutManager);

Rv.setHasFixedSize(true);

//用自定义分割线类设置分割线

Rv.addItemDecoration(new DividerItemDecoration());

//为ListView绑定适配器

myAdapter = new MyAdapter(this,listItem);

Rv.setAdapter(myAdapter);

myAdapter.setOnItemClickListener(this);

2.3.5 结果展示

9a796bb23a47

示意图

2.3.6 源码地址

3. 使用总结

我用一张图总结RecyclerView ItemDecoration类的使用

9a796bb23a47

示意图

4. 结合自定义View的实践应用:时间轴

Android开发中,时间轴的 UI需求非常常见,如下图:

9a796bb23a47

示意图

本次实例将结合 自定义View & RecyclerView的知识,手把手教你实现该常见 & 实用的自定义View:时间轴

下一篇文章我将继续结合 自定义View & RecyclerView.ItemDecoration类进行一些有趣的自定义View实例讲解,感兴趣的同学可以继续关注本人运营的Wechat Public Account:

请点赞!因为你的鼓励是我写作的最大动力!

不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度。

9a796bb23a47

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值