日期选择采用CalendarView
控件,时间选择采用TimePicker
控件,然后通过switch
控件控制其VISIBLE
和GONE
属性,类型通过PopUpWindows弹窗显示,标签通过SharedPreferences
进行传递。最后插入SQLite数据库中。
因为获取的日历控件的月份要比实际少一个月,故因此需要把月份加上一。
然后将获取的年月日字符串数据转为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;
}
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);
}
});
}
建立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即为底部导航的文字和图片效果,可以使用选择器,监听选择和不被选择两种状态,改变其效果。若不设置,系统模式使用样式颜色作为高亮显示。灰色呈不被选择状态。
由于本APP只需要两个节目,故只需要创建两个fragment,然后在与nav进行绑定即可,此处仅作为标记,详细介绍如下文所示
/**
* Set a listener that will be notified when a bottom navigation item is selected. This listener
* will also be notified when the currently selected item is reselected, unless an {@link
* OnNavigationItemReselectedListener} has also been set.
*
* @param listener The listener to notify
* @see #setOnNavigationItemReselectedListener(OnNavigationItemReselectedListener)
*/
设置底部导航栏当前页面显示
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中进行显示,最后根据类型计算总支出和总收入金额
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);
}
}
}
从数据库中获取实体类集合对象,然后根据其收入和支出两种状态计算相对应的总金额
/**
* 获取RecyclerView数据源*/
private void getData(){
recordList = dao.QueryAll();
if (recordList.size() == 0 || recordList == null){
IsEmpty(true);
return;
}
for (int i = 0; i < recordList.size(); i++) {
/**
* 1为支出
* 0为收入*/
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()),
}
);
}
/**
* 南丁格尔玫瑰图数据源x*/
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;
}
至此,本文已经介绍结束