4.5 实战项目:购物车

购物车的应用面很广,凡是电商App都可以看到购物车的身影。本章以购物车为实战项目,除了购物车使用广泛的特点,还因为购物车用到多种存储方式。现在我们开启购物车的体验之旅吧!

4.5.1 设计思路

先来看常见的购物车的外观。第一次进入购物车频道,购物车里面是空的,如图4-17所示。接着去商品频道选购手机,随便挑几款加入购物车,然后返回购物车,即可看到购物车里的商品列表,有商品图片、名称、数量、单价、总价等信息,如图4-18所示。

图4-17 空空如也的购物车

图4-18 购物车的商品列表

购物车的存在感很强,并不仅仅在购物车页面才能看到。往往在商场频道,甚至某个商品详情页面,都会看到某个角落冒出一个购物车图标。一旦有新商品加入购物车,购物车图标上的商品数量就立马加一。当然,用户也可以点击购物车图标直接跳转到购物车页面。商品频道除了商品列表外,页面右上角还有一个购物车图标,这个图标有时在页面右上角,有时又在页面右下角,如图4-19所示。商品详情页面通常也有购物车图标,如果用户在详情页面把商品加入购物车,那么图标上的数字也会加一,如图4-20所示。

图4-19 手机商场的商品列表

图4-20 商品详情页面

现在我们来看购物车到底采取了哪些存储方式。

● 数据库SQLite:最直观的是数据库,购物车里的商品列表一定放在SQLite中,增删改查都少不了SQLite。

● 共享参数SharedPreferences:注意不同页面的右上角购物车图标都有数字,表示购物车中的商品数量,商品数量建议保存在共享参数中。因为每个页面都要显示商品数量,如果每次都到数据库中执行count操作,就会很消耗资源。因为商品数量需要持久地存储,所以不适合放在全局内存中,不然下次启动App时,内存中的变量又从0开始。

● SD卡文件:通常情况下,商品图片来自于电商平台的服务器,这年头流量是很宝贵的,可是图片恰恰很耗流量(尤其是大图)。从用户的钱包着想,App得把下载的图片保存在SD卡中。这样一来,下次用户访问商品详情页面时,App便能直接从SD卡获取商品图片,不但不花流量而且加快浏览速度,一举两得。

● 全局内存:访问SD卡的图片文件固然是个好主意,然而商品频道、购物车频道等可能在一个页面展示多张商品小图,如果每张小图都要访问SD卡,频繁的SD卡读写操作也很耗资源。更好的办法是把商品小图加载进全局内存,这样直接从内存中获取图片,高效又快速。之所以不把商品大图放入全局内存,是因为大图很耗空间,一不小心就会占用几十兆内存。

不找不知道,一找吓一跳,原来购物车用到了这么多种存储方式。

4.5.2 小知识:菜单Menu

之前的章节在进行某项控制操作时一般由按钮控件触发。如果页面上需要支持多个控制操作,比如去商场购物、清空购物车、查看商品详情、删除指定商品等,就得在页面上添加多个按钮。如此一来,App页面显得杂乱无章,满屏按钮既碍眼又不便操作。这时,就可以使用菜单控件。

菜单无论在哪里都是常用控件,Android的菜单主要分两种,一种是选项菜单OptionMenu,通过按菜单键或点击事件触发,对应Windows上的开始菜单;另一种是上下文菜单ContextMenu,通过长按事件触发,对应Windows上的右键菜单。无论是哪种菜单,都有对应的菜单布局文件,就像每个活动页面都有一个布局文件一样。不同的是页面的布局文件放在res/layout目录下,菜单的布局文件放在res/menu目录下。

下面来看Android的选项菜单和上下文菜单。

1.选项菜单OptionMenu

弹出选项菜单的途径有3种:

(1)按菜单键。

(2)在代码中手动打开选项菜单,即调用openOptionsMenu方法。

(3)按工具栏右侧的溢出菜单按钮,这个在后续介绍工具栏时进行介绍。

实现选项菜单的功能需要重写以下两种方法。

● onCreateOptionsMenu:在页面打开时调用。需要指定菜单列表的XML文件。

● onOptionsItemSelected:在列表的菜单项被选中时调用。需要对不同的菜单项做分支处理。

