实现电商项目购物车的效果(类似京东商城)

前言部分

电商一直是一个比较热门的项目,以前没有接触过最近正好事情不多,所以决定学习一个电商的项目,零散的做了一些基础的准备工作,比如:首页的多类型列表,分类页面,购物车页面。

花了几天的时间做了这些东西,由于第一次做走了一些弯路,这里记录一下,防止以后同样的问题。

这里放出一些截图,如果后续时间上还允许的话计划把这个项目做的完整一些,争取能做到放出来给大家参考的程度。
图片1
实现电商项目购物车的效果(类似京东商城)
图片2
实现电商项目购物车的效果(类似京东商城)
图片3
实现电商项目购物车的效果(类似京东商城)

这里看一下购物车的演示效果:
实现电商项目购物车的效果(类似京东商城)

这里主要介绍购物车的效果实现,主要内容是一些商品选择联动等。不涉及很具体的业务,因为我还没有真实数据,所以只是做一些演示效果。写这种联动很多模块一定要细心谨慎,否则会遇到很多逻辑上的漏洞。

内容部分

问题分析:

  1. 购物车由什么组成? 列表+商品+结算部分
  2. 相互之间的关系? 主要是商品和店铺的关系

参考如上问题,我们开始着手代码。

首先常规开始布局准备工作,RecyclerView肯定是首选了,下面看一下页面布局,这里需要介绍一个知识是悬浮头部的实现,因为以前我自己定义过ItemDecoration,所以我也是打算定义ItemDecoration来做的,虽然原理也基本上都是知道的,但是写起来确实还是略复杂,这时候想应该还是有其他的方案,搜索后查询到另外一个方案,觉得很好用,虽然这个大约只能支持线性列表,不过如果自己修改一下其他应该也能支持 原文链接

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorAccent"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rlv_cart_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <LinearLayout
            android:id="@+id/ll_cart_list_stick_header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/grgray"
            android:orientation="horizontal"
            android:visibility="visible">

            <CheckBox
                android:id="@+id/cb_cart_select_goods"
                android:layout_width="40dp"
                android:layout_height="40dp" />

            <TextView
                android:id="@+id/tv_store_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="我的小店" />
        </LinearLayout>

    </FrameLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_alignParentBottom="true"
        android:background="@color/grgray"
        android:orientation="horizontal"
        android:visibility="visible">

        <CheckBox
            android:id="@+id/cb_cart_select_all_goods"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:gravity="center" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="全选" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_weight="1"
            android:text="合计多少钱" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="10dp"
            android:background="@color/app_theme_color"
            android:gravity="center"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:text="去结算" />
    </LinearLayout>
</RelativeLayout>

主要布局就上面这样了,下面介绍主要的逻辑,因为商品需要选中,所以条目中肯定是需要使用checkbox了,列表中使用checkbox也是一个老生常谈的问题,条目的复用会导致选中商品的错乱,采用的是常规的处理方式,将条目选中状态保存列表,在显示列表商品数据的时候选中的状态直接在我们存储的列表中显示。

这里我存储状态的列表就简单的存储在内存中了,实际需求一般都是存储到后台的,因为购物车的内容是要跟着用户来走的。

