专栏首页开发之途Android RecyclerView的简便写法

Android RecyclerView的简便写法

RecyclerView现在可以说是很常用了吧?RecyclerView自然是很方便的控件,但用多了有时候对一些重复性代码也是感觉挺麻烦的,于是乎我就将一些重复性代码封装了起来,从而使 RecyclerView 的使用更加的简便

本篇博客包含的内容有:

1. 通用的单布局 RecyclerView.Adapter

2. 通用的多布局 RecyclerView.Adapter

3. 通用的 RecyclerView.ItemDecoration

4. RecyclerView 的单击和长按事件监听

5. 带头部与底部View的 RecyclerView

6. SnapHelper 的使用

一、通用的 RecyclerView.Adapter

CommonRecyclerViewAdapter 是一个抽象类,利用泛型构造了一个通用的Adapter,并通过 MultiTypeSupport 接口来实现对多布局的支持。 此外,有时候我们在刷新数据时,改变的数据可能只是List集合中的单个数据,如果都采用 Adapter.notifyDataSetChanged() 来刷新整个视图,无疑是浪费资源的,此处采用了 DiffUtil 来对比前后两个数据集,寻找出旧数据集与新数据集的最小变化量,从而对数据进行定向刷新,可以只刷新相应的Item,使得视图的刷新过程更为高效,且增添和删除数据时都伴随有相应的动画效果

/**
 * 作者:叶应是叶
 * 时间:2017/12/21 21:50
 * 说明:通用RecyclerView Adapter
 */
public abstract class CommonRecyclerViewAdapter<T> extends RecyclerView.Adapter<CommonRecyclerViewHolder> {

    //省略一些代码
    ......

    private DiffUtil.Callback callback = new DiffUtil.Callback() {

        @Override
        public int getOldListSize() {
            return getItemCount();
        }

        @Override
        public int getNewListSize() {
            return newDataList.size();
        }

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            return CommonRecyclerViewAdapter.this.areItemsTheSame(oldItemPosition, newItemPosition);
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            return CommonRecyclerViewAdapter.this.areContentsTheSame(oldItemPosition, newItemPosition);
        }

        @Nullable
        @Override
        public Object getChangePayload(int oldItemPosition, int newItemPosition) {
            return CommonRecyclerViewAdapter.this.getChangePayload(oldItemPosition, newItemPosition);
        }
    };
    
    public void setData(final List<T> dataList) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                newDataList.clear();
                newDataList = CommonRecyclerViewAdapter.this.clone(dataList);
                DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback, true);
                Message message = new Message();
                message.what = DIFF_UTIL_UPDATE;
                message.obj = diffResult;
                handler.sendMessage(message);
            }
        }).start();
    }

    @Override
    public int getItemViewType(int position) {
        if (multiTypeSupport != null) {
            return multiTypeSupport.getLayoutId(dataList.get(position), position);
        }
        return DEFAULT_ITEM_VIEW_TYPE;
    }

    @Override
    public CommonRecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (multiTypeSupport != null) {
            layoutId = viewType;
        }
        return new CommonRecyclerViewHolder(layoutInflater.inflate(layoutId, parent, false));
    }

    @Override
    public void onBindViewHolder(CommonRecyclerViewHolder holder, int position, List<Object> payloads) {
        if (payloads.isEmpty()) {
            onBindViewHolder(holder, position);
        } else {
            Bundle bundle = (Bundle) payloads.get(0);
            partialBindData(holder, bundle);
        }
    }

    @Override
    public void onBindViewHolder(CommonRecyclerViewHolder holder, int position) {
        entirelyBindData(holder, dataList.get(position));
        if (clickListener != null) {
            holder.setClickListener(clickListener);
        }
        if (longClickListener != null) {
            holder.setLongClickListener(longClickListener);
        }
    }

    //省略一些代码
    ......

    private List<T> clone(List<T> dataList) {
        List<T> tempDataList = new ArrayList<>(dataList.size());
        for (T data : dataList) {
            tempDataList.add(clone(data));
        }
        return tempDataList;
    }

    /**
     * clone 指定对象,以此获得对象副本
     *
     * @param data 要复制的对象
     * @return 对象副本
     */
    protected abstract T clone(T data);

    /**
     * 判断数据列表刷新前后指定索引的位置是否指向同一条数据
     * 此处只对比两者是否指向同一条数据,而不关心其数据内容是否有变化
     *
     * @param oldItemPosition 更新前的数据索引
     * @param newItemPosition 更新后的数据索引
     * @return 是否指向同一条数据
     */
    protected abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);

    /**
     * 此处来判断指向同一条数据的前后两个索引位置,其数据内容是否相同
     * 只在 areItemsTheSame 返回 true 时才会调用本方法
     *
     * @param oldItemPosition 更新前的数据索引
     * @param newItemPosition 更新后的数据索引
     * @return 数据内容是否有变化
     */
    protected abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);

    /**
     * 获取同条数据在刷新前后是哪些数据内容发生了变化
     * 只在 areContentsTheSame 返回 false 时才会调用本方法
     *
     * @param oldItemPosition 更新前的数据索引
     * @param newItemPosition 更新后的数据索引
     * @return 数据变化内容
     */
    @NonNull
    protected abstract Bundle getChangePayload(int oldItemPosition, int newItemPosition);

    /**
     * 对刷新前后的数据进行定向更新,即只更新数据发生了变化的View
     *
     * @param holder Holder
     * @param bundle getChangePayload 方法的返回值
     */
    protected abstract void partialBindData(CommonRecyclerViewHolder holder, @NonNull Bundle bundle);

    /**
     * 对数据进行完全绑定
     *
     * @param holder Holder
     * @param data   Data
     */
    protected abstract void entirelyBindData(CommonRecyclerViewHolder holder, T data);

}

