RecyclerView进阶(一)之分割线、添加Header和Footer

如今越来越多的开发者开始使用RecyclerView,与传统的ListView相比,它有许多优势:有更多的布局方式,更好的动画效果,更加灵活容易扩展,有局部刷新的能力等等。但不是说这样就能而完全取代ListView,毕竟它添加分割线、Header和Footer十分方便,而且自带有item的点击事件。所以使用RecyclerView还是ListView还是看具体应用场景。

RecyclerView之ItemDecoration

在RecyclerView中,没有divider属性来添加分割线;所以在开发中,RecyclerView使用这个类ItemDecoration来添加分割线,如下:

...
ItemDecoration decoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
recyclerView.addItemDecoration(decoration);
...

但它的能力远非如此,话不多说,先看两个效果:

soogif1.gif
stiky_header_decoration2.gif

要实现这样的效果只需要继承RecyclerView的ItemDecoration,重写它里面的方法就好了。先看一下ItemDecoration的源码,有下面三个方法:

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

/**
 * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
 * Any content drawn by this method will be drawn after the item views are drawn
 * and will thus appear over the views.
 *
 * @param c Canvas to draw into
 * @param parent RecyclerView this ItemDecoration is drawing into
 * @param state The current state of RecyclerView.
 */
public void onDrawOver(Canvas c, RecyclerView parent, State state)
/**
 * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
 * Any content drawn by this method will be drawn after the item views are drawn
 * and will thus appear over the views.
 *
 * @param c Canvas to draw into
 * @param parent RecyclerView this ItemDecoration is drawing into
 * @param state The current state of RecyclerView.
 */
public void onDrawOver(Canvas c, RecyclerView parent, State state)

OutRect

在讲上述三个方法前,先了解一下什么是OutRect

outRect.png

OutRect就是每个itemview下的矩形区域,默认情况下,outRect的left、top、right和bottom的值为0;itemview和OutRect是重合,就只能看见itemview。所以函数:

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

的作用就是把OutRect撑开一定的区域,就可以使用onDraw()onDrawOver()在这些被撑开的区域进行绘画。

RecyclerView的背景、onDraw绘制的内容、ItemonDrawOver绘制的内容,各层级关系如下1

cengjiguanxi.png

也就是说,onDraw绘制的内容可能会被itemView遮挡,itemView也可能被onDrawOver绘制的内容遮挡。

分割线

我们想给RecyclerView添加分割线,思路是把每个itemView的顶部或底部撑开一定的宽度,注意到在主布局中背景色设置为黑色,itemView的背景色在item_view.xml中设置为白色。那么当itemView顶部或底部撑开一段距离后,背景色就露了出来,看上去就像一条条的分割线。废话少说,开始动手写吧。我们创建一个新项目,添加RecyclerView的依赖后,在MainActivity中添加一个RecyclerView,布局文件为:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:background="#000000"
 android:orientation="vertical"
 tools:context=".MainActivity">
​
 <android.support.v7.widget.RecyclerView
     android:id="@+id/recycler_view"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 </android.support.v7.widget.RecyclerView>
</LinearLayout>

然后我们来写RecyclerView 的适配器。先定义一个itemVIew 的布局,命名为item_view,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="#ffffff"
 android:elevation="5dp">
​
 <TextView
    android:id="@+id/tv_item"
    android:text="35sp"
    android:gravity="center_vertical"
    android:layout_width="match_parent"
    android:layout_height="70dp" />
</LinearLayout>

我们只是演示一下效果,就简单定义一个TextView就好了。Adapter的代码如下:

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
   private List<String> mList;
   private Context context;
   public MyAdapter(List<String> list, Context context){
     mList = list;
     this.context = context;
   }
   class ViewHolder extends RecyclerView.ViewHolder{
     public TextView textView;
     public ViewHolder(View itemView) {
       super(itemView);
       textView = itemView.findViewById(R.id.tv_item);
     }
   }
   @NonNull
   @Override
   public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
     View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view, parent, false);
     return new ViewHolder(view);
   }

   @Override
   public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) {
 ((ViewHolder)holder).textView.setText(mList.get(position));
 }

 @Override
 public int getItemCount() {
   return mList.size();
 }
}

