小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

RecyclerView使用详解

RecyclerView是google在2014年I/O大会上提出新的用于取代ListView的组件。相比ListView而言,它更加强大,而且非常灵活,这边文章将会详细介绍会RecyclerView以及它的内部类。

本文中所用的代码我都放在了Github上,地址为:https://github.com/cgspine/RecyclerView

理解RecyclerView的结构

RecyclerView在职责分离上是做得非常好的,所以在使用RecyclerView前我们需要理解RecyclerView的结构层次并清除了解其各个部分的职责。

与其它的View不一样的地方是RecyclerView不负责子view的展现和布局工作,即子view的位置、大小等都不由RecyclerView自己负责。它所关注的是子view的回收与复用,专注于性能的提升。而其它的工作如布局、子view的布局、子view的装饰、子view的动画都交给了其内部类来负责,下表展示了几个关键的内部类和它们的用途。

用途
ViewHolder 用来装载子view数据的容器盒子
Adapter 数据源,创造子view并包裹数据
LayoutManager 布局管理器,用于将特定的子元素放在特定的位置
ItemDecoration 用来装饰子view,如分割线、偏移等
ItemAnimator 用于在子view添加、删除、移动时做动画

ViewHolder

ViewHolder是google在优化ListView性能的技巧上就提到的,虽然google并没有强制使用,但事实上它已经成为ListView的编写规范。在ListView上的使用可以参考下面这一片文章:

ListView Optimisations : Part 1 (the ViewHolder)

RecyclerView上,ViewHolder的使用成为了一种强制手段了,使用方法如下:

public class ListItemViewHolder extends RecyclerView.ViewHolder{
            TextView title;
            TextView desc;
            public ListItemViewHolder(View itemView){
                    super(itemView);
                    title = (TextView)itemView.findViewById(R.id.txt_title);
                    desc = (TextView)itemView.findViewById(R.id.txt_desc);
            }
    }

Recycler.Adapter

RecyclerView使用Adpeter,但不同于ListViewAutoCopmpleteTextView使用的SimpleAdapterArrayAdapter,Google试图为RecyclerView打造一款全新的adapter。

RecyclerView.Adapter中有三个必须继承的抽象方法:

public VH onCreateViewHolder(ViewGroup parent, int viewType)  
public void onBindViewHolder(VH holder, int position)  
public int getItemCount()  

上面的VH是泛型的标识符,当你在Adapter中使用时需要转换为特定的类,下面是一个使用实例:

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ListItemViewHolder> {

        private List<Data.Model> mList;
        private SparseBooleanArray mSelectedItems;

        public RecyclerViewAdapter(List<Data.Model> list){
                if(list == null){
                        throw new IllegalArgumentException("model Data must not be null");
                }
                mList = list;
        }

        @Override
        public ListItemViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
                View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.view_list_item,viewGroup,false);
                return new ListItemViewHolder(itemView);
        }

        @Override
        public void onBindViewHolder(ListItemViewHolder listItemViewHolder, int i) {
                Data.Model model = mList.get(i);
                listItemViewHolder.title.setText(model.title);
                listItemViewHolder.desc.setText(model.desc);
        }

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

        public class ListItemViewHolder extends RecyclerView.ViewHolder{
            //ViewHolder
        }
}

RecyclerView.LayoutManager

这个内部类的作用是布局所有的子view,也是相当有趣的一个类,我们可以用它来实现各色的布局方式,目前已经有一些官方给出的默认实现子类了,如:LinearLayoutManagerGridLayoutManager。这些类可以帮助我们很轻松的实现水平或竖直列表布局和网格列表布局。

在LayoutMangager中只有一个抽象类:

public LayoutParams generateDefaultLayoutParams()  

也就是我们在继承RecyclerView.LayoutManger时,必须复写的方法就只有这一个,这一个方法会返回一个LayoutParams,为子类的布局提供一个基准。除此之外,我们需要注意的是,我们应该复写scrollToPosition(int position)这个方法,因为翻看其源代码可以发现,他可能在不久的将来变成抽象的方法:

public void scrollToPosition(int position) {
    if (DEBUG) {
        Log.e(TAG, "You MUST implement scrollToPosition. It will soon become abstract");
    }
}

另外一个很重要的方法是onLayoutChildren(),,这个方法在首次布局子view的时候会被调用,然后在每次adapter通知有数据改变时又会被调用。我们应该把子view的布局逻辑放在这个方法里面。

