目录
1.背景介绍
2.需求分析
2.1待解决问题
2.2 解决方式
3.开发环境
3.1开发工具简介
3.2开发语言简介
4.功能介绍
5.程序实现及关键代码分析
5.1“添加食材”功能实现及分析
5.1.1基于本地数据库的数据持久化
5.1.2 界面设计
5.2“菜篮状况”功能实现及分析
5.2.1动态SQL语句分类查找
5.2.2 界面设计
5.3“菜篮状况”功能实现及分析
5.3.1简易数据分析实现
5.3.2界面设计
5.4“养生建议”功能实现及分析
5.4.1网站列表实现(WebView)
5.4.2 界面设计
5..5 “备忘记录”功能实现及分析
5.5.1 基于本地数据库的内嵌记事本实现
5.5.2 界面设计
5.6 “菜篮管理”功能实现及分析
5.6.1 菜篮数据库增删实现
5.6.2界面设计
5.7 “菜谱搜索”功能实现及代码分析
5.7.1 API接口调用与JSON数据解析
5.7.2 界面设计
5.8 “私人收藏”功能实现及代码分析
5.8.1 基于文本文件(TXT)数据持久化技术
5.8.2 界面设计
健康是一种人在身体与精神上的完全统一的状态,根据世卫组织(WHO)的统计数据,中国居民中符合世卫组织对健康的定义的人数只占总人口的15%,并且有同等比例的人群处在疾病状态中,剩下近十分之七的人处在所谓“亚健康”状态。
后疫情时代,健康已成为当下热点话题之一,健康消费也正逐步渗透到更多的生活领域当中,与此同时,市场上关于生活与健康的APP数目也逐渐增多,包括keep等健身软件,今日养生等健康知识软件,果蔬百科等生活常识软件多种类型。
而健康饮食是指恰当选择搭配合理和适当份量的食物进行食用,人体摄入各种营养素和恰当热量去维持身体细胞的正常生长,增强身体对疾病的抵抗力和维持一定的体重。.
饮食健康因为与人们日常生活的息息相关,又格外被人们所重视,食材的新鲜程度,菜谱搭配合理与否,都是影响厨房能否高效运行,家人的饮食健康的关键因素。
后疫情时代,新冠疫情防控常态化关键时期,为响应政府号召,民众减少外出,居家抗疫;屯菜也成为不可避免的生活常态。
购菜数目的增长往往带来管理的混乱,列举如下:
为做好家庭食材的管理,借助Android移动端对购买食材信息进行记录,将已购的食材录入到移动端,便捷民众整理与记忆,做到购菜时心中有数(解决问题一/三),时刻关注到菜篮中食材的新鲜程度,包括照片间的比对,购入时间的记录等等(解决问题二)。
同时该APP提供了大量正确有效的烹饪方法,在使用过程中用户能够锻炼厨艺(解决问题四),提升生活质量,进行高品质饮食。
该软件设计基于安卓(Android)进行开发,安卓(Android)是基于Linux内核基础的自由开放源代码的操作系统,适用于便捷携带的设备,如智能手持设备;随后在其发展的过程中不断扩展到其他领域中,例如平板电脑等。安卓(Android)由美国Google公司收购注资。
Android Studio 是美国Google公司推出的Android集成开发环境,整体架构与JetBrains旗下的IntelliJ IDEA类似,并提供了相似的开发调试工具。
Android采取代码与布局相分离的方式对项目进行组织,由Java语言编写内部的逻辑,XML语言修改调整布局界面。
Java:最为广泛使用的编程语言之一,也是Android开发的基础。
XML: 可扩展标记语言,标准通用标记语言的子集。
考虑到菜篮中的食材数据是本手机软件(APP)的关键数据,而保存在文件系统中的数据文件分散程度较高,程序难以读取与管理;采用本地数据库对数据进行存储,数据库技术能够帮助我们实现数据共享,避免不同用户建立独立的重复数据,从而进行数据的集中统一控制管理。
通过food_store.class文件创建数据库(见下方代码),并实现了对数据库的增删改查操作,利用TextView对查询数据的正确性进行了测试。
food_store_database helper; helper = new food_store_database(food_store.this, "basket.db", null, 1); Button create_basket = (Button) findViewById(R.id.button1); create_basket.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { helper.getWritableDatabase(); } }); |
针对食材建立了basket.db文件,并执行SQL语句创建表basket
其各字段的数据类型、索引与字段名如下
索引 |
字段名称 |
数据类型 |
解释 |
0 |
id |
整型 |
主键 |
1 |
name |
字符串 |
名称 |
2 |
date |
字符串 |
购入时间 |
3 |
price |
实数 |
价格 |
4 |
time |
实数 |
保鲜时长 |
5 |
timeunit |
字符串 |
保鲜时长单位 |
6 |
number |
实数 |
数目 |
7 |
numberunit |
字符串 |
数目单位 |
8 |
classify |
字符串 |
类别 |
9 |
address |
字符串 |
图片地址 |
public static final String CREATE_basket="create table basket(" +"id integer primary key autoincrement," + "name text," + "date text," + "price real," + "time real," + "timeunit text," + "number real," + "numberunit text," + "classify text," + "address text)"; db.execSQL(CREATE_basket); |
创建数据库表格表basket关键代码如下:
|
数据库建立后,建立“添加食材”页面,为了制作自定义顶部栏,修改themes.xml文件去除了页面的默认顶部栏。
利用layout代码,自定义了基础的顶部栏,在其中添加了“<”(返回)ImageButton 以实现界面跳转,从而使得页面逻辑通顺。
为实现用户调用相机拍照,并将JPG文件存储至数据库的功能,实现FileProvider类,并在res资源文件路径下建立file_paths.xml文件,从而指定文件存储路径。
调用相机拍照并进行存储的关键代码如下:
用户点击拍照后,照片路径便被存储在数据库中,而其路径下对应的图片存储在虚拟机的内存文件中。
image_uri=FileProvider.getUriForFile(food_add.this,"com.example.Basket.fileprovider",outputImage); Intent intent = new Intent("android.media.action.IMAGE_CAPTURE"); intent.putExtra(MediaStore.EXTRA_OUTPUT, image_uri); startActivityForResult(intent, 1); |
路径例如“/storage/emulated/0/Android/data/com.example.basket/cache/id3.jpg”
通过这种组织方式,能够将不定量数目的图片有效存储,而不是依赖于Android R中的资源文件。
通过EditText文本框获取用户输入,并将信息组织为一条长记录,插入在数据库中,以ID作为主键从而保证对数据库的操作不会出错。
利用EditText的setText(null)方法与picture.setVisibility(View.GONE)方法;,将用户的输入与照片清空,便捷用户下一轮的输入。
check_classify[0] =classify[position]; //准备数据 在此查询数据库 String name; String path; //需要获取名称与图片路径 Cursor cursor_food = db.rawQuery("select * from Basket where classify =?", new String[]{check_classify[0]}); |
动态调整SQL语句,从而获取不同分类
用户进行分类的选择,因此需要实现界面左侧classify的ListView,根据用户对ListView的点击事件来获取用户选择的分类,同步更新右侧ListView展示食材:
将分类查询数据库后的获取的食材列表分配适配器中,并加载ListView:
//准备菜篮食物列表listview_ ListView listView_ = (ListView) findViewById(R.id.food_list); //准备food_list 适配器 FoodAdaper adapter_ = new FoodAdaper(basket_food_.this, R.layout.fooditem, foodList); listView_.setAdapter(adapter_); listView_.setOnItemClickListener(new AdapterView.OnItemClickListener() { |
为使得布局清晰,使用ImageView与ListView类进行线性布局的排布;左侧为类别列表,其包含项目为“蔬菜”“豆制”“水产”“肉禽”“水果”“速食”“饮品”“辅料”“粮油”
而右侧列表为数据库中食材内容,通过对数据库的查询结果来填充。
public void onItemClick(AdapterView> parent, View view, int position, long id) { int jcdun; for(jcdun=0;jcdun View tv = listView_classify.getChildAt(jcdun); tv.setBackgroundColor(Color.parseColor("#f7f7f7")); //利用整数jcdun遍历整个listview //将所有的背景色设置为默认的灰色 } listView_classify.getChildAt( position-listView_classify.getFirstVisiblePosition()) .setBackgroundColor(Color.parseColor("#fd8e0a")); //将选中的方框背景色修改为橙色 |
为了使得界面更加美化,修改并调整分类列表选中项的背景色,并保证其他未被选中项中背景色为灰色,该效果使用ListView. setOnItemClickListener与View.setBackgroundColor实现,关键代码如下:
RadioGroup radioGroup = (RadioGroup) findViewById(R.id.food_tap); RadioButton radioButton = (RadioButton) findViewById(radioGroup.getCheckedRadioButtonId()); TextView textView = (TextView) findViewById(R.id.food_top_title); radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { if (checkedId == R.id.food_tab_home) { textView.setText("首页"); Intent it = new Intent(getApplicationContext(), home_.class); startActivity(it); } else if (checkedId == R.id.food_tab_book) { textView.setText("食谱"); Intent it = new Intent(getApplicationContext(), book.class); startActivity(it); } else if (checkedId == R.id.food_tab_food) { textView.setText("菜篮"); } else if (checkedId == R.id.food_tab_my) { textView.setText("我的"); Intent it = new Intent(getApplicationContext(), mine.class); startActivity(it);}}}); |
底部栏使用单选框RadioGroup实现,通过监听底部栏的状态变化(用户点击),更改顶部栏文字并进行页面的跳转。
利用XML文件修改底部栏的显示,使得被选中的页面底部栏图标变色,这在一定程度上能够提升用户的交互体验:利用Style文件定义风格,能够帮助我们降低工作量,提高代码复用率,在各个主页面的底部,都可以添加该Style的底部栏。
|
paintAxes = new Paint(); //画轴(横轴和竖轴) paintAxes.setStyle(Paint.Style.STROKE); paintAxes.setAntiAlias(true); paintAxes.setDither(true); paintAxes.setColor(ContextCompat.getColor(getContext(), colors.get(0))); paintAxes.setStrokeWidth(1); paintCoordinate = new Paint(Paint.ANTI_ALIAS_FLAG); //画坐标(文字或数值) paintCoordinate.setColor(ContextCompat.getColor(getContext(), colors.get(1))); paintCoordinate.setTextSize(22f); paintRectF = new Paint(); //画矩形 paintRectF.setColor(ContextCompat.getColor(getContext(), colors.get(2))); paintRectF.setStyle(Paint.Style.FILL); paintRectF.setDither(true); paintRectF.setAntiAlias(true); |
该功能与“添加食材”功能为同一页面的子页面,用于对当前用户添加数据构建的菜篮空间进行数据分析,利用常规的柱状图作直观展示与分类统计,分类统计实现较为简单,自数据库中进行查询,利用计数语句count进行分类计算即可,柱状图的构建较为困难,参考网络代码以及结合自身数据类型与大小进行修改调整,在画布上进行绘制,最终得到一个简单的效果。
该界面内容较少,利用简单的布局;将图片放置在上方,文字内容放置在下方,每行数据利用LinearLayout框架容纳,并在不同的线性布局框间利用外边距隔开,从而表现出整齐分布而又存在间隔的排版模式。
顶部利用常规顶部栏,利用SetText()方法修改其中TextView文字即可;而后使用ListView展示养生建议网站,每一项目使用WebView+TextView的形式实现,同步设计点击事件,当用户对具体的项目进行单击,我们能够跳转界面,利用WebView进入相应网站;此处的网站是本地保存的,并非网络实时爬取,但访问仍需要联网进行。
WebView items = (WebView)findViewById(R.id.good_health_item); // 使用Intent对象得到传过来的参数 Intent intent = getIntent(); String list = intent.getStringExtra("name"); items.loadUrl(list); |
关键代码:获取列表页面传参 loadUrl WebView
列表页使用ListView展示,详情页使用WebView展示。
在APP中内嵌记事本,帮助用户进行便捷快速记忆部分无法被记录在菜篮中的内容,以一种更加自由的方式进行备忘。
通过“我的”页面 点击“待办”跳转到内嵌记事本中,通过添加记录,实时更新数据库来保存用户输入,通过ListView展示已保存笔记
public boolean onItemLongClick(AdapterView> parent, View view, final intposition, long id) { AlertDialog dialog; AlertDialog.Builder builder = new AlertDialog.Builder( onenote.this).setMessage("是否删除此事件?").setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { OnenoteBean notepadBean = list.get(position); if(mSQLiteHelper.deleteData(notepadBean.getId())){ list.remove(position); adapter.notifyDataSetChanged(); Toast.makeText(onenote.this,"删除成功", Toast.LENGTH_SHORT).show(); } } }) |
支持数据库的增删操作,能够通过长按,实现对笔记的删除,关键代码如下。
对菜篮进行删查操作,支持用户删除与查找食材,当用户发觉食材过期/变质,或是已经将家中某项食材完全消耗,应当支持用户对其进行删除,从而为之后的输入腾出空间。
利用长按方法进行删除,同步执行数据库内删除与listView中remove。
查找应该属于数据库的基本操作,而对菜篮的查找,也能够为用户带来更好的用户体验,在实际APP中,查找往往是不可避免的操作。
利用SQL中like子句对数据库进行模糊匹配查询的关键代码如下:
Cursor cursor_food = db.rawQuery("select * from Basket where name like '%" + home_search_food_name.getText().toString() + "%'", null); if (cursor_food.moveToFirst()) { do{ name = (cursor_food.getString(1)); path = (cursor_food.getString(9)); File file = new File(path); if(file.exists()){ Bitmap bm = BitmapFactory.decodeFile(path); foodList.add(new food(name, bm)); }} while(cursor_food.moveToNext());} cursor_food.close(); |
查询部分并未制作新界面,而是利用控件View的可见性Visibility进行控制,当用户点击搜索按钮,对数据库进行查询并将内容填充在ListView中,并将其他内容进行隐藏,而后点击取消,将内容可见性进行交换,从而实现单页面内的工作。
通过用户在EditText中输入或是通过菜篮详情页跳转传参,将其作为关键词检索网络接口,获取各类食材的常见烹饪做法
该部分功能难点在于API接口的连接,需要一定网络请求知识与相关的JSON数据解析能力。
用于解析JSON数据的关键代码如下:
public void handleMessage(@NonNull Message msg) { // 解析JSON数据 从中获得我需要的内容! super.handleMessage(msg); if (msg.what == 0) { String strData = (String) msg.obj; str=strData; JSONObject jsonObject1; try { jsonObject1 = new JSONObject(str); // 整个JSON对象 JSONObject jsonObject2=jsonObject1.getJSONObject("result"); // 进入result部分 JSONArray arr = jsonObject2.getJSONArray("list"); // 进入result-list部分 for(int i=0;i<30;i++){ jsonObject1 = arr.getJSONObject(i); foodList.add(new book_food(jsonObject1.opt("name").toString(),jsonObject1.opt("pic").toString())); } //读取三十个结果 } catch (JSONException e) { e.printStackTrace(); } |
而网络请求的关键代码如下:(在主线程中的HTTP请求,运行时都会报错)
public void start(View view) { new Thread(new Runnable() { public void run() { String stringFromNet = getStringFromNet(); Message message = new Message(); message.what = 0; message.obj = stringFromNet; mHandler.sendMessage(message); }}).start();
ext(this,"开启子线程请求网络!",Toast.LENGTH_SHORT).show(); } |
|
当然,连接API接口前需要申请网络权限,并打开虚拟机的wifi功能
在上述菜谱查询的过程中,用户若发现喜爱的食谱做法,可以建立单人的收藏夹,对喜爱的做法进行收藏,并能够通过单独的页面展示。
对此我使用了文本txt文件来做数据持久化的工作,当用户长按列表中的某类食谱,能够弹窗提示是否收藏,若用户点击收藏,则我将该记录的数据追加到txt的末尾处,并在收藏页中利用ListView进行简单展示,若用户选择取消,则弹窗关闭。
public static void savefile(String file, String conent) { BufferedWriter out = null; try { out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true))); out.write(conent); } catch (Exception e) { e.printStackTrace(); } finally { try { if(out != null){ out.close();} } catch (IOException e) { e.printStackTrace();}}} |
savefile( )将收藏内容添加到内存中txt文件中的方法,
关键代码如下
private String loadFromSDFile(String fname) { String result=null; try { File f=new File(fname); int length=(int)f.length(); byte[] buff=new byte[length]; FileInputStream fin=new FileInputStream(f); fin.read(buff); fin.close(); result=new String(buff,"UTF-8"); }catch (Exception e){ e.printStackTrace(); Toast.makeText(shoucang.this,"没有找到指定文件",Toast.LENGTH_SHORT).show(); } return result; } |
将SD卡内存中文件读取为String字符串