此处还需要使用到一个通用的 RecyclerView.ViewHolder

/**
 * 作者:叶应是叶
 * 时间:2017/12/21 21:52
 * 说明:通用RecyclerView ViewHolder
 */
public class CommonRecyclerViewHolder extends RecyclerView.ViewHolder {

    public interface OnClickListener {
        void onClick(int position);
    }

    public interface OnLongClickListener {
        void onLongClick(int position);
    }

    private OnClickListener clickListener;

    private OnLongClickListener longClickListener;

    //用来存放View以减少findViewById的次数
    private SparseArray<View> viewSparseArray;

    CommonRecyclerViewHolder(View view) {
        super(view);
        viewSparseArray = new SparseArray<>();
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (clickListener != null) {
                    clickListener.onClick(getAdapterPosition());
                }
            }
        });
        view.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (longClickListener != null) {
                    longClickListener.onLongClick(getAdapterPosition());
                }
                return true;
            }
        });
    }

    void setClickListener(OnClickListener clickListener) {
        this.clickListener = clickListener;
    }

    void setLongClickListener(OnLongClickListener longClickListener) {
        this.longClickListener = longClickListener;
    }

    /**
     * 根据 ID 来获取 View
     *
     * @param viewId viewID
     * @param <T>    泛型
     * @return 将结果强转为 View 或 View 的子类型
     */
    private <T extends View> T getView(@IdRes int viewId) {
        // 先从缓存中找,找到的话则直接返回
        // 如果找不到则findViewById,再把结果存入缓存中
        View view = viewSparseArray.get(viewId);
        if (view == null) {
            view = itemView.findViewById(viewId);
            if (view != null) {
                viewSparseArray.put(viewId, view);
            }
        }
        return (T) view;
    }

    public CommonRecyclerViewHolder setText(@IdRes int viewId, CharSequence text) {
        TextView textView = getView(viewId);
        if (textView != null) {
            textView.setText(text);
        }
        return this;
    }

    //省略一些代码
    ......
    
}

二、通用的单布局 RecyclerView.Adapter 使用示例

先来新建一个Model

/**
 * 作者:叶应是叶
 * 时间:2017/12/21 21:55
 * 说明:
 */
public class New {

    private int index;

    private String title;

    private String content;

    public New(int index, String title, String content) {
        this.index = index;
        this.title = title;
        this.content = content;
    }

    //省略一些代码
    ......

}

每个子项的布局包含一个索引TextView、一个标题TextView、一个内容TextView

<?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="#439af1"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_index"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="6dp"
        android:textSize="25sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:padding="5dp"
        android:textSize="28sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="start"
        android:padding="10dp"
        android:textSize="22sp" />

