打造高效通用RecyclerView.Adapter及ViewHolder

在使用RecyclerView的过程中,每一个列表Adapter,每一个样式都要重新编写对应的ViewHolder,去重新实现一遍onCreateViewHolder、onBindViewHolder等方法,这样做实在是劳心费神,非常麻烦。况且,当你希望在RecyclerView中加入动画的时候,(如:左右横滑,长按拖动,置顶,删除等)会发现还需要继承实现ItemTouchHelper.Callback(如果你知道要用它的话),再自行处理其中的dataset相关的变更逻辑,那简直是雪上加霜啊。为解决这些痛点,我们要对RecyclerView.ViewHolder、RecyclerView.Adapter进行封装,做到在绝大多数情况下,一次编写,到处复用。

1.通用ViewHolder
如何能做到通用呢?即不再需要定制ViewHolder,甚至不再需要在你的代码中new出任何ViewHolder,在用到ViewHolder中的任何一个控件都不用再每次都使用convertView.findViewById(id),因为即便ViewHolder中的布局很简单,也会有性能损耗(想想findViewById的原理)。但做到这两点优化并不难:ViewHolder的设计初衷就是缓存布局文件的各个控件,方便查找和设置内容。因此我们在遵循该初衷的基础上更进一步,传入layoutId,在ViewHolder初始化时渲染好ItemView;设置缓存机制(使用SparseArray,int-Obj 对应的映射表),即在ViewHolder内部就能直接获取到并返回所需要用到的每一个控件,这样就完成了ComViewHolder的封装,代码如下。

public class ComViewHolder extends RecyclerView.ViewHolder {
    private SparseArrayCompat<View> mViews;
    private View mConvertView;

    public ComViewHolder(Context context, View itemView, ViewGroup parent) {
        super(itemView);
        mConvertView = itemView;
        mViews = new SparseArrayCompat<>();
    }

    public static ComViewHolder getComViewHolder(Context context, int layoutId, ViewGroup parent) {
        View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        return new ComViewHolder(context, itemView, parent);
    }

    /**
     * 缓存+提取
     * @param layoutId
     * @param <T>
     * @return
     */
    public <T extends View> T getView(int layoutId) {
        View view = mViews.get(layoutId);
        if (view == null) {
            view = mConvertView.findViewById(layoutId);
            mViews.put(layoutId, view);
        }
        return (T) view;
    }
}

2.ComRecyclerViewAdapter
通用的Adapter,在使用上述ComViewHolder之后,就避免了手写onCreateViewHolder()和onBindViewHolder()方法的处境,取而代之的是暴露一个虚方法convert()给业务代码,在convert()方法中进行对应item的控件操作。由于ComViewHolder提供了静态方法getComViewHolder(context,layoutId,viewGroup)并返回ComViewHolder实例,因此在Adapter在任何需要初始化ViewHolder场景的情况下,都能直接使用getComViewHolder。相关方法实现如下:

    //init
    protected List<E> mGroup;
    protected Context mContext;
    protected int mLayoutId;

    public ComRecyclerViewAdapter(Context context, int layoutId) {
        mContext = context;
        mLayoutId = layoutId;
        mGroup = new ArrayList<>();
    }
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return ComViewHolder.getComViewHolder(mContext, mLayoutId, parent);
    }


    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        convert((ComViewHolder) holder, mGroup.get(position), getItemViewType(position), position);
    }

    public abstract void convert(ComViewHolder viewHolder, E data, int type, int position);

当然,一开始的时候说过,除了布局渲染和复用方面有所优化,在动画效果方面也有相关的封装。在讲完动效相关封装后再回来说明。

3.SimpleItemTouchHelperCallback
RecyclerView最突出的特性之一就在于它提供了比ListView更友好跟方便的动画效果辅助类:ItemTouchHelper及ItemTouchHelper.Callback。基于这个好用而方便的特性当然要加以利用,我们封装了一套SimpleItemTouchHelperCallback,继承ItemTouchHelper.Callback,将常用操作:长按拖动、左右横扫删除等操作及相关的衍生操作(如:置顶)封装进去,并获得手势松开时的回调用来执行后续操作。此外,该类还需要能够在初始化时控制上述两种手势开关。