一般而言,我们需要在这个方法里进行一下步骤来完成布局:

  1. 在最近的一次事件发生后检查所有附着在RecyclerView上的view的偏移位置;
  2. 决定是否有空的空间因滑动而产生,如果有则从Recycler中获取view填充上去;
  3. 决定是否有子view变成不可见的了,如果有则移除他们并放置在recycler中;
  4. 决定剩余展示在界面的子view是否需要重新组织。上述的步骤也许要求你更新索引去更好的适应它们在适配器中的位置。

RecyclerView.ItemDecoration

通过ItemDecoration,我们可以来改变每个子view的offset或修改子view本身来达到装饰子view或区分子view的目的。

ItemDecoration是可以叠加的,因此你个一为每个子view装饰若干遍。

ItemDecoration提供了三个重要的方法来供我们打造个性化的装饰器:

public void onDraw(Canvas c, RecyclerView parent)  

这个方法允许我们在子view上绘制,但所绘制的内容存在被子view自己内容覆盖的可能。

public void onDrawOver(Canvas c, RecyclerView parent)  

这个方法也是在子view上绘制,但是不会被子view自己的内容覆盖,反而存在覆盖子view内容的可能。

public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)  

这个方法是用来做子view的偏移量的,但是需要注意的一个问题是:采取反而方式不是直接返回数值,而是返回了个Rect,之所以返回Rect,是因为Rect在使用时是可以复用的,因此存在复用情结果不是自己想要的结果的现象。

RecyclerView.ItemAnimator

ItemAnimator是一个方便的类来帮我们做个别子view的添加、移动、删除的相关动画效果。SDK已经为我们准备了一个默认的实现叫DefaultItemAnimator。如果我们没有为RecyclerView设置一个自己的ItemAnimator,那么RecyclerView将会采用默认的实现。

为了去执行动画效果,Android系统需要知道数据的改变。以前我们利用Adapter中的notifyDataSetChanged()来发出数据改变的通知,但显然通知的精度不够高,因此Recycler.Adopter细化了这些通知,其形式为notifyXyz(),如:

public final void notifyItemInserted(int position)  
public final void notifyItemRemoved(int position)  

以上是对RecyclerView体系的各个组成部分做了个简短的介绍,下面给出一些更加具体的使用情景。

多item类型布局的实现

ListView中也是有多item类型的运用的,其关键是利用重写下面两个方法实现的:

 public int getItemViewType(int position)

用于返回当前item所属的布局类型

public int getViewTypeCount()  

用于返回该ListView总共有多少种类型

但在RecyclerView.Adopter中只保留了上述方法中的一个:

public int getItemViewType(int position)  

而另外一个方法在Adapter中的public VH onCreateViewHolder(ViewGroup parent, int viewType)也就能体现出来了。

public class MutiItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {  
    private List<Data.Model> mList;
    private enum ITEM_TYPE{
        ITEM_TYPE_ONE,
        ITEM_TYPE_TWO
    }

    public MutiItemAdapter(List<Data.Model> list){
        if(list == null){
            throw new IllegalArgumentException("model Data must not be null");
        }
        mList = list;
    }

    @Override
    public int getItemViewType(int position) {
        return position%2==0?ITEM_TYPE.ITEM_TYPE_ONE.ordinal():ITEM_TYPE.ITEM_TYPE_TWO.ordinal();
    }

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

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
         if(holder instanceof ItemOneViewHolder){
             ItemOneViewHolder h = (ItemOneViewHolder)holder;
             h.title.setText("title" + position);
             h.desc.setText(new Date().toString());
         }else if(holder instanceof ItemTwoViewHolder){
             ItemTwoViewHolder h = (ItemTwoViewHolder)holder;
             h.title.setText("title" + position);
             h.img.setImageResource(R.mipmap.ic_launcher);
         }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if(viewType == ITEM_TYPE.ITEM_TYPE_ONE.ordinal()){
            return new ItemOneViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.view_list_item,parent,false));
        }else if(viewType == ITEM_TYPE.ITEM_TYPE_TWO.ordinal()){
            return new ItemTwoViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.view_list_item_2,parent,false));
        }
        return null;
    }

    public class ItemOneViewHolder extends RecyclerView.ViewHolder{
        TextView title;
        TextView desc;
        public ItemOneViewHolder(View itemView) {
            super(itemView);
            title = (TextView)itemView.findViewById(R.id.txt_title);
            desc = (TextView)itemView.findViewById(R.id.txt_desc);
        }
    }

    public class ItemTwoViewHolder extends RecyclerView.ViewHolder{
        TextView title;
        ImageView img;
        public ItemTwoViewHolder(View itemView) {
            super(itemView);
            title = (TextView)itemView.findViewById(R.id.item_title);
            img = (ImageView)itemView.findViewById(R.id.item_img);
        }
    }
}