</LinearLayout>

之后就是继承 CommonRecyclerViewAdapter ,泛型指定为 New ,在构造函数里直接指定要使用的布局为 R.layout.item_new,再实现几个抽象方法即可。

/**
 * 作者:叶应是叶
 * 时间:2017/12/21 21:55
 * 说明:单个布局
 */
public class NewCommonAdapter extends CommonRecyclerViewAdapter<New> {

    public NewCommonAdapter(Context context, List<New> dataList) {
        super(context, dataList, R.layout.item_new);
    }

    @Override
    protected New clone(New data) {
        return new New(data.getIndex(), data.getTitle(), data.getContent());
    }

    @Override
    protected boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return dataList.get(oldItemPosition).getIndex() == newDataList.get(newItemPosition).getIndex();
    }

    @Override
    protected boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        String title = dataList.get(oldItemPosition).getTitle();
        String content = dataList.get(oldItemPosition).getContent();
        String newTitle = newDataList.get(newItemPosition).getTitle();
        String newContent = newDataList.get(newItemPosition).getContent();
        return title.equals(newTitle) && content.equals(newContent);
    }

    @NonNull
    @Override
    protected Bundle getChangePayload(int oldItemPosition, int newItemPosition) {
        Bundle bundle = new Bundle();
        String title = dataList.get(oldItemPosition).getTitle();
        String content = dataList.get(oldItemPosition).getContent();
        String newTitle = newDataList.get(newItemPosition).getTitle();
        String newContent = newDataList.get(newItemPosition).getContent();
        if (!title.equals(newTitle)) {
            bundle.putString("Title", newTitle);
        }
        if (!content.equals(newContent)) {
            bundle.putString("Content", newContent);
        }
        return bundle;
    }

    @Override
    protected void partialBindData(CommonRecyclerViewHolder holder, @NonNull Bundle bundle) {
        for (String key : bundle.keySet()) {
            switch (key) {
                case "Title":
                    holder.setText(R.id.tv_title, bundle.getString(key));
                    break;
                case "Content":
                    holder.setText(R.id.tv_content, bundle.getString(key));
                    break;
            }
        }
    }

    @Override
    protected void entirelyBindData(CommonRecyclerViewHolder holder, New data) {
        holder.setText(R.id.tv_title, data.getTitle())
                .setText(R.id.tv_content, data.getContent());
    }

}

这里省略 Activity 中对 RecyclerView 的各种初始化操作,只展现最终效果,具体代码看最下方GitHub地址。 效果图如下所示:

单布局.gif

三、通用的多布局 RecyclerView.Adapter 使用示例

在 CommonRecyclerViewAdapter 类当中,为了支持多布局,通过 MultiTypeSupport 接口来返回相应的布局文件ID,这里除了使用 单布局 中使用的 R.layout.item_new 布局文件外,同时使用另外一个布局文件 R.layout.item_new_multi ,仅仅是背景色不同而已

想要让子 Adapter 支持多布局,只要实现 CommonRecyclerViewAdapter 的另外一个构造函数即可。这里在索引为单数时使用 R.layout.item_new 布局,双数时使用 R.layout.item_new_multi 布局

/**
 * 作者:叶应是叶
 * 时间:2017/12/21 21:56
 * 说明:多布局
 */
public class NewCommonMultiAdapter extends CommonRecyclerViewAdapter<New> {

    public NewCommonMultiAdapter(Context context, List<New> dataList) {
        super(context, dataList, new CommonRecyclerViewAdapter.MultiTypeSupport<New>() {
            @Override
            public int getLayoutId(New item, int position) {
                return item.getIndex() % 2 == 0 ? R.layout.item_new_multi : R.layout.item_new;
            }
        });
    }
    
    //省略一些代码
    ......

    @Override
    protected void partialBindData(CommonRecyclerViewHolder holder, @NonNull Bundle bundle) {
        if (bundle.size() > 0) {
            int index = bundle.getInt("Index");
            for (String key : bundle.keySet()) {
                switch (key) {
                    case "Title":
                        if (index % 2 == 0) {
                            holder.setText(R.id.tv_multi_title, bundle.getString(key));
                        } else {
                            holder.setText(R.id.tv_title, bundle.getString(key));
                        }
                        break;
                    case "Content":
                        if (index % 2 == 0) {
                            holder.setText(R.id.tv_multi_content, bundle.getString(key));
                        } else {
                            holder.setText(R.id.tv_content, bundle.getString(key));
                        }
                        break;
                }
            }
        }
    }