这里开始我们最主要的工作,就是继承ItemDecoration重写它的方法。

public class DividerDecoration extends RecyclerView.ItemDecoration {
   private List<String> mList;
   public DividerDecoration(List<String> list) {
     super();
     mList = list;
   }
   @Override
   public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
     super.getItemOffsets(outRect, view, parent, state);
     outRect.top = 2;//单位是像素px
   }
   @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);
   }
}

最后在主函数中

public class MainActivity extends AppCompatActivity {
    private List<String> mList;
    private RecyclerView recyclerView;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.activity_main);
     recyclerView = findViewById(R.id.recycler_view);
     MyAdapter adapter = new MyAdapter(mList, this);
     LinearLayoutManager manager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
     recyclerView.setLayoutManager(manager);
     recyclerView.setAdapter(adapter);
 }

效果如图所示:

divider.gif

为RecyclerView添加Header和Footer

说了这么多理论的东西,我们动手来实际操作一下。

Header和Footer

在ListView中,给我们提供了addHeader()AddFooter()方法来添加头部和底部。但在RecyclerView中没有封装好的方法供我们调用,需要自己去写相应的方法。从ListView的源码可以知道,它添加Header和Footer的方法是在适配器Adapter中动态添加的;所以在RecyclerView中,我们也要在adapter中添加相应方法。

主要思路是:根据适配器中的getItemViewType()方法,来判断当前的itemview的类型是内容列表项还是Header视图或者是Footer视图。对不同的类型加载各自的布局文件,从而实现添加Header和Footer的效果。

首先创建Header和Footer的布局文件header.xml和footer.xml,这里只在header.xml中写一个TextView,footer.xml布局和header一样。

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 app:cardCornerRadius="0dp"
 app:cardBackgroundColor="#abcd23"
 android:layout_width="match_parent"
 android:layout_height="100dp">
 <TextView
     android:id="@+id/header_title"
     android:textStyle="bold"
     android:text="Header"
     android:layout_gravity="center"
     android:textSize="30sp"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content" />
​
</android.support.v7.widget.CardView>

在RecyclerView的Adapter中,先定义这3种类型、Header视图和Footer视图的数量;Adapter代码如下:

public class HeaderFooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
 private List<String> mList;
 private Context context;
 /**
 * 定义3中itemview的类型
 */
 private static final int HEADER = 0;
 private static final int CONTENT = 1;
 private static final int FOOTER = 2;
 // Header和Footer的数量都为1。
 private int headerCount = 1;
 private int footerCount = 1;
​
 public HeaderFooterAdapter(List<String> list, Context context){
   mList = list;
   this.context = context;
 }
​
 public class HeaderViewHolder extends RecyclerView.ViewHolder{
   public HeaderViewHolder(View itemView) {
     super(itemView);
   }
 }
​
 public class FooterViewHolder extends RecyclerView.ViewHolder{
   public FooterViewHolder(View itemView) {
     super(itemView);
   }
 }
​
 public class ViewHolder extends RecyclerView.ViewHolder {
   TextView textView;
   public ViewHolder(View itemView) {
     super(itemView);
     textView = itemView.findViewById(R.id.item_view);
   }
 }
 /** 
 * 根据当前itemView的类型加载各自的视图
 */
 @NonNull
 @Override
 public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
   View view;
   if (viewType == HEADER){
     view = LayoutInflater.from(parent.getContext()).inflate(R.layout.header, parent, false);
     return new HeaderViewHolder(view);
   }else if (viewType == CONTENT){
     view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_card_view, parent, false);
     return new ViewHolder(view);
   }else if (viewType == FOOTER){
     view = LayoutInflater.from(parent.getContext()).inflate(R.layout.footer, parent, false);
     return new FooterViewHolder(view);
 }
   return null;
 }
​
 @Override
 public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position)  {
   if (holder instanceof HeaderViewHolder){

   }else if (holder instanceof ViewHolder){
 //因为第一项是header视图,所以内容视图从第二项开始,也就是从position=1的位置开始;
 //因为要从0获取list的内容,所以序号为position-headerCount。
   ((ViewHolder) holder).textView.setText(mList.get(position - headerCount));
   }else if (holder instanceof FooterViewHolder){
​
 }
 }