下面是菜单布局文件的代码,很简单,就是menu与item的组合排列:

    <menu xmlns:android="http://schemas.android.com/apk/res/android" >
        <item
          android:id="@+id/menu_change_time"
          android:orderInCategory="1"
          android:title="改变时间"/>
        <item
          android:id="@+id/menu_change_color"
          android:orderInCategory="8"
          android:title="改变颜色"/>
        <item
          android:id="@+id/menu_change_bg"
          android:orderInCategory="9"
          android:title="改变背景"/>
    </menu>

接下来是使用选项菜单的代码片段:

        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
            getMenuInflater().inflate(R.menu.menu_option, menu);
            return true;
        }


        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
            int id = item.getItemId();
            if (id == R.id.menu_change_time) {
                setRandomTime();
            } else if (id == R.id.menu_change_color) {
                tv_option.setTextColor(getRandomColor());
                } else if (id == R.id.menu_change_bg) {
                    tv_option.setBackgroundColor(getRandomColor());
                }
                return true;
            }


            private void setRandomTime() {
                String desc=DateUtil.getNowDateTime("yyyy-MM-dd HH:mm:ss")+" 这里是菜单显示文本";
                tv_option.setText(desc);
            }


            private int[] mColorArray = {
                    Color.BLACK, Color.WHITE, Color.RED, Color.YELLOW, Color.GREEN,
                    Color.BLUE, Color.CYAN, Color.MAGENTA, Color.GRAY, Color.DKGRAY };
            private int getRandomColor() {
                int random = (int) (Math.random()*10 % 10);
                return mColorArray[random];
            }

按菜单键和调用openOptionsMenu方法弹出的选项菜单都是在页面下方,如图4-21所示。

图4-21 选项菜单的菜单列表

2.上下文菜单ContextMenu

弹出上下文菜单的途径有两种:

(1)默认在某个控件被长按时弹出。通常在onStart函数中加入registerForContextMenu方法为指定控件注册上下文菜单,在onStop函数中加入unregisterForContextMenu方法为指定控件注销上下文菜单。

(2)在除长按事件之外的其他事件中打开上下文菜单。先执行registerForContextMenu方法注册菜单,然后执行openContextMenu方法打开菜单,最后执行unregisterForContextMenu方法注销菜单。

实现上下文菜单的功能需要重写以下两种方法。

● onCreateContextMenu:在此指定菜单列表的XML文件,作为上下文菜单列表项的来源。

● onContextItemSelected:在此对不同的菜单项做分支处理。

上下文菜单的布局文件格式同选项菜单,下面是使用上下文菜单的代码片段:

            @Override
            protected void onResume() {
                registerForContextMenu(tv_context);
                super.onResume();
            }


            @Override
            protected void onPause() {
                unregisterForContextMenu(tv_context);
                super.onPause();
            }


            @Override
            public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
                getMenuInflater().inflate(R.menu.menu_option, menu);
            }


            @Override
            public boolean onContextItemSelected(MenuItem item) {
                int id = item.getItemId();
                if (id == R.id.menu_change_time) {
                    setRandomTime();
                } else if (id == R.id.menu_change_color) {
                    tv_context.setTextColor(getRandomColor());
                } else if (id == R.id.menu_change_bg) {
                    tv_context.setBackgroundColor(getRandomColor());
                }
                return true;
            }

上下文菜单的菜单列表固定显示在页面中部,菜单外的其他页面区域颜色会变深,具体效果如图4-22所示。

图4-22 上下文菜单的菜单列表

4.5.3 代码示例

这一章的编码开始有些复杂了,不但有各种控件和布局的操作,还有4种存储方式的使用,再加上Activity与Application两大组件的运用,已然是一个正规App的雏形。

编码过程分为4步(增加的一步是对AndroidManifest.xml认真配置):

步骤01 想好代码文件与布局文件的名称,比如购物车页面的代码文件取名ShoppingCartActivity.java,对应的布局文件名是activity_shopping_cart.xml;商场频道页面的代码文件取名ShoppingChannelActivity.java,对应的布局文件名是activity_shopping_channel.xml;商品详情页面的代码文件取名ShoppingDetailActivity,对应的布局文件名是activity_shopping_detail.xml;另有一个全局应用的代码文件MainApplication.java。

步骤02 在AndroidManifest.xml中补充相应配置,主要有以下3点:

(1)注册3个页面的acitivity节点,注册代码如下:

              <activity android:name=".ShoppingCartActivity" android:theme="@style/AppBaseTheme" />
              <activity android:name=".ShoppingChannelActivity" />
              <activity android:name=".ShoppingDetailActivity" />

(2)给application补充name属性,值为MainApplication,举例如下:

              android:name=".MainApplication"

(3)声明SD卡的操作权限,主要补充下面3行权限配置:

          <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
          <uses-permission android:name="android.permission.READ_EXTERNAL_STORAG" />
          <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />

步骤03 res目录下的XML文件编写也多了起来,主要工作包括:

(1)在res/layout目录下创建布局文件activity_shopping_cart.xml、activity_shopping_channel.xml、activity_shopping_detail.xml,分别根据页面效果图编写3个页面的布局定义文件。

(2)在res/menu目录下创建菜单布局文件menu_cart.xml和menu_goods.xml,分别用于购物车的选项菜单和商品项的上下文菜单。

(3)在values/styles.xml中补充下面的样式定义,给不带导航栏的购物车页面使用:

          <style name="AppBaseTheme" parent="Theme.AppCompat.Light" />

步骤04 在项目的包名目录下创建类MainApplication、ShoppingCartActivity、ShoppingChannelActivity和ShoppingDetailActivity,并填入具体的控件操作与业务逻辑代码。