    @Override
    protected void entirelyBindData(CommonRecyclerViewHolder holder, New data) {
        if (data.getIndex() % 2 == 0) {
            holder.setText(R.id.tv_multi_title, data.getTitle())
                    .setText(R.id.tv_multi_content, data.getContent())
                    .setText(R.id.tv_multi_index, String.valueOf(data.getIndex()));
        } else {
            holder.setText(R.id.tv_title, data.getTitle())
                    .setText(R.id.tv_content, data.getContent())
                    .setText(R.id.tv_index, String.valueOf(data.getIndex()));
        }
    }

}

实现的最终效果如下所示:

多布局.gif

四、带头部与底部View的 RecyclerView

为了实现带头部与底部View的 RecyclerView ,需要自定义一个Adapter来包裹实际的Adapter,重写 getItemViewType(int position) 方法,返回不同的值以区分头部View与底部View,并向外开放添加和移除头部底部View的方法

/**
 * 作者:叶应是叶
 * 时间:2017/12/21 21:53
 * 说明:可添加头部View与底部View的RecyclerView Adapter
 */
public class WrapRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private RecyclerView.Adapter<RecyclerView.ViewHolder> innerAdapter;

    private SparseArray<View> headerViewArray;

    private SparseArray<View> footerViewArray;

    //头部View类型开始位置,用于viewType
    private static int BASE_VIEW_TYPE_HEADER = 1000;

    //底部View类型开始位置,用于viewType
    private static int BASE_VIEW_TYPE_FOOTER = 2000;

    private RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {

        @Override
        public void onChanged() {
            super.onChanged();
            notifyDataSetChanged();
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            super.onItemRangeChanged(positionStart, itemCount);
            notifyItemRangeChanged(positionStart + getHeaderViewCount(), itemCount);
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            super.onItemRangeInserted(positionStart, itemCount);
            notifyItemRangeInserted(positionStart + getHeaderViewCount(), itemCount);
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            super.onItemRangeRemoved(positionStart, itemCount);
            notifyItemRangeRemoved(positionStart + getHeaderViewCount(), itemCount);
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            super.onItemRangeMoved(fromPosition, toPosition, itemCount);
            int headerViewsCountCount = getHeaderViewCount();
            notifyItemRangeChanged(fromPosition + headerViewsCountCount, toPosition + headerViewsCountCount + itemCount);
        }
    };

    public WrapRecyclerViewAdapter(RecyclerView.Adapter innerAdapter) {
        headerViewArray = new SparseArray<>();
        footerViewArray = new SparseArray<>();
        setAdapter(innerAdapter);
    }

    private void setAdapter(RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
        innerAdapter = adapter;
        innerAdapter.registerAdapterDataObserver(dataObserver);
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeaderPosition(position)) {
            return headerViewArray.keyAt(position);
        }
        if (isFooterPosition(position)) {
            return footerViewArray.keyAt(position - headerViewArray.size() - getDataItemCount());
        }
        return innerAdapter.getItemViewType(position - headerViewArray.size());
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (isHeaderView(viewType)) {
            return new ViewHolder(headerViewArray.get(viewType));
        }
        if (isFooterView(viewType)) {
            return new ViewHolder(footerViewArray.get(viewType));
        }
        return innerAdapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        int headerViewsCountCount = getHeaderViewCount();
        if (position >= headerViewsCountCount && position < headerViewsCountCount + innerAdapter.getItemCount()) {
            innerAdapter.onBindViewHolder(holder, position - headerViewsCountCount);
        }
    }

    @Override
    public int getItemCount() {
        return getHeaderViewCount() + getDataItemCount() + getFooterViewCount();
    }

    private int getDataItemCount() {
        return innerAdapter == null ? 0 : innerAdapter.getItemCount();
    }

    public int getHeaderViewCount() {
        return headerViewArray.size();
    }

    public int getFooterViewCount() {
        return footerViewArray.size();
    }

    public RecyclerView.Adapter getInnerAdapter() {
        return innerAdapter;
    }

    /**
     * 判断是否是头部View
     *
     * @param viewType ViewType
     * @return 是否是头部View
     */
    private boolean isHeaderView(int viewType) {
        return headerViewArray.indexOfKey(viewType) > -1;
    }

    /**
     * 判断是否是底部View
     *
     * @param viewType ViewType
     * @return 是否是底部View
     */
    private boolean isFooterView(int viewType) {
        return footerViewArray.indexOfKey(viewType) > -1;
    }

    /**
     * 判断是否是头部View
     *
     * @param view View
     * @return 是否是头部View
     */
    public boolean isHeaderView(View view) {
        return headerViewArray.indexOfValue(view) > -1;
    }

    /**
     * 判断是否是底部View
     *
     * @param view View
     * @return 是否是底部View
     */
    public boolean isFooterView(View view) {
        return footerViewArray.indexOfValue(view) > -1;
    }

    /**
     * 根据索引判断该位置的View是否是头部View
     *
     * @param position 索引
     * @return 是否是头部View
     */
    private boolean isHeaderPosition(int position) {
        return position >= 0 && position < getHeaderViewCount();
    }

    /**
     * 根据索引判断该位置的View是否是底部View
     *
     * @param position 索引
     * @return 是否是底部View
     */
    private boolean isFooterPosition(int position) {
        return position >= (getHeaderViewCount() + getDataItemCount())
                && position < (getHeaderViewCount() + getDataItemCount() + getFooterViewCount());
    }

    /**
     * 添加头部View
     *
     * @param view 头部View
     */
    public void addHeaderView(View view) {
        if (headerViewArray.indexOfValue(view) < 0) {
            headerViewArray.put(BASE_VIEW_TYPE_HEADER++, view);
            notifyItemInserted(headerViewArray.size() - 1);
        }
    }

    /**
     * 添加底部View
     *
     * @param view 底部View
     */
    public void addFooterView(View view) {
        if (footerViewArray.indexOfValue(view) < 0) {
            footerViewArray.put(BASE_VIEW_TYPE_FOOTER++, view);
            notifyItemInserted(getHeaderViewCount() + getDataItemCount() + getFooterViewCount() - 1);
        }
    }

    /**
     * 移除头部View
     *
     * @param view View
     */
    public void removeHeaderView(View view) {
        int index = headerViewArray.indexOfValue(view);
        if (index > -1) {
            headerViewArray.removeAt(index);
            notifyItemRemoved(index);
        }
    }

    /**
     * 移除底部View
     *
     * @param view View
     */
    public void removeFooterView(View view) {
        int index = footerViewArray.indexOfValue(view);
        if (index > -1) {
            footerViewArray.removeAt(index);
            notifyItemRemoved(getHeaderViewCount() + getDataItemCount() + index);
        }
    }

    private class ViewHolder extends RecyclerView.ViewHolder {

        ViewHolder(View itemView) {
            super(itemView);
        }

    }

}

