一个简单的记账本APP
视频效果预览
添加账目记录
效果预览
添加账目记录实现
简述
日期选择采用CalendarView
控件,时间选择采用TimePicker
控件,然后通过switch
控件控制其VISIBLE
和GONE
属性,类型通过PopUpWindows弹窗显示,标签通过SharedPreferences
进行传递。最后插入SQLite数据库中。
实现
获取日期
因为获取的日历控件的月份要比实际少一个月,故因此需要把月份加上一。
然后将获取的年月日字符串数据转为Date格式,最后将Date格式转为当时的星期
字符串时间戳转Date
public static Date getStringToDate(String str){ mSimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); try { Date date = mSimpleDateFormat.parse(str); return date; } catch (ParseException e) { e.printStackTrace(); } return null; }
Date转星期
public static String getWeekOfDate(Date date) { String[] weekDays = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"}; Calendar cal = Calendar.getInstance(); cal.setTime(date); int w = cal.get(Calendar.DAY_OF_WEEK) - 1; if (w < 0) w = 0; return weekDays[w]; }
最后将获取的日期、星期进行保存
private void getDate() { mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() { @Override public void onSelectedDayChange(@NonNull CalendarView view, int year, int month, int dayOfMonth) { month = month + 1; CNDate = year + "年" + month + "月" + dayOfMonth + "日"; String date = year + "-" + month + "-" + dayOfMonth; Log.d(TAG, "date=" + date); Log.d(TAG, "CNdate=" + CNDate); //string日期转date日期,在转为星期 String week = DateUtils.getWeekOfDate(DateUtils.getStringToDate(date)); Log.d(TAG, "week=" + week); SelectDate.setText(CNDate + " " + week); } }); }
获取时间
直接对TimePicker
控件进行事件监听,然后将获取的时间进行保存即可
private void getTime() { mTimePicker.setOnTimeChangedListener(new TimePicker.OnTimeChangedListener() { @Override public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { String time = hourOfDay + ":" + minute; SelectTime.setText(time); Log.d(TAG, time); } }); }
Switch控制显示和隐藏
更改Switch样式
建立thumb.xml和track.xml两个选择器
thumb.xml如下
track.xml如下
然后分别建立两个选择器的不同状态下的效果文件
open_thumb.xml
shut_thumb.xml
open_track.xml
shut_track.xml
最后应用于switch效果如下
事件监听
通过监听Switch事件,判断false和true两种状态,分别对应控件的隐藏和显示
class SwitchListener implements CompoundButton.OnCheckedChangeListener { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { switch (buttonView.getId()) { case R.id.DateSwitch: if (isChecked) DateLayout.setVisibility(View.VISIBLE); else DateLayout.setVisibility(View.GONE); break; case R.id.TimeSwitch: if (isChecked) TimeLayout.setVisibility(View.VISIBLE); else TimeLayout.setVisibility(View.GONE); break; } } }
保存至SQLite数据库
public void SaveMessage(View view) { String date = SelectDate.getText().toString().trim(); String time = SelectTime.getText().toString().trim(); String type = TypeText.getText().toString(); String label = TextLabel.getText().toString(); String name = GoodsName.getText().toString(); String price = GoodsPrice.getText().toString().trim(); if (TextUtils.isEmpty(type) || type.equals("支出or收入")){ toastUtils.ShowFail("类型错误!"); return; } if (TextUtils.isEmpty(label) || label.equals("暂未选择")){ toastUtils.ShowFail("标签错误!"); return; } if (TextUtils.isEmpty(name) || TextUtils.isEmpty(price)){ toastUtils.ShowFail("商品信息或者商品价格格式!"); return; } int t = type.equals("支出") ? 1 : 0; Record record = new Record(date,time,t,label,name,price); int flag = dao.Insert(record); if (flag == 1){ toastUtils.ShowSuccess("保存成功!"); }else { toastUtils.ShowFail("保存失败!"); } }
标签选择实现
效果预览
实现
状态改变
每一个标签有两种状态,选择和不被选择状态,分别对应两种样式效果,一种呈灰色,另一种呈高亮,在进行选择时可以同时点亮多个标签,但最后进行保存时,只能选择一个标签,若条件不满足,系统给予错误提示。
事件监听
class TypeListener implements View.OnClickListener{ @Override public void onClick(View v) { switch (v.getId()){ case R.id.type_1: setTag(type_1,getTag(type_1)); setBG(type_1,1); break; case R.id.type_2: setTag(type_2,getTag(type_2)); setBG(type_2,2); break; case R.id.type_3: setTag(type_3,getTag(type_3)); setBG(type_3,3); break; case R.id.type_4: setTag(type_4,getTag(type_4)); setBG(type_4,4); break; case R.id.type_5: setTag(type_5,getTag(type_5)); setBG(type_5,5); break; case R.id.type_6: setTag(type_6,getTag(type_6)); setBG(type_6,6); break; case R.id.type_7: setTag(type_7,getTag(type_7)); setBG(type_7,7); break; case R.id.type_8: setTag(type_8,getTag(type_8)); setBG(type_8,8); break; } } }
状态监听
每一个标签被点击,其tag自增一次,若未被点击,初始值为1
private void setTag(LinearLayout layout,int tag){ tag++; layout.setTag(tag); }
private int getTag(LinearLayout layout){ Object tag = layout.getTag(); if (tag == null)return 1; return (int) tag; }
然后通过tag值改变标签的样式,成偶数则高亮,奇数则灰色,并记录当前状态值,同时保存被选择的标签数量。
private void setBG(LinearLayout layout,int index){ int tag = (int)layout.getTag(); if (tag % 2 == 0) { layout.setBackground(getResources().getDrawable(R.drawable.blue_radius_bg)); TotalNum++; b_select[index-1] = true; } else { layout.setBackground(getResources().getDrawable(R.drawable.grey_radius_bg)); TotalNum--; b_select[index-1] = false; } }
然后通过监听保存按钮点击事件,取出状态值为true的标签值进行返回
private String selectTag(){ for (int i = 0; i < 8; i++) { if ( b_select[i]){ return s_select[i]; } } return null; }
同时监听被选择的标签总数,若小于1,则未选择任何标签,给予提升;若大于1,则选择多个标签,同样给予提升。最后通过SharedPreferences
进行数据传回;使用Intent传输应该会更安全,一开始设计使用EventBus,但最后不了了。
public void SaveMessage(View view){ if (TotalNum > 1){ toastUtils.ShowFail("选择标签数量不能超过一"); }else if (TotalNum <= 0){ toastUtils.ShowFail("选择标签数量不能少于一"); }else { toastUtils.ShowSuccess("success"); String tag = selectTag(); Log.d(TAG,"TAG="+tag); SP sp = SP.getInstance(); sp.PutData(LabelActivity.this,"Label",tag); //EventBus.getDefault().post(new TextClass(tag)); KillProcess.POP(LabelActivity.this); } }
导航界面
采用底部导航控件BottomNavigationView
切换账单记录界面和账单概览界面,并通过ViewPager
进行页面滑动,两个子页面采用两个不同的Fragment
。
创建menu
次menu即为底部导航的文字和图片效果,可以使用选择器,监听选择和不被选择两种状态,改变其效果。若不设置,系统模式使用样式颜色作为高亮显示。灰色呈不被选择状态。
创建Fragment
由于本APP只需要两个节目,故只需要创建两个fragment,然后在与nav进行绑定即可,此处仅作为标记,详细介绍如下文所示
绑定Fragment
设置底部导航栏当前页面显示
navView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { switch (item.getItemId()) { case R.id.menu_navigation_record: mViewPager.setCurrentItem(0); return true; case R.id.menu_navigation_general: mViewPager.setCurrentItem(1); return true; } return false; } });
将ViewPager和两个fragment进行绑定
private void setupViewPager(ViewPager viewPager) { BottomAdapter adapter = new BottomAdapter(getSupportFragmentManager()); adapter.addFragment(new RecordFragment()); adapter.addFragment(new GeneralFragment()); viewPager.setAdapter(adapter); }
并通过监听ViewPager事件,进行页面切换
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { navView.getMenu().getItem(position).setChecked(true); } @Override public void onPageScrollStateChanged(int state) { } }); }
账单记录显示
效果预览
简述
此界面就比较简单,从数据库中拿出所有数据并构建一个集合类对象,然后放入RecyclerView中进行显示,最后根据类型计算总支出和总收入金额
RecyclerView显示
建立适配器
public class OrderAdapter extends RecyclerView.Adapter { private String[] s_select = {"日用百货","文化休闲","交通出行","生活服务","服装装扮","餐饮美食","数码电器","其他标签"}; private int[] img_select = { R.drawable.icon_type_one, R.drawable.icon_type_two, R.drawable.icon_type_three, R.drawable.icon_type_four, R.drawable.icon_type_five, R.drawable.icon_type_six, R.drawable.icon_type_seven, R.drawable.icon_type_eight}; private List recordList; public OrderAdapter(List recordList){ this.recordList = recordList; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.order_item,parent,false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { Record record = recordList.get(position); holder.item_date.setText(record.getDate()); holder.item_time.setText("时间 "+record.getTime()); holder.item_label.setText("["+record.getLabel()+"]"); holder.item_name.setText(record.getGoodsName()); if (record.getType() == 1){ holder.item_price.setText("-"+record.getGoodsPrice()); }else { holder.item_price.setText("+"+record.getGoodsPrice()); } for (int i = 0; i < 8; i++) { if (record.getLabel().equals(s_select[i])){ holder.item_img.setImageResource(img_select[i]); } } } @Override public int getItemCount() { return recordList.size(); } class ViewHolder extends RecyclerView.ViewHolder{ private TextView item_date,item_time,item_label,item_name,item_price; private ImageView item_img; public ViewHolder(@NonNull View itemView) { super(itemView); item_date = itemView.findViewById(R.id.item_date); item_time = itemView.findViewById(R.id.item_time); item_label = itemView.findViewById(R.id.item_label); item_name = itemView.findViewById(R.id.item_name); item_price = itemView.findViewById(R.id.item_price); item_img = itemView.findViewById(R.id.item_img); } }}
获取数据源
从数据库中获取实体类集合对象,然后根据其收入和支出两种状态计算相对应的总金额
private void getData(){ recordList = dao.QueryAll(); if (recordList.size() == 0 || recordList == null){ IsEmpty(true); return; } for (int i = 0; i < recordList.size(); i++) { if (recordList.get(i).getType() == 1){ totalPay += Double.parseDouble(recordList.get(i).getGoodsPrice()); }else { totalIncome += Double.parseDouble(recordList.get(i).getGoodsPrice()); } } IsEmpty(TotalPay,totalPay,1); IsEmpty(TotalIncome,totalIncome,0); }
保留两位小数
由于在转换格式的之后,进行加减运算精度会失衡,会产生很多位小数点,但由于美观以及界面设计,一般设计保留2位小数即可
以12.345678为例:12.345678*100 = 1234.5678然后将其转为整数,则变为 1234最后除以100.0,此处标签,是100.0不是100,因为前面已经为整型数据,整型除整型依旧为整型,只有除100.0,int数据类型才会强制转换为float或者double类型
private double SaveDecimal(double n){ return n = ((int)(n*100))/100.0; }
概览
效果预览
简述
此界面显示的内容包括共计收入、支出多少笔和合计收入、支出多少金额,以及通过标签分类显示支出、收入占比,以及全部收入、支出记录中前三甲。
分类显示
通过获取单个标签的所有金额除以全部标签的总金额,获取百分比占比,并通过view进行显示,条形bar通过获取屏幕宽度,例如:我的手机屏幕宽度为1080,就以数码电器为例:11998.99/总金额 * 1080 = 条形bar的长度
百分比占比同样以数码电器为例:11998.99/总金额 * 100 = 数码电器百分比
创建适配器
public class BarAdapter extends RecyclerView.Adapter { private String[] s_select = {"日用百货", "文化休闲", "交通出行", "生活服务", "服装装扮", "餐饮美食", "数码电器", "其他标签"}; private int[] img_select = { R.drawable.icon_type_one, R.drawable.icon_type_two, R.drawable.icon_type_three, R.drawable.icon_type_four, R.drawable.icon_type_five, R.drawable.icon_type_six, R.drawable.icon_type_seven, R.drawable.icon_type_eight}; private List viewBarList; public BarAdapter(List viewBarList) { this.viewBarList = viewBarList; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.pay_type_item, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); holder.item_bar.measure(w, h); ViewBar viewBar = viewBarList.get(position); holder.item_bar.setLayoutParams(new LinearLayout.LayoutParams(viewBar.getWidth(), viewBar.getHeight())); holder.item_label.setText(viewBar.getLabel()); holder.item_num.setText(viewBar.getNum()); holder.item_price.setText(viewBar.getPrice()); Log.d("testLabel",viewBar.getLabel()); for (int i = 0; i < 8; i++) { if (viewBar.getLabel().trim().equals(s_select[i])){ Log.d("testLabel",viewBar.getLabel()); holder.item_img.setImageResource(img_select[i]); } } } @Override public int getItemCount() { return viewBarList.size(); } class ViewHolder extends RecyclerView.ViewHolder { private TextView item_label, item_num, item_price; private ImageView item_img; private View item_bar; public ViewHolder(@NonNull View itemView) { super(itemView); item_label = itemView.findViewById(R.id.pay_type_label); item_num = itemView.findViewById(R.id.pay_type_num); item_price = itemView.findViewById(R.id.pay_type_price); item_img = itemView.findViewById(R.id.pay_type_img); item_bar = itemView.findViewById(R.id.pay_type_bar); } }}
获取数据源
获取屏幕宽度,以此作为基数
manager = requireActivity().getWindowManager(); width = manager.getDefaultDisplay().getWidth();
private void getData(){ if (TotalPrice <= 0)return; for (int i = 0; i < d_price.length; i++) { if (d_price[i] == 0)continue; int n = (int) (d_price[i] / TotalPrice * width); double t = SaveDecimal(d_price[i] / TotalPrice * 100); barList.add(new ViewBar(s_select[i]+" ",t+"%","¥"+d_price[i],n,10)); } }
前三甲
通过比较所有账单记录,根据金额升序获取前三甲
创建适配器
public class RankAdapter extends RecyclerView.Adapter { private int[] img_select = { R.drawable.gold, R.drawable.silver, R.drawable.tongpai, }; private List rankLists; public RankAdapter(List rankLists){ this.rankLists = rankLists; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.ranking_list_item,parent,false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { RankList rank = rankLists.get(position); holder.item_label.setText("["+rank.getLabel()+"]"); holder.item_content.setText(rank.getContent()); if (rank.getType() == 1){ holder.item_price.setText("-"+rank.getPrice()); }else { holder.item_price.setText("+"+rank.getPrice()); } switch (rank.getPosition()){ case 1: holder.item_img.setImageResource(img_select[0]); break; case 2: holder.item_img.setImageResource(img_select[1]); break; case 3: holder.item_img.setImageResource(img_select[2]); break; } } @Override public int getItemCount() { return rankLists.size(); } class ViewHolder extends RecyclerView.ViewHolder{ private TextView item_label,item_content,item_price; private ImageView item_img; public ViewHolder(@NonNull View itemView) { super(itemView); item_label = itemView.findViewById(R.id.Rank_label); item_content = itemView.findViewById(R.id.Rank_name); item_price = itemView.findViewById(R.id.Rank_price); item_img = itemView.findViewById(R.id.Rank_img); } }}
获取数据源
代码虽不比递归以及排序整洁,但是时间复杂度控制在0(n),效率比冒泡排序、快速排序等要高一点
private void getRankings(){ if (recordList.size() == 0 || recordList == null)return; double maxPrice = -32768,midPrice = -32768,lowPrice = -32768; int maxIndex = -1,midIndex = -1,lowIndex = -1; for (int i = 0; i < recordList.size(); i++) { double price = Double.parseDouble(recordList.get(i).getGoodsPrice()); if ( price > maxPrice){ lowPrice = midPrice; lowIndex = midIndex; midPrice = maxPrice; midIndex = maxIndex; maxPrice = price; maxIndex = i; } if (price < maxPrice && price > midPrice){ lowPrice = midPrice; lowIndex = midIndex; midPrice = price; midIndex = i; } if (price < maxPrice && price < midPrice && price > lowPrice){ lowPrice = price; lowIndex = i; } } int[] poi = {maxIndex,midIndex,lowIndex}; for (int i = 0; i < 3; i++) { if (poi[i] == -1)continue; rankListList.add(new RankList(i+1,recordList.get(poi[i]).getLabel(),recordList.get(poi[i]).getGoodsName(),recordList.get(poi[i]).getGoodsPrice(),recordList.get(poi[i]).getType())); } }
单标签总价以及总金额
private void getPrice(){ if (recordList.size() == 0 || recordList == null)return; d_price = new double[s_select.length]; for (int i = 0; i < recordList.size(); i++) { for (int j = 0; j < s_select.length; j++) { if (recordList.get(i).getLabel().equals(s_select[j])){ d_price[j] += Double.parseDouble(recordList.get(i).getGoodsPrice()); TotalPrice += Double.parseDouble(recordList.get(i).getGoodsPrice()); break; } } } }
可视化概览
效果预览
简述
本可视化图表工具采用的是AAChartView,此工具相对于老牌MPChartView和HelloChartView而言,使用更加简单,种类更加齐全,重点是粉粉嫩嫩,但他的导入方式与其他不同,不是通过导入闭包进行使用;而且通过复制它到一些文件到自己工程项目中,其样式使用的js写的,所有需要导入一些js文件以及一些其他java文件。这一点不比导入闭包方便。根据需要进行选择使用。
折线图
通过aa_drawChartWithChartModel()
方法获取AAChartModel
对象即可,使用超级简单
lineChartView.aa_drawChartWithChartModel(InitLineChart());
然后可通过配置一些参数,更加形象化图表
例如:categories 为 x轴数据源,类型为String[]yAxisMin 为 y轴数据源最小值yAxisMax 为 y轴数据源最大值series 为 每个数据点的提示内容,其中name为标题;data为数据,类型为Object[]
private AAChartModel InitLineChart() { return new AAChartModel() .chartType(AAChartType.Areaspline) .legendEnabled(false) .yAxisVisible(true) .markerRadius(6f) .markerSymbolStyle(AAChartSymbolStyleType.InnerBlank) .zoomType(AAChartZoomType.XY) .categories(s_select) .yAxisMin(2.0f)//Y轴数据最大值和最小值范围 .yAxisMax(2000.0f) .xAxisTickInterval(2) .series(new AASeriesElement[]{ new AASeriesElement() .name("合计") .color("#2494F3") .data( getPrice()) }); }
获取数据源
由于需要的是Object[] 类型数据,所有需要将string类型数据转为double,然后将double转为Double类型,然后进行强制转换,最后变为Object类型
private Object[] getPrice(){ if (recordList.size() == 0 || recordList == null)return null; double[] d_price = new double[s_select.length]; Object[] o_price = new Object[s_select.length]; for (int i = 0; i < recordList.size(); i++) { for (int j = 0; j < s_select.length; j++) { if (recordList.get(i).getLabel().equals(s_select[j])){ Log.d("DetailedActivity",Double.parseDouble(recordList.get(i).getGoodsPrice())+""); d_price[j] += Double.parseDouble(recordList.get(i).getGoodsPrice()); break; } } } for (int i = 0; i < s_select.length; i++) { o_price[i] = new Double(d_price[i]); } return o_price; }
南丁格尔玫瑰图
同样适用aa_drawChartWithChartModel
方法进行数据体现,同时无论什么类型的图表类型,都知使用AAChartView
控件,并且只需要返回AAChartModel
对象,这极大程度方便进行封装使用
mapChartView.aa_drawChartWithChartModel(InitRoseChart());
private AAChartModel InitRoseChart() { return new AAChartModel() .yAxisTitle("cm") .chartType(AAChartType.Column) .xAxisVisible(false)//是否显示最外一层圆环 .yAxisVisible(true)//是否显示中间的多个圆环 .yAxisAllowDecimals(true) .legendEnabled(false)//隐藏图例(底部可点按的小圆点) .categories(getTitles()) .dataLabelsEnabled(true) .polar(true)//极地化图形 .series(new AASeriesElement[]{ new AASeriesElement() .name("价格") .data(getRosePrice()), } ); }
获取数据源
private Object[] getRosePrice(){ if (recordList.size() == 0 || recordList == null)return null; double[] d_price = new double[recordList.size()]; Object[] o_price = new Object[recordList.size()]; for (int i = 0; i < recordList.size(); i++) { d_price[i] = Double.parseDouble(recordList.get(i).getGoodsPrice()); } for (int i = 0; i < recordList.size(); i++) { o_price[i] = new Double(d_price[i]); } return o_price; }
记录删除
效果图
释
首先删除数据库中的对象,然后删除当前列表数据
adapter.setDeleteListener(new OrderAdapter.onDeleteListener() { @Override public void onClickListener(int pos, Record bean) { dao.Delete(bean.getGoodsName()); recordList.remove(pos); adapter.notifyDataSetChanged(); EventBus.getDefault().postSticky(new UpdateBean(false)); } });
尾言
此代码为刚入门所敲,很多不足之处,最近有很多人找我要此项目代码,修复了一些Bug,增加删除功能,但由于之前技术不足,框架构建不是很好,无心去重构,再次感谢大家的厚爱
来源地址:https://blog.csdn.net/News53231323/article/details/125128638