​
 @Override
 public int getItemCount() {
     return mList.size() + headerCount + footerCount;
 }
 /** 
 * 根据itemView的位置返回它的类型
 */
 @Override
 public int getItemViewType(int position) {
   if (headerCount != 0 && headerCount > position){
     return HEADER;
   }else if (footerCount != 0 && position >= (getItemCount() - 1)) {
     return FOOTER;
   }else {
     return CONTENT;
   }
 }
}

效果如下:

commonHF.gif

Header和Footer扩展

上面我们添加了头部视图和底部视图,但只是加载了一个简单的界面,我们也没给它绑定数据,其实header和footer也是itemview,那么itemview能做到的事,header和footer一样能完成。比如最开始的那张动态图,header就被设置成了一个轮播图,这也是很多APP上常见的布局。下面我们就来做做把header设置为一个轮播图。重复代码就不写了,只把新增的代码写下来,因为结构没变,只是添加一些代码内容。轮播图在github上很多相应的API,随便找一个就行。

首先还是header和footer的布局文件。去掉header.xml中的TextView,添加录播图。

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 app:cardElevation="5dp"
 app:cardCornerRadius="0dp"
 app:cardBackgroundColor="#986953"
 android:layout_width="match_parent"
 android:layout_height="180dp">
 <com.youth.banner.Banner
     android:id="@+id/banner"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"/>
​
</android.support.v7.widget.CardView>

然后修改adapter:

public class HeaderFooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
 ...
​
 public HeaderFooterAdapter(List<String> list, Context context){
 ...
 }
​
 public class HeaderViewHolder extends RecyclerView.ViewHolder{
   Banner banner;
   public HeaderViewHolder(View itemView) {
     super(itemView);
     banner = itemView.findVIewById(R.id.banner);
 }
 }
​
 public class FooterViewHolder extends RecyclerView.ViewHolder{
 ...
 }
​
 public class ViewHolder extends RecyclerView.ViewHolder {
 ...
 }
 /** 
 * 根据当前itemView的类型加载各自的视图
 */
 @NonNull
 @Override
 public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
   View view;
   if (viewType == HEADER){
     view = LayoutInflater.from(parent.getContext()).inflate(R.layout.header, parent, false);
     return new HeaderViewHolder(view);
   }else if (viewType == CONTENT){
     view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_card_view, parent, false);
     return new ViewHolder(view);
   }else if (viewType == FOOTER){
     view = LayoutInflater.from(parent.getContext()).inflate(R.layout.footer, parent, false);
   return new FooterViewHolder(view);
   }
   return null;
 }
​
 @Override
 public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position)  {
   if (holder instanceof HeaderViewHolder){
//这里进行轮播图的一些设置操作
     ((HeaderViewHolder) holder).banner.setDelayTime(4000)
     .setBannerStyle(BannerConfig.CIRCLE_INDICATOR_TITLE_INSIDE)
     .setImageLoader(new MyImageLoader())
     .setImages(imageList)
     .setIndicatorGravity(BannerConfig.CENTER)
     .setBannerTitles(titleList)
     .isAutoPlay(true)
     .setBannerAnimation(Transformer.DepthPage)
     .setOnBannerListener(new OnBannerListener() {
         @Override
         public void OnBannerClick(int position) {
             Toast.makeText(context, "第"+ position + "项", Toast.LENGTH_SHORT).show();
         }
     }).start();
   }else if (holder instanceof ViewHolder){
 //因为第一项是header视图,所以内容视图从第二项开始,也就是从position=1的位置开始;
 //因为要从0获取list的内容,所以序号为position-  headerCount。
     ((ViewHolder) holder).textView.setText(mList.get(position - headerCount));
   }else if (holder instanceof FooterViewHolder){
​
   }
 }
​
 @Override
 public int getItemCount() {
     return mList.size() + headerCount + footerCount;
 }
 /** 
 * 根据itemView的位置返回它的类型
 */
 @Override
 public int getItemViewType(int position) {
     ...
 }
}

效果如下:

bannerHF.gif

由于个人水平有限,如有错误和纰漏之处,还望不吝指教。

[1] 图片引用自层级图,侵删

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容