RecyclerView基本使用

1. RecyclerView概述

从Android 5.0开始,谷歌公司推出了一个用于大量数据展示的新控件RecylerView,可以用来代替传统的ListView,更加强大和灵活。

在ListView中 改变列表某一个item数据,然后刷新列表,会回到最顶部,而RecyclerView可以保持原来滑动的位置不变。

在使用RecyclerView时,可能会涉及到如下内容:

  • 想要控制其Item们的排列方式,使用布局管理器LayoutManager
  • 如果要创建一个适配器,请使用RecyclerView.Adapter
  • 想要控制Item间的间隔,请使用RecyclerView.ItemDecoration
  • 想要控制Item增删的动画,请使用RecyclerView.ItemAnimator
  • CardView扩展FrameLayout类并能够显示卡片内信息,这些信息在整个平台中拥有一致的呈现方式。CardView小部件可拥有阴影和圆角。

上述功能主要涉及到的方法如下:

mRecyclerView = findView(R.id.id_recyclerview);
//设置布局管理器
mRecyclerView.setLayoutManager(layout);
//设置adapter
mRecyclerView.setAdapter(adapter)
//设置Item增加、移除动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//添加分割线
mRecyclerView.addItemDecoration(new DividerItemDecoration(
                getActivity(), DividerItemDecoration.HORIZONTAL_LIST));
RecyclerView基本使用_第1张图片
image

如上图所示,如果要使用RecyclerView,必须指定一个Adapter和LayoutManager

2. RecyclerView使用

2.1 使用流程

RecyclerView定义在support库当中,想要使用该控件,需要在项目的build.gradle中添加相应的依赖库。在app/build.gradle文件,在dependencies闭包中添加当下的版本:

 implementation 'com.android.support:recyclerview-v7:28.0.0'

同步之后,就可以展开对RecyclerView的使用,其使用方式其实与ListView的三要素类似。

在具体演示RecyclerView使用之前,左中了解一下其适配器:RecyclerView.Adapter

2.2 RecyclerView.Adapter

该适配器是一个抽象类,并支持泛型

public abstract static class Adapter {
   ...
}

如果创建一个适配器继承RecyclerView.Adapter,需要重写三个方法:

  • onCreateViewHolder()
  • onBindViewHolder()
  • getItemCount()

指定泛型后,方法一与方法二会根据泛型改变。

创建适配器时,一般会先定义一个ViewHolder内部类继承RecyclerView.ViewHolder,则指定的泛型即为该ViewHolder,可以在其内定义每个列表上的视图控件,并在onCreateViewHolder中初始化

  • onCreateViewHolder()示例
@Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
        ViewHolder holder = new ViewHolder(view);
        return holder;
    }

该方法主要用来创建ViewHolder,可以根据需求的itemType,创建出多个ViewHolder。创建多个itemType时,需要getItemViewType(int position)方法配合

  • onBindViewHolder()示例
@Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getImageId());
        holder.fruitName.setText(fruit.getName());
    }

该方法主要是将layout视图控件与ViewHolder中元件属性绑定。

  • getItemCount()示例
@Override
    public int getItemCount() {
        return mFruitList.size();
    }

这个方法的返回值,便是RecyclerView中实际item的数量。有些情况下,当增加了HeaderView或者FooterView后,需要注意考虑这个返回值

介绍了RecyclerView.Adapter的这三个方法,现在简单示例RecyclerView的使用

  • 在xml中添加RecyclerView控件



    
    

  • 新建RecyclerView需要展示的界面xml布局



    

    


  • 定义数据类
public class Fruit {

    private String name;

    private int imageId;

    public Fruit(String name, int imageId) {
        this.name = name;
        this.imageId = imageId;
    }

    public String getName() {
        return name;
    }

    public int getImageId() {
        return imageId;
    }

}
  • 新建适配器,将其绑定到RecyclerView
public class FruitAdapter extends RecyclerView.Adapter {

    private List mFruitList;

    static class ViewHolder extends RecyclerView.ViewHolder {
        ImageView fruitImage;
        TextView fruitName;

        public ViewHolder(View view) {
            super(view);
            fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
            fruitName = (TextView) view.findViewById(R.id.fruit_name);
        }
    }

    public FruitAdapter(List fruitList) {
        mFruitList = fruitList;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item, parent, false);
        ViewHolder holder = new ViewHolder(view);
        return holder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Fruit fruit = mFruitList.get(position);
        holder.fruitImage.setImageResource(fruit.getImageId());
        holder.fruitName.setText(fruit.getName());
    }

    @Override
    public int getItemCount() {
        return mFruitList.size();
    }
}
  • 使用RecyclerView
public class MainActivity extends AppCompatActivity {