WrapRecyclerViewAdapter 的使用方法也很简单,只要将实际的Adapter作为参数来构造 WrapRecyclerViewAdapter 对象,然后再传给 RecyclerView 即可,之后就可以通过 addHeaderView 和 addFooterView 方法来添加头部和底部View

        RecyclerView rv_wrapDataList = (RecyclerView) findViewById(R.id.rv_wrapDataList);
        NewCommonAdapter adapter = new NewCommonAdapter(this, newList);
        WrapRecyclerViewAdapter wrapRecyclerViewAdapter = new WrapRecyclerViewAdapter(adapter);
        rv_wrapDataList.setLayoutManager(new LinearLayoutManager(this));
        rv_wrapDataList.setAdapter(wrapRecyclerViewAdapter);

使用效果如下所示:

带头部和底部View.gif

五、RecyclerView 的单击和长按事件监听

RecyclerView 的单击和长按事件一直是一个比较麻烦的地方,毕竟没有官方提供的接口,不过此处 CommonRecyclerViewAdapter 也已经提供了相应的设置方法

        NewCommonMultiAdapter newCommonMultiAdapter = new NewCommonMultiAdapter(this, newList);
        newCommonMultiAdapter.setClickListener(new CommonRecyclerViewHolder.OnClickListener() {
            @Override
            public void onClick(int position) {
                toast("单击" + "\n" + newList.get(position).getTitle() + "\n" + newList.get(position).getContent());
            }
        });
        newCommonMultiAdapter.setLongClickListener(new CommonRecyclerViewHolder.OnLongClickListener() {
            @Override
            public void onLongClick(int position) {
                toast("长按" + "\n" + newList.get(position).getTitle() + "\n" + newList.get(position).getContent());
            }
        });