列表动画与两次布局

为了让列表看上去很帅,动画是非常受欢迎的,不过其难度也比较大。RecyclerView的动画是基于itemAnimator实现的,而一些牛人也打造了各式的itemAnimator,其github地址为:

https://github.com/wasabeef/recyclerview-animators

去实现一个自定义的itemAnimator前需要对list的动画实现有一定的了解。Android提供了一个LayoutTransition用于view的动画,其原理就是记录view的初始态和末态,然后程序提供过渡效果,从而达到动画的效果。但这一场景并不适应与列表item的动画。因为列表item动画与adapter的数据挂钩。但是list中view的出现与消失取决于view是否出现在屏幕中,而list中item的添加与删除取决于adopter的更新,这使得view与item的更新是不同步的。因而存在一些场景如:item的删除会造成其它view的添加、item的添加会造成其它view的移除。因此旧的LayoutTransition无法满足新的需求,需要新的解决方案。

为了实现列表item的动画,RecyclerView采取了两次布局的方式来实现这个效果:

删除item

添加item

  • pre-layout:标记作用,在这个阶段要删除的item不会真的删除,要添加的item也不会真的添加,而是会为那些需要变动的元素打上标记,这样就明确了在真正布局的时候item会如何变动。
  • post-layout:真正的布局阶段。

在布局函数onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) 中传入的参数state中得方法isPreLayout()就标示着是否是pre-layout。

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state){
    if(state.isPreLayout()){
        //pre-layout
    }else{
        //post-layout
    }
}

但是需要注意一点的是,我们需要复写supportsPredictiveItemAnimations()并返回true时才能会触发两次布局效果

public boolean supportsPredictiveItemAnimations() {
    return true;
}

滚动实现与滚动类型

列表类型的view在item多于一屏时都需要滚动效果的,但不同于ListView,RecyclerView并不会帮你默认实现滚动效果,而是需要开发者自己去实现,当然其实现也是在布局管理LayoutManager中去实现的。

首先是允许或阻止用户滚动:

@Override
public boolean canScrollVertically() {
    return true;
}

其次是滚动效果的实现,RecyclerView提供了默认的微分方法scrollHorizontallyBy() & scrollVerticallyBy()来做移动,而不是采用用按下手指后手指移动的距离来进行view位置的偏移。

一件需要注意的事情是我们依然需要在scrollXXXBy()方法里面手动偏移RecyclerView的子views,系统提供了offsetXXX(int distance)来做views的偏移。这样做虽然增加了写代码的工作量,但是却能够使手指滑动距离与实际偏移距离的比例在开发者手中更可控。

在RecyclerView中,为滚动赋予了两种状态RecyclerView.SCROLL_STATE_DRAGGINGRecyclerView.SCROLL_STATE_SETTLING,前一种是由人为拖拽产生滚动,后一种由动画产生滚动,然后提供了一个方便的状态改变监听函数onScrollStateChanged(int state)。所以我们可以在状态改变前后做我们想做的事情。

public void onScrollStateChanged(int state) {
    super.onScrollStateChanged(state);
    switch (state){
        case RecyclerView.SCROLL_STATE_IDLE:
            //滚动停止时
            break;
        case RecyclerView.SCROLL_STATE_DRAGGING:
            //拖拽滚动时
            break;
        case RecyclerView.SCROLL_STATE_SETTLING:
            //动画滚动时
            break;
    }
}

状态机制的引入能够让思维更加清晰。但是,这里还是有坑的,那就是很多系统已有的动画实现函数如smoothScrollToPosition(int position)的实现是scrollXXXBy()的积分实现,即滚动过程中会不断切换SCROLL_STATE_SETTLINGSCROLL_STATE_IDLE,即我们的滚动监听函数会不断触发SCROLL_STATE_SETTLINGSCROLL_STATE_IDLE这两个case,也需要我们代码逻辑的足够清晰。

RecyclerView还有很多知识需要探索。深入学习,就必须能够自己完全实现各式各样的LayoutManager、itemDecoration、itemAnimator,这样才能真正熟练地运用RecyclerView到自己的业务逻辑中去。

←支付宝← →微信 →
comments powered by Disqus