    private List fruitList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initFruits();
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        recyclerView.setLayoutManager(layoutManager);
        FruitAdapter adapter = new FruitAdapter(fruitList);
        recyclerView.setAdapter(adapter);
    }

    private void initFruits() {
        for (int i = 0; i < 2; i++) {
            Fruit apple = new Fruit(getRandomLengthName("Apple"), R.drawable.apple_pic);
            fruitList.add(apple);
            Fruit banana = new Fruit(getRandomLengthName("Banana"), R.drawable.banana_pic);
            fruitList.add(banana);
            Fruit orange = new Fruit(getRandomLengthName("Orange"), R.drawable.orange_pic);
            fruitList.add(orange);
            Fruit watermelon = new Fruit(getRandomLengthName("Watermelon"), R.drawable.watermelon_pic);
            fruitList.add(watermelon);
            Fruit pear = new Fruit(getRandomLengthName("Pear"), R.drawable.pear_pic);
            fruitList.add(pear);
            Fruit grape = new Fruit(getRandomLengthName("Grape"), R.drawable.grape_pic);
            fruitList.add(grape);
            Fruit pineapple = new Fruit(getRandomLengthName("Pineapple"), R.drawable.pineapple_pic);
            fruitList.add(pineapple);
            Fruit strawberry = new Fruit(getRandomLengthName("Strawberry"), R.drawable.strawberry_pic);
            fruitList.add(strawberry);
            Fruit cherry = new Fruit(getRandomLengthName("Cherry"), R.drawable.cherry_pic);
            fruitList.add(cherry);
            Fruit mango = new Fruit(getRandomLengthName("Mango"), R.drawable.mango_pic);
            fruitList.add(mango);
        }
    }

    private String getRandomLengthName(String name) {
        Random random = new Random();
        int length = random.nextInt(20) + 1;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length; i++){
            builder.append(name);
        }
        return builder.toString();
    }
}

除了除了LinearLayoutManager之外,还提供了GridLayoutManagerStaggeredGridLayoutManager这两种内置的布局排列方式。前者实现网格布局,后者可以用于实现瀑布流布局。

备注

在onCreateViewHolder()中,映射Layout必须为

View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, parent, false);

而不能是

View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_1, null);

2.3 万能适配器

创建RecyclerView的Adapter时,发现实现过程大同小异,可以创建一个万能适配器,快捷创建Adapter。

下面给出一个简易版万能适配器,学习了RecyclerView其他部分内容在给出完成版。

public abstract class QuickAdapter extends RecyclerView.Adapter{

     private List mDatas;

     public QuickAdapter(List datas){
         this.mDatas = datas;
     }

     public abstract int getLayoutId(int viewType);

     @Override
     public VH onCreateViewHolder(ViewGroup parent, int viewType) {
         return VH.get(parent,getLayoutId(viewType));
     }

     @Override
     public void onBindViewHolder(VH holder, int position) {
         convert(holder, mDatas.get(position), position);
     }

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

     public abstract void convert(VH holder, T data, int position);

     static class VH extends RecyclerView.ViewHolder{...}
 }

其中QuickAdapter.VH的实现如下:

static class VH extends RecyclerView.ViewHolder{
     private SparseArray mViews;
     private View mConvertView;

     private VH(View v){
         super(v);
         mConvertView = v;
         mViews = new SparseArray<>();
     }

     public static VH get(ViewGroup parent, int layoutId){
         View convertView = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
         return new VH(convertView);
     }

     public  T getView(int id){
         View v = mViews.get(id);
         if(v == null){
             v = mConvertView.findViewById(id);
             mViews.put(id, v);
         }
         return (T)v;
     }

     public void setText(int id, String value){
         TextView view = getView(id);
         view.setText(value);
     }
 }

其中:

  • getLayoutId(viewType)是根据viewType返回布局ID
  • convert()做具体Bind操作

设置完上述的万能适配器,如果要创建一个Adapter时,只需:

mAdapter = new QuickAdapter(data) {
     @Override
     public int getLayoutId(int viewType) {
         switch(viewType){
             case TYPE_1:
                 return R.layout.item_1;
             case TYPE_2:
                 return R.layout.item_2;
         }
     }

     public int getItemViewType(int position) {
         if(position % 2 == 0){
             return TYPE_1;
         } else{
             return TYPE_2;
         }
     }

     @Override
     public void convert(VH holder, Model data, int position) {
         int type = getItemViewType(position);
         switch(type){
             case TYPE_1:
                 holder.setText(R.id.text, data.text);
                 break;
             case TYPE_2:
                 holder.setImage(R.id.image, data.image);
                 break;
         }
     }
 };

3. LayoutManager自定义与使用

3.1 概述

如果说Adapter负责提供View,而LayoutManger则负责它们在RecyclerView中摆放的位置以及在窗口中不可见之后的回收策略

