- Android Studio开发实战:从零基础到App上线 (移动开发丛书)
- 欧阳燊
- 2999字
- 2020-11-28 17:31:45
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); } } }