点击事件.gif

六、通用的 RecyclerView.ItemDecoration

想要实现Item之间的分割线,需要继承 RecyclerView.ItemDecoration 在相应的位置进行绘制,这里提供一个通用的分隔线

/**
 * 作者:叶应是叶
 * 时间:2017/12/21 21:52
 * 说明:通用分隔线
 */
public class CommonItemDecoration extends RecyclerView.ItemDecoration{

    private int orientation = LinearLayoutManager.HORIZONTAL;

    private Drawable drawable;

    public CommonItemDecoration(Context context, int orientation) {
        this.orientation = orientation;
        int[] attrs = new int[]{android.R.attr.listDivider};
        TypedArray typedArray = context.obtainStyledAttributes(attrs);
        drawable = typedArray.getDrawable(0);
        typedArray.recycle();
    }

    public CommonItemDecoration(Drawable drawable, int orientation) {
        this.drawable = drawable;
        this.orientation = orientation;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        if (orientation == LinearLayoutManager.HORIZONTAL) {
            outRect.set(0, 0, drawable.getIntrinsicWidth(), 0);
        } else if (orientation == LinearLayoutManager.VERTICAL) {
            outRect.set(0, 0, 0, drawable.getIntrinsicHeight());
        }
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        if (orientation == LinearLayoutManager.HORIZONTAL) {
            drawVerticalDivider(c, parent);
        } else if (orientation == LinearLayoutManager.VERTICAL) {
            drawHorizontalDivider(c, parent);
        }
    }

    private void drawVerticalDivider(Canvas c, RecyclerView parent) {
        for (int i = 0; i < parent.getChildCount(); i++) {
            View child = parent.getChildAt(i);
//            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
//            //受 child layout_marginEnd 属性的影响
//            int left = child.getRight() + params.rightMargin;
            //不受 child layout_marginEnd 属性的影响,会直接绘制在 child 右侧
            int left = child.getRight();
            int top = child.getTop();
            int right = left + drawable.getIntrinsicWidth();
            int bottom = child.getBottom();
            drawable.setBounds(left, top, right, bottom);
            drawable.draw(c);
        }
    }

    private void drawHorizontalDivider(Canvas c, RecyclerView parent) {
        for (int i = 0; i < parent.getChildCount(); i++) {
            View child = parent.getChildAt(i);
            int left = child.getLeft();
            //不受 child layout_marginBottom 属性的影响,会直接绘制在 child 底部
            int top = child.getBottom();
            int right = child.getRight();
            int bottom = top + drawable.getIntrinsicHeight();
            //会受 child layout_marginBottom 属性的影响
            //RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            //int top = child.getBottom() + params.bottomMargin;
            drawable.setBounds(left, top, right, bottom);
            drawable.draw(c);
        }
    }

}

在之前的几张效果图中,其实是已经为RecyclerView添加了一条白色分隔线的,

RecyclerView rv_commonMultiDataList = (RecyclerView) findViewById(R.id.rv_commonMultiDataList);
CommonItemDecoration commonItemDecoration = new CommonItemDecoration(ContextCompat.getDrawable(this, R.drawable.divider), LinearLayoutManager.VERTICAL);
        rv_commonMultiDataList.addItemDecoration(commonItemDecoration);

这里也可以传入不同的 Drawable 对象,从而实现多种风格的分隔线

分隔线.gif

七、SnapHelper的使用

顺带在这里对 SnapHelper 进行一个简单的介绍,SnapHelper 是在 Android 24.2.0 的support 包中新添加的一个支持库,是对RecyclerView的拓展。SnapHelper旨在支持RecyclerView的对齐方式,通过计算对齐RecyclerView中TargetView 的指定点或者容器中的任何像素点,可以使RecyclerView实现类似于 ViewPager 的切换效果