RecyclerView提供的布局管理器:

  • LinearLayoutManager 以垂直或水平滚动列表方式显示项目
  • GridLayoutManager 在网格中显示项目。
  • StaggeredGridLayoutManager 在分散对齐网格中显示项目。

这里简单介绍LinearLayoutManager的几个重要方法,分析一下LayoutManger的实现

  • onLayoutChildren(): 对RecyclerView进行布局的入口方法。
  • fill(): 负责填充RecyclerView。
  • scrollVerticallyBy():根据手指的移动滑动一定距离,并调用fill()填充。
  • canScrollVertically()canScrollHorizontally(): 判断是否支持纵向滑动或横向滑动。

其中onLayoutChildren()的核心实现如下:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
     detachAndScrapAttachedViews(recycler); //将原来所有的Item View全部放到Recycler的Scrap Heap或Recycle Pool
     fill(recycler, mLayoutState, state, false); //填充现在所有的Item View
 }

RecyclerView的回收机制有个重要的概念,即将回收站分为Scrap HeapRecycle Pool,其中Scrap Heap的元素可以被直接复用,而不需要调用onBindViewHolder()。detachAndScrapAttachedViews()会根据情况,将原来的Item View放入Scrap Heap或Recycle Pool,从而在复用时提升效率

fill()是对剩余空间不断地调用layoutChunk(),直到填充完为止。layoutChunk()的核心实现如下:

public void layoutChunk() {
     View view = layoutState.next(recycler); //调用了getViewForPosition()
     addView(view);  //加入View
     measureChildWithMargins(view, 0, 0); //计算View的大小
     layoutDecoratedWithMargins(view, left, top, right, bottom); //布局View
 }

其中next()调用了getViewForPosition(currentPosition),该方法是从RecyclerView的回收机制实现类Recycler中获取合适的View。

在具体演示如何自定义LayoutManager之前,先介绍一个常用的API

  • recycler.getViewForPosition(position)

  • 获取位置为position的View。

  • getPosition(View view)

  • 获取view的位置。

  • measureChildWithMargins(View child, int widthUsed, int heightUsed)

  • 测量view的宽高,包括外边距。

  • layoutDecoratedWithMargins(View child, int left, int top, int right,int bottom)

  • 将child显示在RecyclerView上面,left,top,right,bottom规定了显示的区域。

  • detachView(View child)

  • 临时回收view。

  • attachView(View child)

  • detachView(View child)回收的view拿回来。

  • detachAndScrapAttachedViews(RecyclerView.Recycler recycler)

  • 用指定的recycler临时移除所有添加的views。

  • detachAndScrapView(View child, RecyclerView.Recycler recycler)

  • 用指定的recycler临时回收view。

  • removeAndRecycleAllViews(RecyclerView.Recycler recycler)

  • 移除所有的view并且用给的recycler回收。

  • removeAndRecycleView(View child, RecyclerView.Recycler recycler)

  • 移除指定的view并且用给的recycler回收。

  • offsetChildrenHorizontal(int dx)

  • 水平移动所有的view,同样也有offsetChildrenVertical(int dy)

3.2 简单示例

这里我们通过演示一个简单示例来介绍一下自定义一个LayoutManager需要实现的核心方法。

  • 首先生成一个类,例如CustomLayoutManager,派生自LayoutManager
public class CustomLayoutManager extends LayoutManager {
    @Override
    public LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
                RecyclerView.LayoutParams.WRAP_CONTENT);
    }
}

派生自LayoutManager时,会强制让我们生成generateDefaultLayoutParams()方法,即RecyclerView Item的布局参数。无特殊要求,直接让子Item自己决定宽高即可

  • 添加onLayoutChild()
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //定义竖直方向的偏移量
    int offsetY = 0;
    for (int i = 0; i < getItemCount(); i++) {
        View view = recycler.getViewForPosition(i);
        addView(view);
        measureChildWithMargins(view, 0, 0);
        int width = getDecoratedMeasuredWidth(view);
        int height = getDecoratedMeasuredHeight(view);
        layoutDecorated(view, 0, offsetY, width, offsetY + height);
        offsetY += height;
    }
}

​ 这里做了两件事:

  • 把所有Item对应的view加载进来

  • 把所有Item摆在其应在的地方

这个方法是LayoutManager的主入口,在view需要初始化布局时调用(适配器数据改变或适配器替换时再次被调用)。

通常来说,在这个方法中你需要完成主要步骤如下:

  • 在滚动事件结束后检查所有附加视图当前的偏移位置。
  • 判断是否需要添加新视图填充由滚动屏幕产生的空白部分。并从 Recycler 中获取视图。
  • 判断当前视图是否不再显示。移除它们并放置到 Recycler 中。
  • 判断剩余视图是否需要整理。发生上述变化后可能 需要你修改视图的子索引来更好地和它们的适配器位置校准。

