这里写个简单的例子,给 RecyclerVIEw 添加一个红色的分割线。上一章中,我们写了这么个简单的例子,现在就简单的分析一下。
public class colordivIDerItemdecoration extends RecyclerVIEw.Itemdecoration {
final static String TAG = "colordivIDerItem";
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) {
// 第一个ItemVIEw不需要在上面绘制分割线
if (parent.getChildAdapterposition(vIEw) != 0){
//这里直接硬编码为1px
outRect.top = 1;
mdivIDerHeight = 1;
}
}
@OverrIDe
public voID onDraw(Canvas c, RecyclerVIEw parent, RecyclerVIEw.State 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);
}
}
}
我们使用 linearLayout 布局时,属性设置为垂直,往里面塞了三个vIEw,那么三个vIEw会依次从上到下排布;如果是 FrameLayout 布局,没有设置 gravity 属性,那么三个vIEw会按顺序依次叠加在一起;使用 ListVIEw 时,我们能看到 item 一条接一条,紧密挨在一起;使用 RecyclerVIEw 时,设置垂直属性,效果与 ListVIEw 雷同。刚开始时,这样的效果给我造成了一个错觉,一直觉得 item 在复用,item 之间就应该是紧挨在一起,没有空隙的,后来随着接触的东西多了,见识广了,才知道当初的无知有多么的可笑。VIEwGroup 中的子vIEw的大小与布局,都在父控件的掌控之中,换句话说,父控件决定子控件的位置,所以只要我们喜欢,完全可以让item之间隔个100像素,左边都不与父控件左边对齐。上一章中,我们看到了 RecyclerVIEw 在布局item时,把从 Itemdecoration 中 getItemOffsets() 方法中获取的数据都用上了,这就导致 layout 的时候,如果 getItemOffsets() 中返回有值,那么子vIEw也就是 item 之间就有间隙了。这个时候,item 会绘制出自己的布局,间隙就显示父控件的背景颜色,就像上面 colordivIDerItemdecoration 中,如果我们把 onDraw() 方法中的代码移除,同时在 xml 布局中把 RecyclerVIEw 的背景色设置为红色,此时也能达到 item 之间添加红线的效果。
我们不通过xml这种方法,而是在 Itemdecoration 把它给画出来,并且还能实现更复杂的画面。我们看看 onDraw() 方法中,首先是 int childCount = parent.getChildCount() 获取到 RecyclerVIEw 的子vIEw 的个数,注意假如 childCount 为8,则说明 RecyclerVIEw 中有8个子vIEw,并不代表 Adapter 中只有8个item,因为子 vIEw 是可以复用的,所以我们根据 VIEw vIEw = parent.getChildAt(i) 这里的 i 与 Adapter 中的 position 并不是一回事,因此才有了 getChildAdapterposition(vIEw) 方法,通过反查来查出了 position 的值,即当前 item 在 RecyclerVIEw 中的位置。item之间的红色间隔线,是从0开始,倒数第二个结束,最后一个item是没有间隔线,我们有两种绘制方法,一是在item的底部绘制,最后一条不绘制;二是在item的顶部绘制,第0个item不绘制。这里我们用的是方法二,所以当 index 为 0 时,跳过当前。vIEw.gettop() 获取的是 item 距离 RecyclerVIEw 顶部的距离,暂时可以认为随着 RecyclerVIEw 上下滑动,每个item的 top 值是不停变化的,由于我们要绘制间隔线,所以item的top就是间隔线的bottom,那间隔线的top就是在底部的基础上向上移动 mdivIDerHeight 的距离,由于屏幕左上角为坐标原点,所以间隔线的top就是在bottom的基础上减去 mdivIDerHeight;绘制间隔线,没能超过父容器的约束的距离,比如RecyclerVIEw 设置了 padding 值,我们也要遵守,所以 left 就要从父容器的 paddingleft 开始,right 则要父容器的宽度减去 paddingRight。计算出间隔线的四个顶点的坐标后,用 Canvas 和 Paint 把它画出来,就像在自定义view控件中一样,就是这样。
如果我们想实现微信中通讯录模块,一个列表按照姓氏分类,拼音的首字母现在在同类型的最上端类似的功能,我们就可以像做分割线一样来实现它,还有一种思路,就是在item中显示拼音首字母,根据名字中计算哪个需要隐藏,哪个需要显示。我们先说说用item实现,再说用 Itemdecoration 实现。
public List<Bean> getData() {
List<Bean> List = new ArrayList<>();
for (int index = 0; index < 50; index++) {
if (index < 15) {
List.add(new Bean(
"粘性文本1", "name" + index));
} else if (index < 25) {
List.add(new Bean(
"粘性文本2", "name" + index));
} else if (index < 35) {
List.add(new Bean(
"粘性文本3", "name" + index));
} else {
List.add(new Bean(
"粘性文本4", "name" + index));
}
}
return List;
}
class StickyAdapter extends RecyclerVIEw.Adapter<StickyAdapter.RecyclerVIEwHolder> {
//第一个吸顶
static final int FirsT_STICKY_VIEW = 1;
//别的吸顶
static final int OTHER_STICKY_VIEW = 2;
//正常VIEw
static final int norMAL_VIEW = 3;
private final LayoutInflater mInflate;
private final List<Bean> datas;
StickyAdapter(Context context, List<Bean> datas){
mInflate = LayoutInflater.from(context);
this.datas = datas;
}
@OverrIDe
public RecyclerVIEwHolder onCreateVIEwHolder(VIEwGroup parent, int vIEwType) {
VIEw inflate = mInflate.inflate(R.layout.item_ui, parent, false);
return new RecyclerVIEwHolder(inflate);
}
@OverrIDe
public voID onBindVIEwHolder(RecyclerVIEwHolder holder, int position) {
Bean stickyBean = datas.get(position);
holder.tvname.setText(stickyBean.name);
if (position == 0) {
holder.tvStickyheader.setVisibility(VIEw.VISIBLE);
holder.tvStickyheader.setText(stickyBean.sticky);
holder.itemVIEw.setTag(FirsT_STICKY_VIEW);
} else {
if (!TextUtils.equals(stickyBean.sticky, datas.get(position - 1).sticky)) {
holder.tvStickyheader.setVisibility(VIEw.VISIBLE);
holder.tvStickyheader.setText(stickyBean.sticky);
holder.itemVIEw.setTag(OTHER_STICKY_VIEW);
} else {
holder.tvStickyheader.setVisibility(VIEw.GONE);
holder.itemVIEw.setTag(norMAL_VIEW);
}
}
//通过此处设置ContentDescription,作为内容描述,可以通过getContentDescription取出,功效跟setTag差不多。
holder.itemVIEw.setContentDescription(stickyBean.sticky);
}
@OverrIDe
public int getItemCount() {
return datas == null ? 0 : datas.size();
}
public class RecyclerVIEwHolder extends RecyclerVIEw.VIEwHolder{
TextVIEw tvStickyheader;
relativeLayout rlContentWrapper;
TextVIEw tvname;
RecyclerVIEwHolder(VIEw itemVIEw) {
super(itemVIEw);
tvStickyheader = (TextVIEw) itemVIEw.findVIEwByID(R.ID.tv_sticky_header_vIEw);
rlContentWrapper = (relativeLayout) itemVIEw.findVIEwByID(R.ID.rl_content_wrapper);
tvname = (TextVIEw) itemVIEw.findVIEwByID(R.ID.name);
}
}
}
public class Bean {
public String name;
public String sticky;
public Bean(String sticky, String name) {
this.sticky = sticky;
this.name = name;
}
}
xml 布局
<?xml version="1.0" enCoding="utf-8"?>
<FrameLayout xmlns:androID="http://schemas.androID.com/apk/res/androID"
xmlns:tools="http://schemas.androID.com/tools"
androID:layout_wIDth="match_parent"
androID:layout_height="match_parent"
androID:background="@color/color_10"
>
<androID.support.v7.Widget.RecyclerVIEw
androID:ID="@+ID/Feed_List"
androID:layout_wIDth="match_parent"
androID:layout_height="match_parent"
androID:background="@androID:color/white"
androID:scrollbars="vertical" />
<TextVIEw
androID:ID="@+ID/tv_suspensionbar"
androID:layout_wIDth="match_parent"
androID:layout_height="50dp"
androID:background="#EFFAE7"
androID:gravity="center"
androID:text="@string/hello_blank_fragment" />
</FrameLayout>
注意看 onBindVIEwHolder() 中,我们根据 sticky 值不一样,说明到了不同的文案的交错点,所以要显示出头部文案。如果此时要加一个粘性头部,就像微信ios版通讯录的效果,此时添加滑动监听机制
Activity 中布局
<?xml version="1.0" enCoding="utf-8"?>
<FrameLayout xmlns:androID="http://schemas.androID.com/apk/res/androID"
xmlns:tools="http://schemas.androID.com/tools"
androID:layout_wIDth="match_parent"
androID:layout_height="match_parent"
androID:background="@color/color_10"
>
<androID.support.v7.Widget.RecyclerVIEw
androID:ID="@+ID/Feed_List"
androID:layout_wIDth="match_parent"
androID:layout_height="match_parent"
androID:background="@androID:color/white"
androID:scrollbars="vertical" />
<TextVIEw
androID:ID="@+ID/tv_suspensionbar"
androID:layout_wIDth="match_parent"
androID:layout_height="50dp"
androID:background="#EFFAE7"
androID:gravity="center"
androID:text="@string/hello_blank_fragment" />
</FrameLayout>
mSuspensionbar 是 @+ID/tv_suspensionbar; mRecyclerVIEw 是 @+ID/Feed_List
mRecyclerVIEw.addOnScrollListener(new RecyclerVIEw.OnScrollListener() {
@OverrIDe
public voID onScrolled(RecyclerVIEw recyclerVIEw, int dx, int dy) {
super.onScrolled(recyclerVIEw, dx, dy);
VIEw stickvIEw = recyclerVIEw.findChildVIEwUnder(0, 0);
if (stickvIEw != null && stickvIEw.getContentDescription() != null) {
if (!TextUtils.equals(mSuspensionbar.getText(), stickvIEw.getContentDescription())) {
mSuspensionbar.setText(stickvIEw.getContentDescription());
}
}
VIEw transInfoVIEw = recyclerVIEw.findChildVIEwUnder(0, mSuspensionbar.getHeight() + 1);
if (transInfoVIEw.getTag() != null) {
int transVIEwStatus = (int) transInfoVIEw.getTag();
int top = transInfoVIEw.gettop();
if (transVIEwStatus == StickyAdapter.OTHER_STICKY_VIEW) {
if (top > 0) {
int dealtY = top - mSuspensionbar.getMeasuredHeight();
mSuspensionbar.setTranslationY(dealtY);
} else {
mSuspensionbar.setTranslationY(0);
}
} else {
mSuspensionbar.setTranslationY(0);
}
}
}
});
VIEw stickvIEw = recyclerVIEw.findChildVIEwUnder(0, 0) 找到的最上面的item, VIEw transInfoVIEw = recyclerVIEw.findChildVIEwUnder(0, mSuspensionbar.getHeight() + 1) 找到的是在 mSuspensionbar 这个控件下面1像素的地方所在的item,所以就有了下面的逻辑:一、如果列表position在15之前,transVIEwStatus 为 FirsT_STICKY_VIEW 或 norMAL_VIEW,此时 mSuspensionbar 待在原始位置即可;二、position到了15,transInfoVIEw 对应的 position 为 15 时,说明此时两个头部布局要接壤了,此时需要下面的把上面的给顶上去,怎么顶?计算距离,让 mSuspensionbar 随着RecyclerVIEw列表的滑动向上移动,所以有了 int top = transInfoVIEw.gettop(),此时 int dealtY = top - mSuspensionbar.getMeasuredHeight()算出了位移;如果 top 小于0,说明该item已经向上滑出了 RecyclerVIEw 的范围,所以就不用管了,把 mSuspensionbar 复原;三、接着又是类似一的逻辑,然后是二,就这样循环到结束。
上述如果使用 Itemdecoration 怎么实现呢?换一种思路和对象集合,我们先把对象在集合中分好组,添加属性来标识位置,
class GroupInfo {
private String content;
private String mTitle;
private boolean isFirstVIEwInGroup;
private boolean isLastVIEwInGroup;
public GroupInfo(String content, String Title) {
this.content = content;
this.mTitle = Title;
}
public String getContent() {
return content;
}
public String getTitle() {
return mTitle;
}
public voID setFirstVIEwInGroup(boolean firstVIEwInGroup) {
isFirstVIEwInGroup = firstVIEwInGroup;
}
public voID setLastVIEwInGroup(boolean lastVIEwInGroup) {
isLastVIEwInGroup = lastVIEwInGroup;
}
public boolean isFirstVIEwInGroup () {
return isFirstVIEwInGroup;
}
public boolean isLastVIEwInGroup () {
return isLastVIEwInGroup;
}
}
Activity 中代码
List<GroupInfo> List = getData();
mRecyclerVIEw.setAdapter(new TestAdapter(List));
mRecyclerVIEw.addItemdecoration(new Sectiondecoration(context, List));
适配器
class TestAdapter extends RecyclerVIEw.Adapter<TestAdapter.TestHolder> {
private final List<GroupInfo> datas;
TestAdapter(List<GroupInfo> datas){
this.datas = datas;
}
@OverrIDe
public TestHolder onCreateVIEwHolder(VIEwGroup parent, int vIEwType) {
TextVIEw tv = new TextVIEw(parent.getContext());
tv.setLayoutParams(new VIEwGroup.LayoutParams(VIEwGroup.LayoutParams.MATCH_PARENT, 120));
tv.setGravity(Gravity.CENTER_VERTICAL);
TestHolder holder = new TestHolder(tv);
return holder;
}
@OverrIDe
public voID onBindVIEwHolder(TestHolder holder, int position) {
if (datas != null && datas.size() > 0 ) {
String text = datas.get(position).getContent();
holder.tvname.setText(text);
}
}
@OverrIDe
public int getItemCount() {
return datas == null ? 0 : datas.size();
}
public class TestHolder extends RecyclerVIEw.VIEwHolder{
TextVIEw tvname;
TestHolder(VIEw itemVIEw) {
super(itemVIEw);
tvname = (TextVIEw) itemVIEw;
}
}
}
重点来了,
class Sectiondecoration extends RecyclerVIEw.Itemdecoration {
private List<GroupInfo> mList;
private int mheaderHeight;
private int mdivIDerHeight;
//用来绘制header上的文字
private TextPaint mTextPaint;
private Paint mPaint;
private float mTextSize;
private Paint.FontMetrics mFontMetrics;
public Sectiondecoration(Context context, List<GroupInfo> List) {
this.mList = List;
mdivIDerHeight = context.getResources().getDimensionPixelOffset(R.dimen.header_divIDer_height);
mheaderHeight = context.getResources().getDimensionPixelOffset(R.dimen.header_height);
mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.header_textsize);
mheaderHeight = (int) Math.max(mheaderHeight,mTextSize);
mTextPaint = new TextPaint();
mTextPaint.setcolor(color.BLACK);
mTextPaint.setTextSize(mTextSize);
mFontMetrics = mTextPaint.getFontMetrics();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setcolor(color.YELLOW);
}
@OverrIDe
public voID getItemOffsets(Rect outRect, VIEw vIEw, RecyclerVIEw parent, RecyclerVIEw.State state) {
super.getItemOffsets(outRect, vIEw, parent, state);
int position = parent.getChildAdapterposition(vIEw);
if ( mList != null ) {
GroupInfo groupInfo = mList.get(position);
//如果是组内的第一个则将间距撑开为一个header的高度,或者就是普通的分割线高度
if ( groupInfo != null && groupInfo.isFirstVIEwInGroup() ) {
outRect.top = mheaderHeight;
} else {
outRect.top = mdivIDerHeight;
}
}
}
@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);
if ( mList != null ) {
GroupInfo groupinfo = mList.get(index);
//只有组内的第一个ItemVIEw之上才绘制
if ( groupinfo.isFirstVIEwInGroup() ) {
int left = parent.getpaddingleft();
int top = vIEw.gettop() - mheaderHeight;
int right = parent.getWIDth() - parent.getpaddingRight();
int bottom = vIEw.gettop();
//绘制header
c.drawRect(left,top,right,bottom,mPaint);
float TitleX = left;
float TitleY = bottom - mFontMetrics.descent;
//绘制Title
c.drawText(groupinfo.getTitle(),TitleX,TitleY,mTextPaint);
}
}
}
}
}
这样,我们在 getItemOffsets() 中计算出item之间的间隙,然后在 onDraw() 中把头部画出来。如果想画出粘性头部布局呢?我们此时重写 onDrawOver() 方法,
@OverrIDe
public voID onDrawOver(Canvas c, RecyclerVIEw parent, RecyclerVIEw.State state) {
VIEw vIEw = parent.getChildAt(0);
int index = parent.getChildAdapterposition(vIEw);
if (mList != null) {
GroupInfo groupinfo = mList.get(index);
int left = parent.getpaddingleft();
int right = parent.getWIDth() - parent.getpaddingRight();
int top = parent.getpaddingtop();
if (groupinfo.isLastVIEwInGroup()) {
int suggesttop = vIEw.getBottom() - mheaderHeight;
if (suggesttop < top) {
top = suggesttop;
}
}
int bottom = top + mheaderHeight;
drawheaderRect(c, groupinfo, left, top, right, bottom);
}
}
private voID drawheaderRect(Canvas c, GroupInfo groupinfo, int left, int top, int right, int bottom) {
//绘制header
c.drawRect(left,top,right,bottom,mPaint);
float TitleX = left + mTextOffsetX;
float TitleY = bottom - mFontMetrics.descent;
//绘制Title
c.drawText(groupinfo.getTitle(),TitleX,TitleY,mTextPaint);
}
这个里面的原理与方法一种的滑动监听的原理相似,这里是直接获取RecyclerVIEw中首个item的vIEw,然后获取到位置position。注意,此时不同组的item是有间隔的,正常情况下,我们绘制最上层的粘性头部时,把它固定在顶部即可,即 top 为 parent.getpaddingtop(),bottom 为 top + mheaderHeight,然后通过 Canvas 把它画出来。最关键的一点就是两个粘性头部接壤了,此时需要把上面一个顶上,如果RecyclerVIEw中首个item的底部到父容器的距离小于了 mheaderHeight,说明两个头部接壤了,同理,算出它们的距离,然后算出 top 的值,理论上和方法一同样的原理。但此时这里需要注意一点,就是 Adapter 中的item的高度,要大于粘性头部布局的高度。
以上是内存溢出为你收集整理的RecyclerView 中 ItemDecoration 使用细节及逻辑分析 (粘性头部)全部内容,希望文章能够帮你解决RecyclerView 中 ItemDecoration 使用细节及逻辑分析 (粘性头部)所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)