根据数据结构分为几个步骤来实现,下面依次介绍一下

  1. 需要先初始化一个状态的列表出来,应该和我们的数据列表是一一对应的。

     //初始化
            cartGoodsBeanList.clear();
            for (int i = 0; i < goodsCartResList.size(); i++) {
                GoodsCartRes goodsCartRes = goodsCartResList.get(i);
                CartGoodsBean cartGoodsBean = new CartGoodsBean();
                cartGoodsBean.setCheck(false);
                List<CartGoodsInnerBean> cartGoodsInnerBeanList = new ArrayList<>();
                for (GoodsCartRes.GoodsBean goodsBean : goodsCartRes.getGoods()) {
                    CartGoodsInnerBean cartGoodsInnerBean = new CartGoodsInnerBean();
                    cartGoodsInnerBean.setCheck(false);
                    cartGoodsInnerBeanList.add(cartGoodsInnerBean);
                }
                cartGoodsBean.setCartGoodsInnerBeanList(cartGoodsInnerBeanList);
                cartGoodsBeanList.put(goodsCartRes.getStore(), cartGoodsBean);
            }
    
  2. 开始初始化数据到adapter中,这里我使用了开源库

        implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.40'
    

    其实自己写也没什么问题,下面是RecyclerView常规三步走。

    cartAdapter = new CartAdapter(R.layout.item_cart_list, goodsCartResList);
            linearLayoutManager = new LinearLayoutManager(getActivity());
            rlvCartContent.setLayoutManager(linearLayoutManager);
            rlvCartContent.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
            rlvCartContent.setAdapter(cartAdapter);
    

    这里比较核心的内容都在CartAdapter中,主要是店铺很店铺下商品的关联逻辑,我写完后觉得写的很啰嗦,但是我还没有想到其他更好的方式,这里写成博客也是希望如果有人有更好方案可以提供给我,大家共同学习。CartAdapter代码如下所示:

    /**
     * @Author : dongfang
     * @Created Time : 2018-12-19  13:59
     * @Description:
     */
    public class CartAdapter extends BaseQuickAdapter<GoodsCartRes, BaseViewHolder> {
    
        private CartListGoodsAdapter adapter;
        private final ArrayList<CartListGoodsAdapter> cartListGoodsAdapterArrayList;
        private CartListGoodsAdapter cartListGoodsAdapter;
        List<GoodsCartRes> data;
    
    
        public CartAdapter(int layoutResId, @Nullable List<GoodsCartRes> data) {
            super(layoutResId, data);
            this.data = data;
            //这里我是因为用了嵌套RecyclerView,所以把条目中的adapter存储到集合中。
            cartListGoodsAdapterArrayList = new ArrayList<>();
            for (int i = 0; i < data.size(); i++) {
                adapter = new CartListGoodsAdapter(R.layout.item_cart_goods_detail, data.get(i).getGoods());
                cartListGoodsAdapterArrayList.add(adapter);
            }
    
        }
    
        @Override
        protected void convert(final BaseViewHolder helper, final GoodsCartRes item) {
            helper.setText(R.id.tv_item_cart_title, item.getStore());
            CartGoodsBean cartGoodsBean = ShoppingCartFragment.cartGoodsBeanList.get(item.getStore());
            //选择状态在列表中取出
            helper.setChecked(R.id.cb_cart_select_goods, cartGoodsBean.isCheck());
            helper.addOnClickListener(R.id.cb_cart_select_goods);
            int adapterPosition = helper.getAdapterPosition();
    		//条目中的列表
            RecyclerView rlvItemClassifyContent = helper.getView(R.id.rlv_cart_item_goods);
            rlvItemClassifyContent.setLayoutManager(new LinearLayoutManager(mContext));
            cartListGoodsAdapter = cartListGoodsAdapterArrayList.get(adapterPosition);
            rlvItemClassifyContent.setAdapter(cartListGoodsAdapter);
            //条目中的RecyclerView中的条目中的checkbox点击事件,这里提一下如果你的checkbox不光通过点击变化,也要接受代码变化,就不要使用setOnCheckedChangeListener监听。
            cartListGoodsAdapter.setOnItemChildClickListener(new OnItemChildClickListener() {
                @Override
                public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
                    //这里是拿到CartAdapter条目的状态类,以为改变内部的checkbox可能会导致店铺上的checkbox变化
                    CartGoodsBean cartGoodsBean = ShoppingCartFragment.cartGoodsBeanList.get(item.getStore());
                    int currentPosition = 0;
                    for (int i = 0; i < data.size(); i++) {
                        if (data.get(i).getStore() == item.getStore()) {
                            currentPosition = i;
                        }
                    }
    
                    List<CartGoodsInnerBean> cartGoodsInnerBeanList = cartGoodsBean.getCartGoodsInnerBeanList();
                    //点击为false取消全选,否则全选
                    //当前点击的条目
                    CartGoodsInnerBean cartGoodsInnerBean = cartGoodsInnerBeanList.get(position);
                    //更新新的选择状态
                    cartGoodsInnerBean.setCheck(!cartGoodsInnerBean.isCheck());
    
                    if (!cartGoodsInnerBean.isCheck()) {
                        cartGoodsBean.setCheck(false);
                        CartAdapter.this.notifyItemChanged(currentPosition);
                        if (listenerCheckChange!=null) {
                            listenerCheckChange.changePostion(false);
                        }
                    } else {
                        for (int i = 0; i < cartGoodsInnerBeanList.size(); i++) {
                            if (position != i && !cartGoodsInnerBeanList.get(i).isCheck()) {
                                return;
                            }
    
                        }
                        cartGoodsBean.setCheck(true);
                        CartAdapter.this.notifyItemChanged(currentPosition);
                        if (listenerCheckChange!=null) {
                            listenerCheckChange.changePostion(true);
                        }
                    }
                    //更新单条数据
    
                }
            });
        }
    
        public void notifyDataSetChangedForChild(int mCurrentPosition) {
            cartListGoodsAdapter = cartListGoodsAdapterArrayList.get(mCurrentPosition);
            cartListGoodsAdapter.notifyDataSetChanged();
    
        }
    
        private ListenerCheckChange listenerCheckChange;
    
        public void setListenerCheckChange(ListenerCheckChange listenerCheckChange) {
            this.listenerCheckChange = listenerCheckChange;
        }
    
        public interface ListenerCheckChange {
            void changePostion(boolean isCheck);
        }
    
    }
    
    

    这里详细说一下上面逻辑:

    1. 在CartAdapter中我们注册CartListGoodsAdapter的checkbox的点击事件,当发生点击的时候我们首先更新商品的选中状态,然后进一步判断店铺的选中状态,然后在判断悬浮头的选中状态。
    2. 当同一店铺下所有商品被选中后,店铺的选中按钮需要变成选中状态,取消其中一个的选中状态,店铺上的状态就要取消。
    3. 点击店铺上的checkbox的时候,直接操作店铺下所有的商品选择状态变更。
    4. 当我们显示的头布局上悬浮的布局我们需要单独去更新头布局上checkbox,这里我是通过一个callback来做的。
  3. 下面看一下主布局中的使用,包括一些监听器的设置工作。下面代码上店铺上的checkbox的监听设置:

    cartAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
                @Override
                public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
                    //这部分上改变点不选中状态
                    String store = goodsCartResList.get(position).getStore();
                    CartGoodsBean cartGoodsBean = cartGoodsBeanList.get(store);
                    LogUtils.i("CartAdapter1--" + cartGoodsBean.isCheck() + position);
                    boolean b = !cartGoodsBean.isCheck();
                    cartGoodsBean.setCheck(b);
                    LogUtils.i("CartAdapter2--" + cartGoodsBean.isCheck() + position);
    
                    List<CartGoodsInnerBean> cartGoodsInnerBeanList = cartGoodsBean.getCartGoodsInnerBeanList();
                    //这个方法上改变店铺下商品的选中状态
                    CheckAllOrNo(cartGoodsInnerBeanList, cartGoodsBean.isCheck());
                    if (adapter instanceof CartAdapter) {
                        //列表更新一下
                        adapter.notifyDataSetChanged();
                        ((CartAdapter) adapter).notifyDataSetChangedForChild(position);
                    }
                }
            });
    

    以为使用了点击事件,所以checkbox的选中状态需要自己去判断一下,这里也很简单就上反选就好了。

  4. 这是悬浮的头的checkbox的选择监听器,代码如下:

    cbCartSelectGoods.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //拿到当前悬浮头控制的是哪一个条目,然后做处理
                    String store = goodsCartResList.get(mCurrentPosition).getStore();
                    CartGoodsBean cartGoodsBean = cartGoodsBeanList.get(store);
                    cartGoodsBean.setCheck(!cartGoodsBean.isCheck());
                    List<CartGoodsInnerBean> cartGoodsInnerBeanList = cartGoodsBean.getCartGoodsInnerBeanList();
                    CheckAllOrNo(cartGoodsInnerBeanList, cartGoodsBean.isCheck());
    //                cartAdapter.notifyDataSetChanged();
                    cartAdapter.notifyDataSetChangedForChild(mCurrentPosition);
                }
            });
    

    这里主要需要知道虚浮的tab处于哪个item 上,然后对应选择修改当前item的选择状态就好了。

  5. 这个全选的就比较简单了,就通过一个boolean 值来控制就行,点击这个按钮时候所有其他的选中状态都失效,完全以这个选择状态为依据。代码简单看一下:

    cbCartSelectAllGoods.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    selectAllGoods = !selectAllGoods;
                    cartGoodsBeanList.clear();
                    for (int i = 0; i < goodsCartResList.size(); i++) {
                        GoodsCartRes goodsCartRes = goodsCartResList.get(i);
                        CartGoodsBean cartGoodsBean = new CartGoodsBean();
                        cartGoodsBean.setCheck(selectAllGoods);
                        List<CartGoodsInnerBean> cartGoodsInnerBeanList = new ArrayList<>();
                        for (GoodsCartRes.GoodsBean goodsBean : goodsCartRes.getGoods()) {
                            CartGoodsInnerBean cartGoodsInnerBean = new CartGoodsInnerBean();
                            cartGoodsInnerBean.setCheck(selectAllGoods);
                            cartGoodsInnerBeanList.add(cartGoodsInnerBean);
                        }
                        cartGoodsBean.setCartGoodsInnerBeanList(cartGoodsInnerBeanList);
                        cartGoodsBeanList.put(goodsCartRes.getStore(), cartGoodsBean);
                    }
                    cartAdapter.notifyDataSetChanged();
                    cartAdapter.notifyDataSetChangedForChild(mCurrentPosition);
                    updateSuspensionBar();
    
                }
    
            });
    

    这部分代码和初始化很相似,初始化中我们都设置为为选中,但实际开发中肯定还是要通过接口来设置购物车的。

  6. 改变悬浮头部的监听回掉上面我也提到过了。代码如下:

    cartAdapter.setListenerCheckChange(new CartAdapter.ListenerCheckChange() {
        @Override
        public void changePostion(boolean isCheck) {
            updateSuspensionBar();
    
        }
    });
    
     private void updateSuspensionBar() {
            String store = goodsCartResList.get(mCurrentPosition).getStore();
            tvStoreName.setText(store);
            CartGoodsBean cartGoodsBean = cartGoodsBeanList.get(store);
            Log.d("HHHH", "updateSuspensionBar: " + mCurrentPosition + "---" + cartGoodsBean.isCheck());
            cbCartSelectGoods.setChecked(cartGoodsBean.isCheck());
        }
    

结束语

如上步骤基本完成了,部分常规的购物车需求,还剩下的需求

  1. 如长按条目弹出删除按钮
  2. 点击进入编辑界面等

因为这只是一个简单的基础布局,后续业务还是更不相同,后续会加上以上功能,更加贴近京东的效果。

有问题欢迎纠正,谢谢啦

如果对你有帮助就点个赞把,你的鼓励是我前进的动力。