这个示例较为简单,只是通过measureChildWithMargins(view, 0, 0);函数测量这个view,并且通过getDecoratedMeasuredWidth(view)得到测量出来的宽度,需要注意的是通过getDecoratedMeasuredWidth(view)得到的是item+decoration的总宽度。如果你只想得到view的测量宽度,通过view.getMeasuredWidth()就可以得到了

然后通过layoutDecorated();函数将每个item摆放在对应的位置,每个Item的左右位置都是相同的,从左侧x=0开始摆放,只是y的点需要计算。所以这里有一个变量offsetY,用以累加当前Item之前所有item的高度。

  • 添加滑动效果
@Override
public boolean canScrollVertically() {
    return true;
}

@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // 平移容器内的item
    offsetChildrenVertical(-dy);
    return dy;
}

通过在canScrollVertically()中return true;使LayoutManager具有垂直滚动的功能。然后在scrollVerticallyBy()中接收每次滚动的距离dy。

其中dy表示手指在屏幕上每次滑动的位移:

  • 当手指由下往上滑时,dy>0
  • 当手指由上往下滑时,dy<0

明显手指上滑,子Item上移,需要减去dy,让item移动-dy距离是合理的。可以通过offsetChildrenVertical()移动所有item

  • 添加异常判断

前述代码实现滚动,但是Item到顶或者底仍可以继续滚动,需要加以判断是否到顶或者到底

判断到顶了

判断到顶只需把所有dy相加,如果小于0,表示到顶,不再移动

private int mSumDy = 0;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    int travel = dy;
    //如果滑动到最顶部
    if (mSumDy + dy < 0) {
        travel = -mSumDy;
    }
    mSumDy += travel;
    // 平移容器内的item
    offsetChildrenVertical(-travel);
    return dy;
}

判断到底了

判断到底的方法是用总高度减去最后一屏高度,即到底的偏移值。如果大于这个偏移值,则说明超过底部。

首先要得到所有Item的总高度,在onLayoutChildren中会测量所有的item并对每一个item布局,只需在onLayoutChildren()中将所有的item高度相加即可得到所有Item的总高度。

private int mTotalHeight = 0;
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //定义竖直方向的偏移量
    int offsetY = 0;
    for (int i = 0; i < getItemCount(); i++) {
        View view = recycler.getViewForPosition(i);
        addView(view);
        measureChildWithMargins(view, 0, 0);
        int width = getDecoratedMeasuredWidth(view);
        int height = getDecoratedMeasuredHeight(view);
        layoutDecorated(view, 0, offsetY, width, offsetY + height);
        offsetY += height;
    }
    //如果所有子View的高度和没有填满RecyclerView的高度,
    // 则将高度设置为RecyclerView的高度
    mTotalHeight = Math.max(offsetY, getVerticalSpace());
}
private int getVerticalSpace() {
    return getHeight() - getPaddingBottom() - getPaddingTop();
}

其中getVerticalSpace()函数可以得到RecyclerView用于显示item的真实高度。接着在srcollVerticallyBy中判断到底与否并处理:

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    int travel = dy;
    //如果滑动到最顶部
    if (mSumDy + dy < 0) {
        travel = -mSumDy;
    } else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
        travel = mTotalHeight - getVerticalSpace() - mSumDy;
    }

    mSumDy += travel;
    // 平移容器内的item
    offsetChildrenVertical(-travel);
    return dy;
}

mSumDy + dy 表示当前的移动距离,mTotalHeight - getVerticalSpace()表示当滑动到底时滚动的总距离;

3.3 LayoutManager附加特性

LayoutManager中提供了一些辅助方法操作decoration(关于ItemDecoration在后面详细介绍):

  • getDecoratedLeft()代替child.getLeft()获取子视图的 left 边缘。
  • getDecoratedTop()代替getTop()获取子视图的 top 边缘。
  • getDecoratedRight()代替getRight()获取子视图的 right 边缘。
  • getDecoratedBottom()代替getBottom()获取子视图的 bottom 边缘。
  • 使用 measureChild()measureChildWithMargins() 代替child.measure() 测量来自 Recycler 的新视图。
  • 使用layoutDecorated() 代替 child.layout() 布局来自 Recycler 的新视图。
  • 使用 getDecoratedMeasuredWidth()getDecoratedMeasuredHeight() 代替 child.getMeasuredWidth()或 child.getMeasuredHeight()获取 子视图的测量数据

3.3.1 数据集改变

当使用 notifyDataSetChanged()触发 RecyclerView.Adapter 的更新操作时, LayoutManager 负责更新布局中的视图。这时,onLayoutChildren()会被再次调用。实现这个功能需要我们在onLayoutChildre()方法中判断出当前状态是生成一个新的视图还是adapter更新期间视图改变。

。。。。。

3.3.2 onAdapterChanged()