SnapHelper是一个抽象类,官方提供了 LinearSnapHelper 和 PagerSnapHelper 两个具体实现,这里来实现类似于第一次使用App时显示的引导页的效果

/**
 * 作者:叶应是叶
 * 时间:2017/12/21 22:02
 * 说明:
 */
public class SnapRecyclerViewActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_snap_recycler_view);
        RecyclerView rv_snap = (RecyclerView) findViewById(R.id.rv_snap);
        SnapAdapter snapAdapter = new SnapAdapter(this, getData());
        rv_snap.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
//        rv_snap.setLayoutManager(new LinearLayoutManager(this));
        rv_snap.setAdapter(snapAdapter);
//        LinearSnapHelper snapHelper = new LinearSnapHelper();
//        snapHelper.attachToRecyclerView(rv_snap);
        PagerSnapHelper pagerSnapHelper=new PagerSnapHelper();
        pagerSnapHelper.attachToRecyclerView(rv_snap);
    }

    private List<Image> getData() {
        List<Image> imageList = new ArrayList<>();
        imageList.add(new Image(0, R.drawable.drawable_1));
        imageList.add(new Image(1, R.drawable.drawable_0));
        imageList.add(new Image(2, R.drawable.drawable_1));
        imageList.add(new Image(3, R.drawable.drawable_0));
        imageList.add(new Image(4, R.drawable.drawable_1));
        imageList.add(new Image(5, R.drawable.drawable_0));
        imageList.add(new Image(6, R.drawable.drawable_1));
        imageList.add(new Image(7, R.drawable.drawable_0));
        return imageList;
    }

}

实现的效果如下所示:

Snap.gif

源代码我也已经放到了GitHub上,点击查看:Android RecyclerView的简便写法

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 三方库源码笔记(9)-超详细的Glide源码详解

    Glide 的源码有点复杂,如果要细细展开来讲解,那么写个十篇文章也囊括不完??所以我就想着换个思路来看源码:以小点来划分,每个小点只包含 Glide 实现某个...

    叶志陈
  • 一步步封装实现自己的网络请求框架

    现如今 Android 领域流行的网络请求框架基本都是用 Retrofit 加 RxJava 来搭配构建的,而以 ViewModel + LiveData + ...

    叶志陈
  • 从源码看 Jetpack (2)-Lifecycle衍生

    上篇文章详细讲述了 Lifecycle 的整个事件分发逻辑,本篇文章再来介绍下 Lifecycle 的几个开发者比较容易忽略的衍生产物

    叶志陈
  • Android RecyclerView浅析(分类型)

    整体上看RecyclerView架构,提供了一种插拔式的体验,高度的解耦,异常的灵活,通过设置它提供的不同LayoutManager,ItemDecoratio...

    Vance大飞
  • Laravel5.1 框架数据库操作DB运行原生SQL的方法分析

    本文实例讲述了Laravel5.1 框架数据库操作DB运行原生SQL的方法。分享给大家供大家参考,具体如下:

    砸漏
  • Android BannerView通用封装详解

    之前封装过一个,但总觉得不够优雅,就有了这个通用封装,很简洁,不知道够不够优雅,不过原来那个有跟随指示器和丝滑滑动效果,感兴趣可以看一下。

    砸漏
  • netty案例,netty4.1中级拓展篇三《Netty传输Java对象》

    Netty在实际应用级开发中,有时候某些特定场景下会需要使用Java对象类型进行传输,但是如果使用Java本身序列化进行传输,那么对性能的损耗比较大。为此我们需...

    小傅哥
  • android嵌套滚动入门实践

    嵌套滚动是 Android OS 5.0之后,google 为我们提供的新特性。这种机制打破了我们对之前 Android 传统的事件处理的认知。从一定意义上可以...

    砸漏
  • 设计模式之桥接模式(结构型)

    桥接模式(Bridge Pattern)是将抽象部分和实现部分分离,使它们可以独立地改变,是一种对象结构型模式。

    SmileNicky
  • 用最简单的例子说明设计模式(二)之模版方法、策略模式、组合模式、观察者模式

    六月的雨

扫码关注云+社区

领取腾讯云代金券