RecyclerView的简单封装记录

RecyclerView的简单封装

  • 前言
  • 1. RecyclerView任务分工
  • 2. 封装过程
    • 2.1 Adapter封装
    • 2.2 ViewHolder封装
    • 2.3 RecyclerView封装
  • 3. 使用例

前言

难得有机会做一次控件的封装,于是将开发中用的最多的RecyclerView封装了一次。顺便写一个文记录一下封装的过程。

1. RecyclerView任务分工

在一般情况下,我们要使用一个RecyclerView,会一定会同时用到RecyclerView,RecyclerView.Adaper,ViewHolder三个类。这三个类在显示一个列表的时候,各自有不同的分工。

  • RecyclerView:RecyclerView负责整个视图的范围大小显示,以及通过layoutManager设置内部item的排列方式(横向还是纵向,每行显示一个还是多个)。
  • Adaper:Adapter类负责将RecyclerView和item进行绑定和连接,以及掌握着item视图的具体显示和刷新、设置item的监听事件、计算item的数量等操作。
  • ViewHolder:负责item中子控件的绑定。

这里就可以看出来,虽然三个类都会有用上,但是逻辑最多的还是Adapter类。

2. 封装过程

封装的目的是为了在调用的时候,可以通过比较少量的代码,就实现出原本的效果,为此进行封装。而刚才也说过,Adapter类是重点,所以先描述下对Adapter类如何进行封装。

2.1 Adapter封装

在封装之前,先统计一下Adapter类需要用上哪些方法和哪些些对象。

  1. 一个同时绑定父控件RecyclerView和子控件ViewHolder的onCreateViewHolder。
  2. 实现item具体布局的onBindViewHolder。
  3. 一个确定Recycler长度的getItemCount。
  4. 保存数据实体的List。

在这之中,1除了自带的ViewGroup参数和viewType参数以外,还有一个可变动的参数就是item的layout布局;2是主要操作item的显示,不能省略,3一般返回list.size()。

在思考完了这些内容之后,就可以对Adapter类进行第一步的封装了。

/**
 * @description:由于是封装,所以并不希望直接使用这个类,设置为抽象类。
 */
public abstract class BaseAdapter<T, B extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<BaseViewHolder> {
    private static final String TAG = "BaseAdapter";

    private LayoutInflater mLayoutInflater;
    private View mView;
    private List<T> mList; // 数据集合

    public BaseAdapter(){
        mList = new ArrayList<>();
    }

    public BaseAdapter(List<T> mList){
        this.mList = mList;
    }

    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        mLayoutInflater = LayoutInflater.from(parent.getContext());
        mView = mLayoutInflater.inflate(getLayoutResource(), parent, false); // 返回布局文件的id
        BaseViewHolder baseViewHolder = new BaseViewHolder(mView); 
        return baseViewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull BaseViewHolder vh, int position) {
        setItemView(vh, mList.get(position), position); // 做成抽象方法,让子类自己设置子布局
    }

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

    // 抽象方法,即继承类必须实现
    protected abstract int getLayoutResource(); //返回布局的Rid

    protected abstract void setItemView(BaseViewHolder vh, T t, int position); //设置子布局
}

由于封装还需要有一些比较方便的功能,所以也将这些功能套进去,封装就完成了。

  • item的按键点击功能
  • 对List的一些简单操作功能。
  • 设置选择其中一项的功能(有些时候总会有这种选定某一项item的需求)。
public abstract class BaseAdapter<T, B extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<BaseViewHolder> {
    private static final String TAG = "BaseAdapter";

    private LayoutInflater mLayoutInflater;
    private View mView;
    private List<T> mList; // 数据集合

    private OnItemClickListener onItemClickListener; //点击监听事件
    private OnItemLongClickListener onItemLongClickListener; //长按监听事件

    private int selectedItem; //选定的项

    public BaseAdapter(){
        mList = new ArrayList<>();
    }