这个方法提供了另一个重置布局的场所。设置新的 adapter 会触发这个事件。

示例:

@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
    //Completely scrap the existing layout
    removeAllViews();
}

移除视图会触发一个新的布局过程,当 onLayoutChildren() 被再次调用时, 我们的代码会执行创建新视图的布局过程,因为现在没有 attched 的子视图。

3.3.3 Scroll to Position

其一个重要的特性就是给LayoutManager添加滚动到特定位置的功能。可以带有动画效果,也可以没有

  • scrollToPosition()

    layout将当前位置设为第一个可见Item时,调用RecyclerView的scrollToPosition()

示例

@Override
public void scrollToPosition(int position) {
    if (position >= getItemCount()) {
        Log.e(TAG, "Cannot scroll to "+position+", item count is "+getItemCount());
        return;
    }

    //Ignore current scroll offset, snap to top-left
    mForceClearOffsets = true;
    //Set requested position as first visible
    mFirstVisiblePosition = position;
    //Trigger a new view layout
    requestLayout();
}
  • smoothScrollToPosition()

在带有动画的情况下,需要在该方法中创建一个RecyclerView.SmoothScroller实例,在方法返回前请求startSmoothScroll()启动动画

RecyclerView.SmoothScroller 是提供 API 的抽象类,含有四个方法:

  1. onStart()

    当滑动动画开始时被触发

  2. onStop()

    当滑动动画停止时被触发

  3. onSeekTargetStep()

    当 scroller 搜索目标 view 时被重复调用,这个方法负责读取提供的 dx/dy ,然后更新应该在这两个方向移动的距离。

  4. onTragetFound()

    只在目标视图被 attach 后调用一次。 这是将目标视图要通过动画移动到准确位置最后的场所。

4. ItemDecoration

RecyclerView并没有支持divider这样的属性,我们可以自己定制分割线。

RecyclerView添加分割线的方法是:mRecyclerView.addItemDecoration(),该方法的参数为RecyclerView.ItemDecoration,该类为抽象类

查看一下其源码:

public abstract static class ItemDecoration {
        public ItemDecoration() {
        }

        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.onDraw(c, parent);
        }

        /** @deprecated */
        @Deprecated
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
        }

        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.onDrawOver(c, parent);
        }

        /** @deprecated */
        @Deprecated
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
        }

        /** @deprecated */
        @Deprecated
        public void getItemOffsets(@NonNull Rect outRect, int itemPosition, @NonNull RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }

        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.getItemOffsets(outRect, ((RecyclerView.LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent);
        }
    }

调用addItemDecoration()方法添加decoration的时候,会调用该类的onDraw()onDrawOver()方法

  • onDraw方法先于drawChildren
  • onDrawOver在drawChildren之后,一般我们选择复写其中一个即可。
  • getItemOffsets为每一个item设置一定的偏移量,主要用于绘制Decorator,设置分割线宽、高等

简单示例:(这里LayoutManager为LinearLayoutManager)

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    private Drawable mDivider;

    private int mOrientation;

    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent) {

        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }

    }


    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            android.support.v7.widget.RecyclerView v = new android.support.v7.widget.RecyclerView(parent.getContext());
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

该实现类可以看到通过读取系统主题中的 android.R.attr.listDivider作为Item间的分割线,并且支持横向和纵向

然后在原来的代码中添加一句:

mRecyclerView.addItemDecoration(new DividerItemDecoration(this,
DividerItemDecoration.VERTICAL_LIST));

