目录
一、测试环境说明
电脑环境
Windows 11
编写语言
JAVA
开发软件
Android Studio (2020)
开发软件只要大于等于测试版本即可(近几年官网直接下载也可以),若是版本低于测试版本请自行测试。项目需要根据你的软件自行适配
二、项目简介
该项目简介来自网络,具体内容需要自行测试
该记账本App基于Android Studio开发,采用Java语言和Room数据库实现,具备完善的收支管理功能。应用通过三个核心页面(记账、查询、统计)实现数据记录、筛选分析和可视化展示,采用BottomNavigationLayout实现流畅的页面切换。
系统支持分类记录收支数据,提供灵活的增删改查功能,并通过TabLayout实现收入/支出分类展示。创新的联动选择器和条件筛选功能(日期范围、金额区间、类别)显著提升了数据查询效率。
数据可视化是项目的突出亮点,通过折线图动态展示年/月结余趋势,结合饼图直观呈现日支出构成。严格的输入校验机制(如金额格式限制)和人性化交互设计(自定义对话框、日历控件)保障了用户体验。
该应用将数据库操作(RoomDB)、UI组件(ViewPager、碎片)和图表库深度整合,体现了完整的Android开发技术栈应用。项目不仅满足基础记账需求,更通过多维统计功能帮助用户掌握财务动向,具有实用性和创新性。
三、项目演示
网络资源模板--基于Android studio 记账本App
四、部设计详情(部分)
首页
1. 页面的结构
该页面采用经典的底部导航+顶部标签页布局。底部通过BottomNavigationView实现主页、查询、统计三大模块切换,顶部使用TabLayout与ViewPager2联动,分为"全部"、"支出"、"收入"三个标签页。
核心区域为可横向滑动的碎片视图,每个标签对应独立的Fragment展示数据列表。
右下角悬浮按钮触发添加记账的弹窗对话框,该对话框内嵌二级标签页区分收支类型,包含金额输入、日期选择、备注填写等功能区域。
2. 使用到的技术
采用Jetpack组件库构建现代化UI:通过ViewPager2实现横向滑动交互,配合TabLayoutMediator完成标签联动;使用Room数据库进行本地数据持久化,结合LiveData实现数据驱动UI更新;
对话框采用自定义布局与窗口动画,金额输入框通过TextWatcher实现实时校验(限制8位整数+2位小数);日期选择集成原生DatePickerDialog,多线程处理数据库操作避免主线程阻塞,通过Handler实现线程间通信刷新界面。
3. 页面详细介绍
主页功能页作为应用核心入口,采用三层视觉层级:顶部标签分类展示流水记录,中间区域动态加载列表碎片,底部悬浮按钮提供快捷操作。
列表项展示简化的金额、类型和日期信息,支持点击查看详情与长按删除。添加记账时通过智能输入校验(如自动补全小数点、拦截非法输入)提升用户体验,日期选择默认当天并支持历史记录补登。
数据变动后自动同步更新所有关联视图,保持多标签页数据一致性,整体交互流畅符合Material Design规范。
package com.cqu.accountbook.ui.home;
import static android.content.Context.MODE_PRIVATE;
import android.app.DatePickerDialog;
import android.app.Dialog;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Looper;
import android.os.Message;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.cqu.accountbook.R;
import com.cqu.accountbook.database.DatabaseAction;
import com.cqu.accountbook.database.MyDatabase;
import com.cqu.accountbook.databinding.FragmentHomeBinding;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.Calendar;
public class HomeFragment extends Fragment {
private FragmentHomeBinding binding;
public static String type = "餐饮";
public static boolean inOut = true;
public static String remark = "";
public static long moneyAcc = 0;
public static int[] date = {0, 0, 0};
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentHomeBinding.inflate(inflater, container, false);
View root = binding.getRoot();
binding.floatingAddButton.setOnClickListener(view -> show());
ViewPager2 viewPager2 = root.findViewById(R.id.viewpager);
viewPager2.setAdapter(new ViewPagerAdapter(this));
TabLayout tabLayout = root.findViewById(R.id.tabs);
TabLayoutMediator mediator = new TabLayoutMediator(tabLayout, viewPager2, (tab, position) -> {
switch (position) {
case 0:
tab.setText("全部");
tab.setIcon(R.drawable.ic_all_items);
break;
case 1:
tab.setText("支出");
tab.setIcon(R.drawable.ic_expenditure);
break;
default:
tab.setText("收入");
tab.setIcon(R.drawable.ic_income);
break;
}
});
mediator.attach();
return root;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
private void show() {
type = "餐饮";
inOut = true;
Dialog dialog = new Dialog(this.getContext());
//去掉标题线
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
View view = LayoutInflater.from(this.getContext()).inflate(R.layout.add_dialog, null, false);
dialog.setContentView(view);
//背景透明
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
dialog.show();
Window window = dialog.getWindow();
WindowManager.LayoutParams lp = window.getAttributes();
lp.gravity = Gravity.CENTER; // 居中位置
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
window.setAttributes(lp);
window.setWindowAnimations(R.style.animStyle); //添加动画
ViewPager2 viewPager3 = view.findViewById(R.id.typePage);
viewPager3.setAdapter(new TypePageAdapter(this));
TabLayout tabLayout1 = view.findViewById(R.id.typeTab);
TabLayoutMediator mediator = new TabLayoutMediator(tabLayout1, viewPager3, (tab, position) -> {
if (position == 0)
tab.setText("支出");
else
tab.setText("收入");
});
mediator.attach();
TextView chooseDate = view.findViewById(R.id.chooseDate);
EditText moneyTxt = view.findViewById(R.id.money);
EditText remarkTxt = view.findViewById(R.id.remarkText);
Button addBt = view.findViewById(R.id.addBt);
chooseDate.setOnClickListener(v -> {
//获取实例,包含当前年月日
Calendar calendar = Calendar.getInstance();
DatePickerDialog dialog1 = new DatePickerDialog(getActivity(), (view1, year, month, dayOfMonth) -> {
chooseDate.setText(String.format(getString(R.string.chooseDate), year, month + 1, dayOfMonth));
// Toast.makeText(getContext(), year + "-" + (month + 1) + "-" + dayOfMonth, Toast.LENGTH_SHORT).show();
date[0] = year;
date[1] = month + 1;
date[2] = dayOfMonth;
},
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH));
dialog1.show();
});
moneyTxt.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
//如果第一个数字为0,第二个不为点,就不允许输入
if (s.toString().startsWith("0") && s.toString().trim().length() > 1) {
if (s.toString().charAt(1) != '.') {
moneyTxt.setText(s.subSequence(0, 1));
moneyTxt.setSelection(1);
return;
}
}
//如果第一为点,直接显示0.
if (s.toString().startsWith(".")) {
moneyTxt.setText("0.");
moneyTxt.setSelection(2);
return;
}
//限制输入小数位数(2位)
if (s.toString().contains(".")) {
if (s.length() - 1 - s.toString().indexOf(".") > 2) {
s = s.toString().subSequence(0, s.toString().indexOf(".") + 2 + 1);
moneyTxt.setText(s);
moneyTxt.setSelection(s.length());
}
if (s.toString().indexOf(".") > 8) {
s = s.toString().substring(0, s.toString().indexOf(".") - 1) + s.toString().substring(s.toString().indexOf("."), s.length());
moneyTxt.setText(s);
moneyTxt.setSelection(s.length());
}
}
//限制整数部分长度
if (!s.toString().contains(".")) {
if (s.length() > 8) {
moneyTxt.setText(s.subSequence(0, s.length() - 1));
moneyTxt.setSelection(s.length() - 1);
}
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
addBt.setOnClickListener(v -> {
if (String.valueOf(chooseDate.getText()).equals("今天")) {
date[0] = Calendar.getInstance().get(Calendar.YEAR);
date[1] = Calendar.getInstance().get(Calendar.MONTH) + 1;
date[2] = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
}
remark = String.valueOf(remarkTxt.getText());
double mn = String.valueOf(moneyTxt.getText()).equals("") ? 0 : Double.parseDouble(String.valueOf(moneyTxt.getText()));
moneyAcc = (long) (mn * 100);
Log.d("TAG", "onClick: " + mn + " " + moneyAcc);
new Thread(() -> {
// 获取 SharedPreferences 实例
SharedPreferences sharedPreferences = requireActivity().getSharedPreferences("MyPreferences", MODE_PRIVATE);
String account = sharedPreferences.getString("account", null); // 如果键不存在,返回 null
String password = sharedPreferences.getString("password", null); // 如果键不存在,返回 null
if (account != null && password != null) {
DatabaseAction.getInstance(getContext()).getAllIncomesDao().insert(new MyDatabase(moneyAcc, date[0], date[1], date[2], type, remark, account, inOut));
AllItems.allItemList = DatabaseAction.getInstance(getContext()).getAllIncomesDao().getAllAccounts(account);
Message msg = new Message();
msg.what = AllItems.COMPLETED;
AllItems.handler.sendMessage(msg);
Message msg2 = new Message();
msg2.what = AllItems.COMPLETED;
if (inOut) {
Expenditure.expenditureList = DatabaseAction.getInstance(getContext()).getAllIncomesDao().getAllExpense(account);
Expenditure.handler.sendMessage(msg2);
} else {
Income.incomeList = DatabaseAction.getInstance(getContext()).getAllIncomesDao().getAllIncomes(account);
Income.handler.sendMessage(msg2);
}
Looper.prepare();
Toast.makeText(getContext(), "添加成功", Toast.LENGTH_SHORT).show();
Looper.loop();
} else {
}
}).start();
dialog.dismiss();
});
}
}
class ViewPagerAdapter extends FragmentStateAdapter {
public ViewPagerAdapter(@NonNull Fragment fragment) {
super(fragment);
}
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
return AllItems.newInstance("fragment1", "f1");
case 1:
return Expenditure.newInstance("fragment2", "f2");
default:
return Income.newInstance("fragment3", "f3");
}
}
@Override
public int getItemCount() {
return 3;
}
}
class TypePageAdapter extends FragmentStateAdapter {
public TypePageAdapter(@NonNull Fragment fragment) {
super(fragment);
}
@NonNull
@Override
public Fragment createFragment(int position) {
return (position == 0) ? AddExpenditure.newInstance("fragment4", "f4") : AddIncome.newInstance("fragment5", "f5");
}
@Override
public int getItemCount() {
return 2;
}
}
五、项目源码
👇👇👇👇👇快捷方式👇👇👇👇👇