    public BaseAdapter(List<T> mList){
        this.mList = mList;
    }

    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        mLayoutInflater = LayoutInflater.from(parent.getContext());
        mView = mLayoutInflater.inflate(getLayoutResource(), parent, false);
        BaseViewHolder baseViewHolder = new BaseViewHolder(mView);
        setClickListener(baseViewHolder);
        return baseViewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull BaseViewHolder vh, int position) {
        setItemView(vh, mList.get(position), position);
    }

    /**
     * 绑定监听事件
     * @param vh
     */
    protected void setClickListener(final BaseViewHolder vh){
        if (vh == null) return;
        View view = vh.itemView;
        if (view == null) return;

        if (onItemClickListener != null){
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    onItemClickListener.onItemClick(v, vh.getLayoutPosition());
                }
            });
        }

        if (onItemLongClickListener != null){
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    onItemLongClickListener.onItemLongClick(v, vh.getLayoutPosition());
                }
            });
        }

    }

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

    public void setSelectedItem(int position) {
        this.selectedItem = position;
        notifyDataSetChanged(); // 修改选定项之后,进行一次视图刷新
    }

    public boolean isSelected(int positon){
        return positon == selectedItem;
    }

    public T getSelectedItem() {
        return mList.get(selectedItem);
    }

    //把list的方法套个皮
    public void addAll(List<T> list){
        mList.addAll(list);
    }
    
    //把list的方法套个皮
    public void add(T t){
        mList.add(t);
    }

    //把list的方法套个皮
    public boolean remove(T t){
        return mList.remove(t);
    }

    //把list的方法套个皮
    public boolean removeAll(List<T> list){
        return mList.removeAll(list);
    }

	//把list的方法套个皮
    public void clear(){
        mList.clear();
    }
    
    //把list的方法套个皮
    public List<T> getmList() {
        return mList;
    }

    // 一些setter和getter方法
    public BaseAdapter setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
        return this;
    }

    public OnItemClickListener getOnItemClickListener() {
        return onItemClickListener;
    }

    public BaseAdapter setOnItemLongClickListener(OnItemLongClickListener onItemLongClickListener) {
        this.onItemLongClickListener = onItemLongClickListener;
        return this;
    }

    public OnItemLongClickListener getOnItemLongClickListener() {
        return onItemLongClickListener;
    }

    // 抽象方法,即继承类必须实现
    protected abstract int getLayoutResource(); //返回布局的Rid

    protected abstract void setItemView(BaseViewHolder vh, T t, int position); //设置子布局

    // 接口,短点击和长点击按钮接口
    public interface OnItemClickListener{
        void onItemClick(View v, int position);
    }

    interface OnItemLongClickListener{
        void onItemLongClick(View v, int position);
    }
}

2.2 ViewHolder封装

我们减少ViewHolder的任务,让它只做View的绑定,最终ViewHodler长这样。

public class BaseViewHolder extends RecyclerView.ViewHolder{

    private View mView; //保存View

    public BaseViewHolder(@NonNull View itemView) {
        super(itemView);
        mView = itemView;
    }

    /**
     * 绑定资源文件的id,但是要做强制类型转换
     * @param Rid
     * @return
     */
    public View setView(int Rid){
        return mView.findViewById(Rid);
    }

}

2.3 RecyclerView封装

RecyclerView主要做adapter的绑定和item的排列方式,姑且也做一个简易的封装。

public class BaseRecyclerView extends RecyclerView {

    private static final String TAG = "BaseRecyclerView";

    private int columns; // 列数或者行数
    private RecyclerMode recyclerMode; // 设置模式

    public BaseRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public BaseRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public BaseRecyclerView setMode(RecyclerMode mode){
        return setMode(mode, 1);
    }

    public BaseRecyclerView setMode(RecyclerMode mode, int columns){
        recyclerMode = mode;
        if (columns < 1) columns = 1;
        this.columns =columns;
        switch (mode){
            case LINEAR_VERTICAL:
                LinearLayoutManager manager = new LinearLayoutManager(getContext());
                manager.setOrientation(LinearLayoutManager.VERTICAL);
                setLayoutManager(manager);
                break;
            case LINEAR_HORIZONTAL:
                LinearLayoutManager manager1 = new LinearLayoutManager(getContext());
                manager1.setOrientation(LinearLayoutManager.HORIZONTAL);
                setLayoutManager(manager1);
                break;
            case GRID_VERTICAL:
                GridLayoutManager manager2 = new GridLayoutManager(getContext(), columns);
                manager2.setOrientation(VERTICAL);
                setLayoutManager(manager2);
                break;
            case GRID_HORIZONTAL:
                GridLayoutManager manager3 = new GridLayoutManager(getContext(), columns);
                manager3.setOrientation(HORIZONTAL);
                setLayoutManager(manager3);
                break;

        }
        return this;
    }
}
public enum RecyclerMode{
    LINEAR_VERTICAL, LINEAR_HORIZONTAL, GRID_VERTICAL, GRID_HORIZONTAL
}

但是我个人在这里碰到了一个新的需求:内部每个item的间距显示。

我个人认为item间距这种事情是不应该写在item的布局文件中的,所以我打算通过代码的方式来设置每个item的间距。

接下来的内容都以竖直排列Vertical来说明:

  1. LinearLayout设置间距的方式很容易,因为LinearLayout的情况下,item的width绝大部分是match_parent的。
  2. 重点是GridLayout(网格布局),也就是一行显示多个item的情况。我个人不喜欢在这种时候将item的宽度设置为match_parent,即使我知道这样设置之后,设置水平方向上的间距比较容易。
    所以我的代码的间距是以item的宽度为固定值为基础的,最终实现的效果是当使用GridLayoutManager情况时,水平方向上左右的item贴边,中间的按比例分布,竖直方向上按照传入的参数设置。
  3. 关于我是如何用代码设置水平间距的,可以参考这篇:RecyclerView中用GridLayoutManager时如何正确的设置内部控件的间距
public class BaseRecyclerView extends RecyclerView {

	.......省略之前出现过的代码

	private int width;
    private int height;