4.1 ItemDecoration解析

  • 前述代码分割线是系统默认,你可以在theme.xml中找到该属性的使用情况,使用系统的listDivider可以方便我们随意改变,其属性声明:

    
        
    

    当然可以自己写个Drawable,例如:

    
    
    
        
        
    
    
    
  • 要使用ItemDecoration,必须先自定义,继承ItemDecoration,重写getItemOffsets()onDraw()

    这里先解释ItemDecoration含义。

    ItemDecoration即对Item起装饰作用

    RecyclerView基本使用_第2张图片
    image

    getItemOffsets()就是设置item周边的偏移量(也就是装饰区域的宽度),而onDraw()才是真正实现装饰的回调方法,该方法可以在装饰区域任意画画。

    前面ItemDecoration源码中可以看出,其主要有三个方法:

    OnDraw()
    onDrawOver()
    getItemOffsets()
    

    这三个方法的作用可以简单理解如下图

    RecyclerView基本使用_第3张图片
    image

    其中绿色表示内容,红色表示装饰:

    • 图1表示getItemOffsets(),实现类似padding效果
    • 图2表示onDraw(),实现类似绘制背景效果,内容在上
    • 图3表示onDrawOver(),可以绘制在内容上面,覆盖内容

    假设我们要实现线性列表的分割线:

    • 当线性列表是水平方向时,分割线竖直的;当线性列表是竖直方向时,分割线是水平的
    • 当画竖直分割线时,需要在item的右边偏移出一条线的宽度;当画水平分割线时,需要在item的下边偏移出一条线的高度
    /**
     * 画线
     */
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        if (orientation == RecyclerView.HORIZONTAL) {
            drawVertical(c, parent, state);
        } else if (orientation == RecyclerView.VERTICAL) {
            drawHorizontal(c, parent, state);
        }
    }
    
    
    /**
    * 设置条目周边的偏移量
    */
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        if (orientation == RecyclerView.HORIZONTAL) {
            //画垂直线
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        } else if (orientation == RecyclerView.VERTICAL) {
            //画水平线
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        }
    }
    

    getItemOffsets()是相对每个item而言的,即每个item都会偏移出相同的装饰区域,而onDraw则不同,它是相对Canvas来说的,通俗的说就是要自己找到要画的线的位置

    /**
     * 在构造方法中加载系统自带的分割线(就是ListView用的那个分割线)
     */
    public MyDecorationOne(Context context, int orientation) {
        this.orientation = orientation;
        int[] attrs = new int[]{android.R.attr.listDivider};
        TypedArray a = context.obtainStyledAttributes(attrs);
        mDivider = a.getDrawable(0);
        a.recycle();
    }
    
    /**
     * 画竖直分割线
     */
    private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state) {
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getRight() + params.rightMargin;
            int top = child.getTop() - params.topMargin;
            int right = left + mDivider.getIntrinsicWidth();
            int bottom = child.getBottom() + params.bottomMargin;
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
    
    /**
     * 画水平分割线
     */
    private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state) {
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getLeft() - params.leftMargin;
            int top = child.getBottom() + params.bottomMargin;
            int right = child.getRight() + params.rightMargin;
            int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
    
    RecyclerView基本使用_第4张图片
    image

简单示例ItemDecoration的三个主要方法:点击查看原文阅读

下面是没有添加任何ItemDecoration的界面:

RecyclerView基本使用_第5张图片
image
  1. 预计实现效果如下时:

    RecyclerView基本使用_第6张图片
    image

    从效果图看实现了分割线效果。根据前述知识,只需先用getItemOffset()方法在Item下方空出一定的高度控件,然后用onDraw()绘制这个空间即可

    public class SimpleDividerDecoration extends RecyclerView.ItemDecoration {
    
        private int dividerHeight;
        private Paint dividerPaint;
    
        public SimpleDividerDecoration(Context context) {
            dividerPaint = new Paint();
            dividerPaint.setColor(context.getResources().getColor(R.color.colorAccent));
            dividerHeight = context.getResources().getDimensionPixelSize(R.dimen.divider_height);
        }
    
    
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            outRect.bottom = dividerHeight;  //item下方空出一块空间
        }
    
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            int childCount = parent.getChildCount();
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
         //遍历item
            for (int i = 0; i < childCount - 1; i++) {
                View view = parent.getChildAt(i);
                float top = view.getBottom();
                float bottom = view.getBottom() + dividerHeight;
                c.drawRect(left, top, right, bottom, dividerPaint);
            }
        }
    }
    
  2. 预计实现效果如下图时:

    RecyclerView基本使用_第7张图片
    image

    这种效果有点像商品标签,覆盖在内容之上可以通过onDrawOver()实现

    public class LeftAndRightTagDecoration extends RecyclerView.ItemDecoration {
        private int tagWidth;
        private Paint leftPaint;
        private Paint rightPaint;
    
        public LeftAndRightTagDecoration(Context context) {
            leftPaint = new Paint();
            leftPaint.setColor(context.getResources().getColor(R.color.colorAccent));
            rightPaint = new Paint();
            rightPaint.setColor(context.getResources().getColor(R.color.colorPrimary));
            tagWidth = context.getResources().getDimensionPixelSize(R.dimen.tag_width);
        }
    
        @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 child = parent.getChildAt(i);
                int pos = parent.getChildAdapterPosition(child);
                boolean isLeft = pos % 2 == 0;
                if (isLeft) {
                    float left = child.getLeft();
                    float right = left + tagWidth;
                    float top = child.getTop();
                    float bottom = child.getBottom();
                    c.drawRect(left, top, right, bottom, leftPaint);
                } else {
                    float right = child.getRight();
                    float left = right - tagWidth;
                    float top = child.getTop();
                    float bottom = child.getBottom();
                    c.drawRect(left, top, right, bottom, rightPaint);
    
                }
            }
        }
    }
    

    ItemDecoration效果是可以叠加的

    recyclerView.addItemDecoration(new LeftAndRightTagDecoration(this));
    recyclerView.addItemDecoration(new SimpleDividerDecoration(this));
    

    将上面两个ItemDecoration同时添加到RecyclerView时,效果如下:

    RecyclerView基本使用_第8张图片
    image
  3. 预计实现效果如下图时:

    RecyclerView基本使用_第9张图片
    image

    效果有点类似手机通讯录分组,这种效果就是有些Item添加分割线,有些没有而已。

    • 定义一个接口给Activity进行回调用来进行数据分组和获取首字母

      public interface DecorationCallback {
      
              long getGroupId(int position);
      
              String getGroupFirstLine(int position);
          }
      
    • 实现ItemDecoration

      public class SectionDecoration extends RecyclerView.ItemDecoration {
          private static final String TAG = "SectionDecoration";
      
          private DecorationCallback callback;   //回调接口
          private TextPaint textPaint;
          private Paint paint;
          private int topGap;
          private Paint.FontMetrics fontMetrics;
      
         //初始化
          public SectionDecoration(Context context, DecorationCallback decorationCallback) {
              Resources res = context.getResources();
              this.callback = decorationCallback;
      
              paint = new Paint();
              paint.setColor(res.getColor(R.color.colorAccent));
      
              textPaint = new TextPaint();
              textPaint.setTypeface(Typeface.DEFAULT_BOLD);
              textPaint.setAntiAlias(true);
              textPaint.setTextSize(80);
              textPaint.setColor(Color.BLACK);
              textPaint.getFontMetrics(fontMetrics);
              textPaint.setTextAlign(Paint.Align.LEFT);
              fontMetrics = new Paint.FontMetrics();
              topGap = res.getDimensionPixelSize(R.dimen.sectioned_top);//32dp
      
      
          }
      
      
          @Override
          public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
              super.getItemOffsets(outRect, view, parent, state);
              int pos = parent.getChildAdapterPosition(view);
              Log.i(TAG, "getItemOffsets:" + pos);
              long groupId = callback.getGroupId(pos);
              if (groupId < 0) return;
              if (pos == 0 || isFirstInGroup(pos)) {//同组的第一个才添加padding
                  outRect.top = topGap;
              } else {
                  outRect.top = 0;
              }
          }
      
          @Override
          public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
              super.onDraw(c, parent, state);
              int left = parent.getPaddingLeft();
              int right = parent.getWidth() - parent.getPaddingRight();
              int childCount = parent.getChildCount();
              for (int i = 0; i < childCount; i++) {
                  View view = parent.getChildAt(i);
                  int position = parent.getChildAdapterPosition(view);
                  long groupId = callback.getGroupId(position);
                  if (groupId < 0) return;
                  String textLine = callback.getGroupFirstLine(position).toUpperCase();
                  //判断是否需要绘制分割线
                  if (position == 0 || isFirstInGroup(position)) {
                      float top = view.getTop() - topGap;
                      float bottom = view.getTop();
                      c.drawRect(left, top, right, bottom, paint);//绘制红色矩形
                      c.drawText(textLine, left, bottom, textPaint);//绘制文本
                  }
              }
          }
      
         
          private boolean isFirstInGroup(int pos) {
              if (pos == 0) {
                  return true;
              } else {
                  long prevGroupId = callback.getGroupId(pos - 1);
                  long groupId = callback.getGroupId(pos);
                  return prevGroupId != groupId;
              }
          }
      
          public interface DecorationCallback {
      
              long getGroupId(int position);
      
              String getGroupFirstLine(int position);
          }
      }
      
    • 在Activity中使用

      recyclerView.addItemDecoration(new SectionDecoration(this, new SectionDecoration.DecorationCallback() {
                  @Override
                  public long getGroupId(int position) {
                      return Character.toUpperCase(dataList.get(position).getName().charAt(0));
                  }
      
                  @Override
                  public String getGroupFirstLine(int position) {
                      return dataList.get(position).getName().substring(0, 1).toUpperCase();
                  }
              }));
      
  4. 预计实现效果如下时:

    [图片上传失败...(image-ca730d-1565686110808)]

    这种效果叫做粘性头部。

    Header不动则一定要绘制在Item内容之上,需要重写onDrawOver(),其他与前述代码一致

    public class PinnedSectionDecoration extends RecyclerView.ItemDecoration {
        private static final String TAG = "PinnedSectionDecoration";
    
        private DecorationCallback callback;
        private TextPaint textPaint;
        private Paint paint;
        private int topGap;
        private Paint.FontMetrics fontMetrics;
    
    
        public PinnedSectionDecoration(Context context, DecorationCallback decorationCallback) {
            Resources res = context.getResources();
            this.callback = decorationCallback;
    
            paint = new Paint();
            paint.setColor(res.getColor(R.color.colorAccent));
    
            textPaint = new TextPaint();
            textPaint.setTypeface(Typeface.DEFAULT_BOLD);
            textPaint.setAntiAlias(true);
            textPaint.setTextSize(80);
            textPaint.setColor(Color.BLACK);
            textPaint.getFontMetrics(fontMetrics);
            textPaint.setTextAlign(Paint.Align.LEFT);
            fontMetrics = new Paint.FontMetrics();
            topGap = res.getDimensionPixelSize(R.dimen.sectioned_top);
    
    
        }
    
    
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            int pos = parent.getChildAdapterPosition(view);
            long groupId = callback.getGroupId(pos);
            if (groupId < 0) return;
            if (pos == 0 || isFirstInGroup(pos)) {
                outRect.top = topGap;
            } else {
                outRect.top = 0;
            }
        }
    
    
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
            int itemCount = state.getItemCount();
            int childCount = parent.getChildCount();
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            float lineHeight = textPaint.getTextSize() + fontMetrics.descent;
    
            long preGroupId, groupId = -1;
            for (int i = 0; i < childCount; i++) {
                View view = parent.getChildAt(i);
                int position = parent.getChildAdapterPosition(view);
    
                preGroupId = groupId;
                groupId = callback.getGroupId(position);
                if (groupId < 0 || groupId == preGroupId) continue;
    
                String textLine = callback.getGroupFirstLine(position).toUpperCase();
                if (TextUtils.isEmpty(textLine)) continue;
    
                int viewBottom = view.getBottom();
                float textY = Math.max(topGap, view.getTop());
                if (position + 1 < itemCount) { //下一个和当前不一样移动当前
                    long nextGroupId = callback.getGroupId(position + 1);
                    if (nextGroupId != groupId && viewBottom < textY ) {//组内最后一个view进入了header
                        textY = viewBottom;
                    }
                }
                c.drawRect(left, textY - topGap, right, textY, paint);
                c.drawText(textLine, left, textY, textPaint);
            }
    
        }
    
    }
    