public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
    //一个实现了ItemTouchHelperAdapter接口协议的任意Adapter    
    private ItemTouchHelperAdapter mAdapter; 
    
    //控制两种手势的开关
    private boolean mCanDrag;
    private boolean mCanSwipe;
    
    //手势松开释放的监听
    private OnSelectEndListener mOnSelectEndListener;

    public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter, boolean canDrag, boolean canSwipe) {
        mAdapter = adapter;
        mCanDrag = canDrag;
        mCanSwipe = canSwipe;
    }

    public void setOnSelectEndListener(OnSelectEndListener onSelectEndListener) {
        mOnSelectEndListener = onSelectEndListener;
    }

    
    //默认支持竖直方向上下,水平方向左右动作
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END);
    }

    //用于两个item交换位置,若两item属于纵向列表方向,则为上下交换。
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        mAdapter.onItemSwap(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }
     //两个item交换位置后回调(注意跟手势释放的回调加以区分)
    @Override
    public void onMoved(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int fromPos, RecyclerView.ViewHolder target, int toPos, int x, int y) {
        super.onMoved(recyclerView, viewHolder, fromPos, target, toPos, x, y);
    }


    //用于扫动某个item,可根据direction自行定制,这里未区分(若列表为纵向,则为横扫)
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
    }

    //控制能否长按拖动
    @Override
    public boolean isLongPressDragEnabled() {
        return mCanDrag;
    }

    //控制能否扫动
    @Override
    public boolean isItemViewSwipeEnabled() {
        return mCanSwipe;
    }
   
    
    //判断手势是否放开对应item的ViewHolder,一般用于拖动情况中
    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {

        if (viewHolder == null) {
            if (mOnSelectEndListener != null) {
                mOnSelectEndListener.onSelectEnd();
            }
        } else {
            Log.e("ss","start");
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

    public interface OnSelectEndListener {
        void onSelectEnd();
    }
}

4.再谈ComRecyclerViewAdapter
为了能够使用SimpleItemTouchHelper,令ComRecyclerViewAdapter实现接口ItemTouchHelperAdapter及相关协议:

public interface ItemTouchHelperAdapter {
    void onItemTop(int fromPosition);

    void onItemDismiss(int position);

    void onItemSwap(int itemAPosition, int itemBPosition);
}

最终得到可支持通用动效的高度复用的ComRecyclerViewAdapter,这其中无非就是两项,ViewHolder及ItemTouchHelperAdapter协议,是不是非常简单?


public abstract class ComRecyclerViewAdapter<E> extends RecyclerView.Adapter implements ItemTouchHelperAdapter {

    protected List<E> mGroup;
    protected Context mContext;
    protected int mLayoutId;

    public ComRecyclerViewAdapter(Context context, int layoutId) {
        mContext = context;
        mLayoutId = layoutId;
        mGroup = new ArrayList<>();
    }

    public ComRecyclerViewAdapter(Context context, int layoutId, List<E> datas) {
        this(context, layoutId);
        if (datas == null)
            mGroup = new ArrayList<>();
        else
            mGroup = datas;
    }

    public void setGroup(List<E> group) {
        mGroup = group;
        notifyDataSetChanged();
    }

    /**
     * 置顶
     * @param fromPosition
     */
    @Override
    public void onItemTop(int fromPosition) {
        E data = mGroup.get(fromPosition);
        for (int i = fromPosition; i > 0; i--) {
            mGroup.set(i, mGroup.get(i - 1));
        }
        mGroup.set(0, data);
        notifyItemMoved(fromPosition, 0);
    }


    /**
     * 拖动交换
     * @param itemAPosition
     * @param itemBPosition
     */
    @Override
    public void onItemSwap(int itemAPosition, int itemBPosition) {
        Collections.swap(mGroup, itemAPosition, itemBPosition);
        notifyItemMoved(itemAPosition, itemBPosition);
    }

    /**
     * 删除
     * @param position
     */
    @Override
    public void onItemDismiss(int position) {
        mGroup.remove(position);
        notifyItemRemoved(position);
    }


    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return ComViewHolder.getComViewHolder(mContext, mLayoutId, parent);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        convert((ComViewHolder) holder, mGroup.get(position), getItemViewType(position), position);
    }

    public abstract void convert(ComViewHolder viewHolder, E data, int type, int position);


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

    @Override
    public long getItemId(int position) {
        return getItemIdFromData(mGroup.get(position));
    }

    public long getItemIdFromData(E data) {
        return RecyclerView.NO_ID;
    }

}

后续还会继续对多类型及涉及到Header和Footer的Adapter怎么封装。

附源码工程:
https://github.com/GhostInMatrix/PullToRefreshRecyclerview

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

推荐阅读更多精彩内容