版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
【移动应用开发技术】怎么在Android中利用ItemDecoration实现一个分组索引列表
怎么在Android中利用ItemDecoration实现一个分组索引列表?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。分组GroupHeader分割线SideBar前两个部分涉及到一个ItemDecoration类,也是我们接下来的重点,该类是RecyclerView的一个抽象静态内部类,主要作用就是给RecyclerView的ItemView绘制额外的装饰效果,例如给RecyclerView添加分割线。使用ItemDecoration时需要继承该类,根据需求可以重写如下三个方法,其它的方法已经deprecated了:public
class
GroupHeaderItemDecoration
extends
RecyclerView.ItemDecoration
{
@Override
public
void
getItemOffsets(Rect
outRect,
View
view,
RecyclerView
parent,
RecyclerView.State
state)
{
super.getItemOffsets(outRect,
view,
parent,
state);
}
@Override
public
void
onDraw(Canvas
c,
RecyclerView
parent,
RecyclerView.State
state)
{
super.onDraw(c,
parent,
state);
}
@Override
public
void
onDrawOver(Canvas
c,
RecyclerView
parent,
RecyclerView.State
state)
{
super.onDrawOver(c,
parent,
state);
}
}然后将其添加到RecyclerView中:recyclerView.addItemDecoration(new
GroupHeaderItemDecoration())了解这个三个方法的作用,这样才能更好的实现我们想要的功能:1、getItemOffsets()给指定的ItemView设置偏移量,具体怎么设置呢,咱们看图说话:图中左边的是原始RecyclerView列表,右边是设置了ItemView偏移量的列表,其实相当于在ItemView外部添加了一个矩形区域其中left、top、right、bottom就是ItemView在四个方向的偏移量,对应的设置代码如下:outRect.set(left,
top,
right,
bottom)在我们的分组索引列表中,只需要对ItemView设置顶部的偏移量,其它三个偏移量为0即可。这样就可以在ItemView顶部预留出一定高度的区域,如下图:2、onDraw()在getItemOffsets()方法中,我们设置了偏移量,进而得到了对应的偏移区域,接下来在onDraw()中就可以给ItemView绘制装饰效果了,所以我们在该方法中将分组索引列表中的GroupHeader的内容绘制在ItemView顶部偏移区域里。也就是绘制前边gif图里的A、B、C...GroupHeader,虽然看起来像一个个独立的ItemView,但并不是的哦!注意该绘制操作会在ItemView的onDraw()前完成的!3、onDrawOver()该方法同样也是用来绘制的,但是它在ItemDecoration的onDraw()方法和ItemView的onDraw()完成后才执行。所以其绘制的内容会遮挡在RecyclerView上,因此我们可以在该方法中绘制分组索引列表中悬浮的GroupHeader,也就是在列表顶部随着列表滚动切换的GroupHeader。一、分组GroupHeader三个方法的作用已经解释完了,接下来就是代码实现我们的效果了:首先保证RecyclerView的数据源已经按照某种规律进行了分组排序,具体什么规律你说了算,我们例子中按照数据源中指定字段的值的首字母升序排列,也就是常见通讯录的排序方式。然后在每个data中保存需要在GroupHeader上显示的内容,可以使用tag字段,我们这里保存的是对应的首字母。这里没必要将整个数据源设置到ItemDecoration里边,所以我们只需要提取排序后数据源的tag保存到列表中,然后设置到ItemDecoration里边,后边的操作就依赖设置的数据源了,根据tag的异同来决定是否绘制GroupHeader等。上边已经分析了,GroupHeader只在列表中每组数据对应的第一个ItemView顶部显示,只需要对ItemView设置顶部的偏移量即可:public
class
GroupHeaderItemDecoration
extends
RecyclerView.ItemDecoration
{
@Override
public
void
getItemOffsets(Rect
outRect,
View
view,
RecyclerView
parent,
RecyclerView.State
state)
{
super.getItemOffsets(outRect,
view,
parent,
state);
RecyclerView.LayoutManager
manager
=
parent.getLayoutManager();
//只处理线性垂直类型的列表
if
((manager
instanceof
LinearLayoutManager)
&&
LinearLayoutManager.VERTICAL
!=
((LinearLayoutManager)
manager).getOrientation())
{
return;
}
int
position
=
parent.getChildAdapterPosition(view);
//ItemView的position==0
或者
当前ItemView的data的tag和上一个ItemView的不相等,则为当前ItemView设置top
偏移量
if
(!Utils.listIsEmpty(tags)
&&
(position
==
0
||
!tags.get(position).equals(tags.get(position
-
1))))
{
outRect.set(0,
groupHeaderHeight,
0,
0);
}
}
@Override
public
void
onDraw(Canvas
c,
RecyclerView
parent,
RecyclerView.State
state)
{
super.onDraw(c,
parent,
state);
}
@Override
public
void
onDrawOver(Canvas
c,
RecyclerView
parent,
RecyclerView.State
state)
{
super.onDrawOver(c,
parent,
state);
}
}其中tags就是我们设置到ItemDecoration的数据源,是一个String集合。groupHeaderHeight就是ItemView的顶部偏移量。之后就是在ItemView的顶部偏移区域绘制GroupHeader了:public
class
GroupHeaderItemDecoration
extends
RecyclerView.ItemDecoration
{
@Override
public
void
getItemOffsets(Rect
outRect,
View
view,
RecyclerView
parent,
RecyclerView.State
state)
{
super.getItemOffsets(outRect,
view,
parent,
state);
}
@Override
public
void
onDraw(Canvas
c,
RecyclerView
parent,
RecyclerView.State
state)
{
super.onDraw(c,
parent,
state);
for
(int
i
=
0;
i
<
parent.getChildCount();
i++)
{
View
view
=
parent.getChildAt(i);
int
position
=
parent.getChildAdapterPosition(view);
String
tag
=
tags.get(position);
//和getItemOffsets()里的条件判断类似,开始绘制分组的GroupHeader
if
(!Utils.listIsEmpty(tags)
&&
(position
==
0
||
!tag.equals(tags.get(position
-
1))))
{
drawGroupHeader(c,
parent,
view,
tag);
}
}
}
@Override
public
void
onDrawOver(Canvas
c,
RecyclerView
parent,
RecyclerView.State
state)
{
super.onDrawOver(c,
parent,
state);
}
private
void
drawGroupHeader(Canvas
c,
RecyclerView
parent,
View
view,
String
tag)
{
RecyclerView.LayoutParams
params
=
(RecyclerView.LayoutParams)
view.getLayoutParams();
int
left
=
parent.getPaddingLeft();
int
right
=
parent.getWidth()
-
parent.getPaddingRight();
int
bottom
=
view.getTop()
-
params.topMargin;
int
top
=
bottom
-
groupHeaderHeight;
c.drawRect(left,
top,
right,
bottom,
mPaint);
int
x
=
left
+
groupHeaderLeftPadding;
int
y
=
top
+
(groupHeaderHeight
+
Utils.getTextHeight(mTextPaint,
tag))
/
2;
c.drawText(tag,
x,
y,
mTextPaint);
}
}绘制GroupHeader就是Canvasc操作,先绘制一个矩形框,再绘制相应的文字,当然绘制图片也是没问题的,其中groupHeaderLeftPadding是个可配置字段,代表绘制的文字或图片到列表左边沿的距离,也可以理解为GroupHeader的左padding。最后就是悬浮在顶部的GroupHeader绘制了:public
class
GroupHeaderItemDecoration
extends
RecyclerView.ItemDecoration
{
@Override
public
void
getItemOffsets(Rect
outRect,
View
view,
RecyclerView
parent,
RecyclerView.State
state)
{
super.getItemOffsets(outRect,
view,
parent,
state);
}
@Override
public
void
onDraw(Canvas
c,
RecyclerView
parent,
RecyclerView.State
state)
{
super.onDraw(c,
parent,
state);
}
@Override
public
void
onDrawOver(Canvas
c,
RecyclerView
parent,
RecyclerView.State
state)
{
super.onDrawOver(c,
parent,
state);
if
(!show)
{
return;
}
//列表第一个可见的ItemView位置
int
position
=
((LinearLayoutManager)
(parent.getLayoutManager())).findFirstVisibleItemPosition();
String
tag
=
tags.get(position);
View
view
=
parent.findViewHolderForAdapterPosition(position).itemView;
//当前ItemView的data的tag和下一个itemView的不相等,则代表将要重新绘制悬停的GroupHeader
boolean
flag
=
false;
if
(!Utils.listIsEmpty(tags)
&&
(position
+
1)
<
tags.size()
&&
!tag.equals(tags.get(position
+
1)))
{
//如果第一个可见ItemView的底部坐标小于groupHeaderHeight,则执行Canvas向上位移操作
if
(view.getBottom()
<=
groupHeaderHeight)
{
c.save();
flag
=
true;
c.translate(0,
view.getHeight()
+
view.getTop()
-
groupHeaderHeight);
}
}
drawSuspensionGroupHeader(c,
parent,
tag);
if
(flag)
{
c.restore();
}
}
private
void
drawSuspensionGroupHeader(Canvas
c,
RecyclerView
parent,
String
tag)
{
int
left
=
parent.getPaddingLeft();
int
right
=
parent.getWidth()
-
parent.getPaddingRight();
int
bottom
=
groupHeaderHeight;
int
top
=
0;
c.drawRect(left,
top,
right,
bottom,
mPaint);
int
x
=
left
+
groupHeaderLeftPadding;
int
y
=
top
+
(groupHeaderHeight
+
Utils.getTextHeight(mTextPaint,
tag))
/
2;
c.drawText(tag,
x,
y,
mTextPaint);
}
}绘制操作和onDraw中的类似,gif中有一个悬浮GroupHeader上移的动画,就是通过Canvas位移来实现的,注意在Canvas位移的前后进行save()和restore()操作。我们给GroupHeaderItemDecoration提供了设置GroupHeader左padding、高度、背景色、文字颜色、尺寸、以及是否显示顶部悬浮GroupHeader的方法,方便使用。关于绘制操作需要注意的是,GroupHeader所在的偏移区域和ItemView是相互独立的,不要把GroupHeader当做ItemView的一部分哦。到这里GroupHeader的功能就实现了,只需要将GroupHeaderItemDecoration添加到RecyclerView即可。至于如何通过layout或者View来实现GroupHeader,做过一些尝试,效果都不理想,期待大家的好想法哦!这里先用一个接口,对外提供自定义绘制GroupHeader的方法:public
interface
OnDrawItemDecorationListener
{
/**
*
绘制GroupHeader
*
@param
c
*
@param
paint
绘制GroupHeader区域的paint
*
@param
textPaint
绘制文字的paint
*
@param
params
共四个值left、top、right、bottom
代表GroupHeader所在区域的四个坐标值
*
@param
position
原始数据源中的position
*/
void
onDrawGroupHeader(Canvas
c,
Paint
paint,
TextPaint
textPaint,
int[]
params,
int
position);
/**
*
绘制悬浮在列表顶部的GroupHeader
*/
void
onDrawSuspensionGroupHeader(Canvas
c,
Paint
paint,
TextPaint
textPaint,
int[]
params,
int
position);
}二、分割线现在RecyclerView还差一个分割线,当前最笨的办法可以在ItemView的布局文件中设置,既然系统都提供了ItemDecoration,那用它来优雅的实现为何不可呢,我们只需要给列表中每组数据除了最后一项数据对应的ItemView之外的添加分割线即可,也就是不给每组数据对应的最后一个ItemView添加分割线。很简单,直接上核心代码:public
class
DivideItemDecoration
extends
RecyclerView.ItemDecoration
{
@Override
public
void
getItemOffsets(Rect
outRect,
View
view,
RecyclerView
parent,
RecyclerView.State
state)
{
super.getItemOffsets(outRect,
view,
parent,
state);
RecyclerView.LayoutManager
manager
=
parent.getLayoutManager();
//只处理线性垂直类型的列表
if
((manager
instanceof
LinearLayoutManager)
&&
LinearLayoutManager.VERTICAL
!=
((LinearLayoutManager)
manager).getOrientation())
{
return;
}
int
position
=
parent.getChildAdapterPosition(view);
if
(!Utils.listIsEmpty(tags)
&&
(position
+
1)
<
tags.size()
&&
tags.get(position).equals(tags.get(position
+
1)))
{
//当前ItemView的data的tag和下一个ItemView的不相等,则为当前ItemView设置bottom
偏移量
outRect.set(0,
0,
0,
divideHeight);
}
}
@Override
public
void
onDraw(Canvas
c,
RecyclerView
parent,
RecyclerView.State
state)
{
super.onDraw(c,
parent,
state);
for
(int
i
=
0;
i
<
parent.getChildCount();
i++)
{
View
view
=
parent.getChildAt(i);
int
position
=
parent.getChildAdapterPosition(view);
//和getItemOffsets()里的条件判断类似
if
(!Utils.listIsEmpty(tags)
&&
(position
+
1)
<
tags.size()
&&
tags.get(position).equals(tags.get(position
+
1)))
{
drawDivide(c,
parent,
view);
}
}
}
@Override
public
void
onDrawOver(Canvas
c,
RecyclerView
parent,
RecyclerView.State
state)
{
super.onDrawOver(c,
parent,
state);
}
private
void
drawDivide(Canvas
c,
RecyclerView
parent,
View
view)
{
RecyclerView.LayoutParams
params
=
(RecyclerView.LayoutParams)
view.getLayoutParams();
int
left
=
parent.getPaddingLeft();
int
right
=
parent.getWidth();
int
top
=
view.getBottom()
+
params.bottomMargin;
int
bottom
=
top
+
divideHeight;
c.drawRect(left,
top,
right,
bottom,
mPaint);
}
}三、SideBarSideBar就是gif图右边的垂直字符条,是一个自定义View。手指触摸选中一个字符,则列表会滚动到对应的分组头部位置。实现起来也蛮简单的,核心代码如下:public
class
SideBar
extends
View
{
@Override
protected
void
onMeasure(int
widthMeasureSpec,
int
heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec,
heightMeasureSpec);
int
widthSize
=
MeasureSpec.getSize(widthMeasureSpec);
int
widthMode
=
MeasureSpec.getMode(widthMeasureSpec);
int
heightSize
=
MeasureSpec.getSize(heightMeasureSpec);
int
heightMode
=
MeasureSpec.getMode(heightMeasureSpec);
//重新计算SideBar宽高
if
(heightMode
==
MeasureSpec.AT_MOST
||
widthMode
==
MeasureSpec.AT_MOST)
{
getMaxTextSize();
if
(heightMode
==
MeasureSpec.AT_MOST)
{
heightSize
=
(maxHeight
+
15)
*
indexArray.length;
}
if
(widthMode
==
MeasureSpec.AT_MOST)
{
widthSize
=
maxWidth
+
10;
}
}
setMeasuredDimension(widthSize,
heightSize);
}
@Override
protected
void
onDraw(Canvas
canvas)
{
for
(int
i
=
0;
i
<
indexArray.length;
i++)
{
String
index
=
indexArray[i];
float
x
=
(mWidth
-
mTextPaint.measureText(index))
/
2;
float
y
=
mMarginTop
+
mHeight
*
i
+
(mHeight
+
Utils.getTextHeight(mTextPaint,
index))
/
2;
//绘制字符
canvas.drawText(index,
x,
y,
mTextPaint);
}
}
@Override
public
boolean
onTouchEvent(MotionEvent
event)
{
switch
(event.getAction())
{
case
MotionEvent.ACTION_DOWN:
case
MotionEvent.ACTION_MOVE:
//
选中字符的下标
int
pos
=
(int)
((event.getY()
-
mMarginTop)
/
mHeight);
if
(pos
>=
0
&&
pos
<
indexArray.length)
{
setBackgroundColor(TOUCH_COLOR);
if
(onSideBarTouchListener
!=
null)
{
for
(int
i
=
0;
i
<
tags.size();
i++)
{
if
(indexArray[pos].equals(tags.get(i)))
{
onSideBarTouchListener.onTouch(indexArray[pos],
i);
break;
}
else
{
onSideBarTouchListener.onTouch(indexArray[pos],
-1);
}
}
}
}
break;
case
MotionEvent.ACTION_UP:
case
MotionEvent.ACTION_CANCEL:
setBackgroundColor(UNTOUCH_COLOR);
if
(onSideBarTouchListener
!=
null)
{
onSideBarTouchListener.onTouchEnd();
}
break;
}
return
true;
}
}在onMeasure()方法里,如果SideBar的宽、高测量模式为MeasureSpec.AT_MOST则重新计算SideBar的宽、高。onDraw()方法则是遍历索引数组,并绘制字符索引。在onTouchEvent()方法里,我们根据手指在SideBar上触摸坐标点的y值,计算出触摸的相应字符,以便在OnSideBarTouchListener接口进行后续操作,例如列表的跟随滚动等等。四、实例前边已经完成了三大核心功能,最后来愉快的使用下吧:public
class
MainActivity
extends
AppCompatActivity
{
@Override
protected
void
onCreate(Bundle
savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView
recyclerView
=
(RecyclerView)
findViewById(R.id.list);
SideBar
sideBar
=
(SideBar)
findViewById(R.id.side_bar);
final
TextView
tip
=
(TextView)
findViewById(R.id.tip);
final
List<ItemData>
datas
=
new
ArrayList<>();
ItemData
data
=
new
ItemData("北京");
datas.add(data);
ItemData
data1
=
new
ItemData("上海");
datas.add(data1);
ItemData
data2
=
new
ItemData("广州");
datas.add(data2);
.
.
.
ItemData
data34
=
new
ItemData("Hello
China");
datas.add(data34);
ItemData
data35
=
new
ItemData("宁波");
datas.add(data35);
SortHelper<ItemData>
sortHelper
=
new
SortHelper<ItemData>()
{
@Override
public
String
sortField(ItemData
data)
{
return
data.getTitle();
}
};
sortHelper.sortByLetter(datas);//将数据源按指定字段首字母排序
List<String>
tags
=
sortHelper.getTags(datas);//提取已排序数据源的tag值
MyAdapter
adapter
=
new
MyAdapter(this,
datas,
false);
final
LinearLayoutManager
layoutManager
=
new
LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
//添加分割线
recyclerView.addItemDecoration(new
DivideItemDecoration().setTags(tags));
//添加GroupHeader
recyclerView.addItemDecoration(new
GroupHeaderItemDecoration(this)
.setTags(tags)//设置tag集合
.setGroupHeaderHeight(30)//设置GroupHeader高度
.setGroupHeaderLeftPadding(20));//设置GroupHeader
左padding
recyclerView.setAdapter(adapter);
sideBar.setOnSideBarTouchListener(tags,
new
OnSideBarTouchListener()
{
@Override
public
void
onTouch(String
text,
int
position)
{
tip.setVisibility(View.VISIBLE);
tip.setText(text);
if
("↑".equals(text))
{
layoutManager.scrollToPositionWithOffset(0,
0);
return;
}
//滚动列表到指定位置
if
(position
!=
-1)
{
layoutManager.scrollToPositionWithOffset(position,
0);
}
}
@Override
public
void
onTouchEnd()
{
tip.setVisibility(View.GONE);
}
});
}
}这也就是文章开头的gif效果。如果需要自定义ItemView的绘制可以这样写:recyclerView.addItemDecoration(new
GroupHeaderItemDecoration(this)
.setTags(tags)
.setGroupHeaderHeight(30)
.setGroupHeaderLeftPadding(20)
.setOnDrawItemDecorationListener(new
OnDra
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
评论
0/150
提交评论