    /**
     * 设置RecyclerView的内部间距
     * @param space 内部的间距(单位是dp)
     */
	public BaseRecyclerView setInnerMargin(int space){
        space = PixelUtil.dipToPx(space, getContext());
        final int finalSapce = space;
        post(new Runnable() {
            @Override
            public void run() {
                getViewWidthAndHeight();
                int itemWidth = 0;
                int itemHeight = 0;
                try {
                    itemWidth = getLayoutManager().getChildAt(0).getWidth();
                    itemHeight = getLayoutManager().getChildAt(0).getHeight();
                }catch (NullPointerException e){
                    Log.e(TAG,"you need to set adapter and item before set innerMargin");
                    Log.e(TAG,"you need to set adapter and item before set innerMargin");
                }

                addItemDecoration(new InnerItemDecoration(finalSapce, height, width,
                        itemHeight, itemWidth, columns, recyclerMode));
            }
        });

        return this;
    }

    private void getViewWidthAndHeight(){
        width = getMeasuredWidth();
        height = getMeasuredHeight();
    }
}

class PixelUtil {
    /**
     * 像素转换,dp转px
     * @param dp dp
     * @param context 上下文
     * @return px
     */
    public static int dipToPx(int dp, Context context){
        float scale = context.getResources().getDisplayMetrics().density;
        return (int)(dp * scale + 0.5f);
    }
}


class InnerItemDecoration extends RecyclerView.ItemDecoration{

    private static final String TAG = "InnerItemDecoration";

    private int space; // 单位是px不是dp
    private int height;
    private int width;
    private int itemHeight;
    private int itemWidth;

    private int columns; // 列数
    private RecyclerMode mode;


    public InnerItemDecoration(int space, int height, int width, int itemHeight, int itemWidth, int columns, RecyclerMode recyclerMode){
        this.space = space;
        this.columns = columns;
        this.mode = recyclerMode;
        this.width = width;
        this.height = height;
        this.itemHeight = itemHeight;
        this.itemWidth = itemWidth;
    }

    /*
    这一段逻辑要着重处理
    1. 设置内部每个item的margin
    2. 在1的基础上还要增加对Grid模式的适配

    对于Grid模式而言
    GRID_VERTICAL的话,竖直自定义,水平贴边均分
    1. 具体做法,获取RecyclerView的宽度,让左右两边的贴边,再设置中间元素的left
     */
    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        int position = parent.getChildAdapterPosition(view);
        int totalCount = parent.getAdapter().getItemCount();

        setOffset(outRect, position, totalCount);
    }

    private void setOffset(Rect outRect, int position, int totalCount) {
        switch (mode){
            case GRID_HORIZONTAL:
            case LINEAR_HORIZONTAL:
                setOffsetHorizontal(outRect, position, totalCount);
                break;
            case GRID_VERTICAL:
            case LINEAR_VERTICAL:
                setOffsetVertical(outRect, position, totalCount);
                break;
        }

    }

    // Vertical,不包括边缘
    private void setOffsetVertical(Rect outRect, int position, int totalCount) {

        outRect.top = space/2;
        outRect.bottom = space/2;

        double w = ((double)width - itemWidth * columns) / (columns * (columns-1));
        int p = position % columns;
        outRect.left = (int)(w*p);

        // 最上面的一排(columns个item)
        if (position < columns)
            outRect.top = 0;
        // 最下面一排
        int tem = (int) (Math.ceil((double) totalCount/columns) * columns);
        if (position >= tem-columns)
            outRect.bottom = 0;

    }

    // Horizontal
    private void setOffsetHorizontal(Rect outRect, int position, int totalCount) {
        outRect.left = space /2;
        outRect.right = space /2;

        double w = ((double)height - itemHeight * columns) / (columns * (columns-1));
        int p = position % columns;
        outRect.top = (int)(w*p);

        // 最左边的一排(columns个item)
        if (position < columns)
            outRect.left = 0;
        // 最右边一排
        int tem = (int) (Math.ceil((double) totalCount/columns) * columns);
        if (position >= tem-columns)
            outRect.right = 0;
    }

}

3. 使用例

封装完之后就是如何使用了,用起来还是很方便的,Adapter类只要实现两个方法。

// String是数据实体
public class MyAdapter extends BaseAdapter<String, BaseViewHolder> {

    @Override
    protected int getLayoutResource() {
        return R.layout.item_string;
    }

    @Override
    protected void setItemView(BaseViewHolder baseViewHolder, String s, int position) {
        ((TextView)baseViewHolder.setView(R.id.tv)).setText(s); // 实现具体的界面逻辑
    }
}

public class MainActivity extends AppCompatActivity {

    BaseRecyclerView recyclerView;
    MyAdapter adapter = new MyAdapter(); // 创建实例的时候直接用无参构造函数就行

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recyclerView = findViewById(R.id.recyclerView);

        List<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");

        adapter.addAll(list);
        adapter.setOnItemClickListener(new BaseAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(View v, int position) {
                Toast.makeText(getApplicationContext(), adapter.getmList().get(position),Toast.LENGTH_SHORT).show();
            }
        });

        recyclerView.setMode(RecyclerMode.GRID_VERTICAL, 3);
        recyclerView.setAdapter(adapter);
        recyclerView.setInnerMargin(10); //调用这行之前要保证已经setAdapter,且adapter内有item

    }
}

你可能感兴趣的:(实际问题,Android)