5. Item Animator

6. Item监听

RecyclerView默认没有像ListView一样提供setOnItemClickListener()接口,需要我们自己去改造实现。

6.1 常规实现

关于RecyclerView的Item点击事件常规做法就是在Adapter中增加OnItemClickListener接口和setOnItemClickListener接口,为每个Item添加点击监听。

代码示例:

  • 自定义Adapter适配器,添加item click接口,设置点击事件
public class MyAdapter extends RecyclerView.Adapter {

    private LayoutInflater mInflater;
    private Context mContext;
    private List mDatas;
    private int[] imgIds;

    private OnItemClickListener mOnItemClickListener;

    public MyAdapter(Context context, List datas, int[] imgIds) {
        this.mContext = context;
        this.mDatas = datas;
        this.imgIds = imgIds;
        mInflater = LayoutInflater.from(context);
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = mInflater.inflate(R.layout.item_single_textview, parent, false);
        MyViewHolder viewHolder = new MyViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(final MyViewHolder holder, final int position) {
        holder.textView.setText(mDatas.get(position));
        holder.imageView.setBackgroundResource(imgIds[ position % imgIds.length]);

        // item click
        if (mOnItemClickListener != null) {
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    mOnItemClickListener.onItemClick(holder.itemView, position);
                }
            });