下面是购物车页面ShoppingCartActivity.java的主要代码片段:

            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                requestWindowFeature(Window.FEATURE_NO_TITLE);
                setContentView(R.layout.activity_shopping_cart);
                iv_menu = (ImageView) findViewById(R.id.iv_menu);
                tv_title = (TextView) findViewById(R.id.tv_title);
                tv_count = (TextView) findViewById(R.id.tv_count);
                tv_total_price=(TextView)findViewById(R.id.tv_total_price);
                ll_content=(LinearLayout)findViewById(R.id.ll_content);
                ll_cart=(LinearLayout)findViewById(R.id.ll_cart);
                ll_empty=(LinearLayout)findViewById(R.id.ll_empty);
                iv_menu.setOnClickListener(this);
                findViewById(R.id.btn_shopping_channel).setOnClickListener(this);
                findViewById(R.id.btn_settle).setOnClickListener(this);
                iv_menu.setVisibility(View.VISIBLE);
                tv_title.setText("购物车");
                mCount=Integer.parseInt(SharedUtil.getIntance(this).readShared("count", "0"));
                showCount(mCount);
            }


            //显示购物车图标中的商品数量
            private void showCount(int count){
                mCount=count;
                tv_count.setText(""+mCount);
                if(mCount==0){
                    ll_content.setVisibility(View.GONE);
                    ll_cart.removeAllViews();
                    ll_empty.setVisibility(View.VISIBLE);
                }else{
                    ll_content.setVisibility(View.VISIBLE);
                    ll_empty.setVisibility(View.GONE);
                }
            }


            @Override
            public void onClick(View v){
                if(v.getId()==R.id.iv_menu){
                    openOptionsMenu();
                }else if(v.getId()==R.id.btn_shopping_channel){
                    Intent intent=new Intent(this, ShoppingChannelActivity.class);
                    startActivity(intent);
                }else if(v.getId()==R.id.btn_settle){
                    AlertDialog.Builder builder=new AlertDialog.Builder(this);
                    builder.setTitle("结算商品");
                    builder.setMessage("客官抱歉,支付功能尚未开通,请下次再来");
                    builder.setPositiveButton("我知道了", null);
                    builder.create().show();
                }
            }


            @Override
            public boolean onCreateOptionsMenu(Menu menu){
                getMenuInflater().inflate(R.menu.menu_cart, menu);
                return true;
            }


            @Override
            public boolean onOptionsItemSelected(MenuItem item){
                int id=item.getItemId();
                if(id==R.id.menu_shopping){
                    Intent intent=new Intent(this, ShoppingChannelActivity.class);
                    startActivity(intent);
                }else if(id==R.id.menu_clear){
                    //清空购物车数据库
                    mCartHelper.deleteAll();
                    ll_cart.removeAllViews();
                    SharedUtil.getIntance(this).writeShared("count", "0");
                    showCount(0);
                    mGoodsView.clear();
                    mGoodsMap.clear();
                    Toast.makeText(this, "购物车已清空", Toast.LENGTH_SHORT).show();
                }else if(id==R.id.menu_return){
                    finish();
                }
                return true;
            }


            private HashMap<Integer, Long>mGoodsView=new HashMap<Integer, Long>();
            private View mContextView;
            @Override
            public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo){
                mContextView=v;
                getMenuInflater().inflate(R.menu.menu_goods, menu);
            }


            @Override
            public boolean onContextItemSelected(MenuItem item){
                int id=item.getItemId();
                if(id==R.id.menu_detail){
                    //跳转到查看商品详情页面
                    goDetail(mGoodsView.get(mContextView.getId()));
                }else if(id==R.id.menu_delete){
                    //从购物车删除商品的数据库操作
                    long goods_id=mGoodsView.get(mContextView.getId());
                    mCartHelper.delete("goods_id="+goods_id);
                    ll_cart.removeView(mContextView);
                    //更新购物车中的商品数量
                    int left_count=mCount-1;
                    for(int i=0; i<mCartArray.size(); i++){
                        if(goods_id==mCartArray.get(i).goods_id){
                            left_count=mCount-mCartArray.get(i).count;
                            mCartArray.remove(i);
                            break;
                        }
                    }
                    SharedUtil.getIntance(this).writeShared("count", ""+left_count);
                    showCount(left_count);
                    Toast.makeText(this, "已从购物车删除"+mGoodsMap.get(goods_id).name,
    Toast.LENGTH_SHORT).show();
                    mGoodsMap.remove(goods_id);
                    refreshTotalPrice();
                }
                return true;
            }


            private void goDetail(long rowid){
                Intent intent=new Intent(this, ShoppingDetailActivity.class);
                intent.putExtra("goods_id", rowid);
                startActivity(intent);
            }


            private GoodsDBHelper mGoodsHelper;
            private CartDBHelper mCartHelper;
            private String mFirst="true";
            @Override
            protected void onResume(){
                super.onResume();
                mGoodsHelper=GoodsDBHelper.getInstance(this,1);
                mGoodsHelper.openWriteLink();
                mCartHelper=CartDBHelper.getInstance(this,1);
                mCartHelper.openWriteLink();
                mFirst=SharedUtil.getIntance(this).readShared("first", "true");
                downloadGoods();
                SharedUtil.getIntance(this).writeShared("first", "false");
                showCart();
            }


            @Override
            protected void onPause() {
                super.onPause();
                mGoodsHelper.closeLink();
                mCartHelper.closeLink();
            }


            //模拟网络数据,初始化数据库中的商品信息
            private void downloadGoods() {
                String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_
    DOWNLOADS) + "/";
                if (mFirst.equals("true")) {
                    for (int i=0; i<mNameArray.length; i++) {
                        GoodsInfo info = new GoodsInfo();
                        info.name = mNameArray[i];
                        info.desc = mDescArray[i];
                        info.price = mPriceArray[i];
                        long rowid = mGoodsHelper.insert(info);
                        info.rowid = rowid;
                        //往全局内存写入商品小图
                        Bitmap thumb = BitmapFactory.decodeResource(getResources(), mThumbArray[i]);
                        MainApplication.getInstance().mIconMap.put(rowid, thumb);
                        String thumb_path = path + rowid + "_s.jpg";
                        FileUtil.saveImage(thumb_path, thumb);
                        info.thumb_path = thumb_path;
                        //往SD卡保存商品大图
                        Bitmap pic = BitmapFactory.decodeResource(getResources(), mPicArray[i]);
                        String pic_path = path + rowid + ".jpg";
                        FileUtil.saveImage(pic_path, pic);
                        pic.recycle();
                        info.pic_path = pic_path;
                        mGoodsHelper.update(info);
                    }
                } else {
                    ArrayList<GoodsInfo> goodsArray = mGoodsHelper.query("1=1");
                    for (int i=0; i<goodsArray.size(); i++) {
                        GoodsInfo info = goodsArray.get(i);
                        Bitmap thumb = BitmapFactory.decodeFile(info.thumb_path);
                        MainApplication.getInstance().mIconMap.put(info.rowid, thumb);
                    }
                }
            }