            // item long click
            holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View view) {
                    mOnItemClickListener.onItemLongClick(holder.itemView, position);
                    return true;
                }
            });
        }
    }

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

    public void setOnItemClickListener(OnItemClickListener listener) {
        this.mOnItemClickListener = listener;
    }

    public void addData(int pos) {
        mDatas.add(pos, "Add one");
        notifyItemInserted(pos);
    }

    public void deleteData(int pos) {
        mDatas.remove(pos);
        notifyItemRemoved(pos);
    }

    public interface OnItemClickListener {
        void onItemClick(View view, int position);
        void onItemLongClick(View view, int position);
    }
}
class MyViewHolder extends RecyclerView.ViewHolder {

    ImageView imageView;
    TextView textView;

    public MyViewHolder(View itemView) {
        super(itemView);
        textView = itemView.findViewById(R.id.textView);
        imageView = itemView.findViewById(R.id.imageView);
    }
}
  • 修改MainActivity
mAdapter = new MyAdapter(this, mDatas, imgIds);
        recyclerView.setAdapter(mAdapter);

        //设置RecyclerView的布局管理
        LinearLayoutManager manager = new LinearLayoutManager(this,
                LinearLayoutManager.VERTICAL, false);
        recyclerView.setLayoutManager(manager);

        recyclerView.setItemAnimator(new DefaultItemAnimator());

        mAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                Toast.makeText(MainActivity.this, "clicked " + position,
                        Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onItemLongClick(View view, int position) {
                Toast.makeText(MainActivity.this, "long clicked " + position,
                        Toast.LENGTH_SHORT).show();
            }
        });
    }

6.2 addOnItemTouchListener()

你可能感兴趣的:(RecyclerView基本使用)