# sticky-note **Repository Path**: wshzard/sticky-note ## Basic Information - **Project Name**: sticky-note - **Description**: Android 便签软件 - **Primary Language**: Unknown - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 5 - **Created**: 2022-05-09 - **Last Updated**: 2022-06-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Sticky Note - Android 便签软件 | 项目 | 信息 | | ---- | ---- | | 项目地址 | https://gitee.com/wshzard/sticky-note | | 软件包名 | com.team6.stickynote | | minSDK / targetSDK | 21 (Android 5.0) / 32 (Android 12L) | ## 开发人员 | 用户名 | 姓名 | | ---- |--| | wshzard | 吴世昊 | | Swchun | 孙万春 | | EEEEEE | 鲍云达 | | fstring | 范俊宝 | | EnjoyFailure | 蒲志立 | # 需求分析 现在网络上各种文档编辑器数不胜数。功能也是应有尽有,五花八门,有能改变字体的,有能改变文字颜色的。 但是,仅从日常应用方面来说,一个文本编辑器只需一些简单实用的功能就够了。 本程序设计就是依照这样一种使用需要设计了一个简单且轻量的记事本程序。 # 软件功能 实现添加有标题和文本的便签,同时实现编辑、删除和修改功能。 # 系统的功能表 查看 Git 提交记录。 # 软件架构设计 ## App 类 ```java public class App extends Application { /** * 全局静态 App Context,在 App onCreate 赋值,不存在内存泄漏 */ @SuppressLint("StaticFieldLeak") public static Context context; /** * 全局数据库访问,不会有内存泄露,如 room 数据库框架一般使用的单例 AppDatabase 类,内部进行 Dao 操作,持有 Application 的上下文(Context) */ @SuppressLint("StaticFieldLeak") public static DBAdapter database; @Override public void onCreate() { super.onCreate(); context = getApplicationContext(); database = new DBAdapter(context); database.open(); } } ``` 方便了对数据库的访问,以及封装 Uitl 工具类 ToastUtil ,将 toast 持有 App 的 context ,调用的时候就减少了一个参数的传递,更方便,也不会产生内存泄漏。 ## 基类 BaseActivity 设计 定义了 abstract 类,方便 initViewBinding 在实体类定义。按照 Android 官方推荐使用 ViewBinding 减少大量 findViewById 方法。 ## MVVM 架构 软件在多个界面使用 MVVM 架构,使用了 ViewModel ,由用户向 ViewModel 发起操作,ViewModel 向数据库请求数据返回赋值给 ViewModel 内数据,UI 负责监听数据显示给用户。 本 APP MainActivty 示例架构图: ![MVVM](image/mvvm.bmp) #### MainActivity 数据显示 ```java // Sticky Note 适配器 stickyNoteAdapter = new StickyNoteAdapter(new StickyNoteDiffCallback()); ``` # 数据库设计 本数据库使用了sqlite数据库。 设计了StickyNoteBean.class为基本数据集,里面包含了id(long),title(String),text(String)以及获取和设置的方法 通过DBAdapter类设计了名为sticky.db的数据库,里面含有data数据表,表里包含"_id","_title","_text",属性 同时设置_id为主键(自增),然后实现了数据库的打开(open())、关闭(close())、添加(insert())、查询(queryAllData()、queryOneData()) 删除( deleteAllData()、deleteOneData())、更新(updateData())。 |字段|类型|约束|备注| | -- |--|--|--| |_id | long |主键|唯一不重复| |_title|String| | | |_text|String| | | # 布局设计 ## RecyclerView 该 App 采用 RecyclerView的形式建立界面布局,通过修改自定义Adapter的布局管理器,实现ListView布局、GridView 布局和 StaggerView 布局。 ### 自定义的 Adapter 适配器 使用了 RecyclerView 的 ListAdapter ,对于列表的数据显示更加友好,同时通过实现 Diff 进行数据变更判断进行局部数据更新,也自带了动画效果。 ```java public class StickyNoteAdapter extends ListAdapter { // ... } ``` StickyNoteAdapter 用于对从数据库中读出的数据进行一系列预处理并显示到 UI 。 #### ViewHolder ```java class ViewHolder extends RecyclerView.ViewHolder { /*...代码段...*/ } ``` ViewHolder为一个自定义的帮助类,用于封装数据,即其包含每个Item的View样式以及数据等。 #### onCreateViewHolder ```java public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { // 获取自定义的 Sticky 的 View View view = View.inflate(parent.getContext(), R.layout.sticky_layout, null); // 把 View 发给 holder return new ViewHolder(view); } ``` 该方法用于创建Item的样式view,并new一个ViewHoder实例,将view传给它。 #### onBindViewHolder ```java public void onBindViewHolder(@NonNull ViewHolder holder, int position) { StickyNoteBean stickyNoteBean = getItem(position); holder.selectedStickyNoteBean = stickyNoteBean; holder.tvSticky.setText(stickyNoteBean.getText()); } ``` 该方法用于将数据绑定到该holder的view上去,position代表数据的位置。 ### 实现ListView布局效果 ```java //用于实现ListView布局 void showList(boolean isVertical) { //创建布局管理器 LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); //设置方向定位 if (isVertical) { linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); } else { linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); } //设置布局管理器 binding.rvStickyNote.setLayoutManager(linearLayoutManager); } ``` 调用showList方法即可实现ListView布局的效果,参数isvertical用于控制垂直布局还是水平布局,可由用户自行选择。 其实现逻辑是:通过创建线性布局管理器并设置好布局方向,最后赋给适配器StickyNoteAdapter的对象rvStickyNote即可。 ### 实现GridView布局效果 ```java // 用于实现 Grid 布局 void showGrid(boolean isvertival) { //创建布局管理器 GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 2); //设置方向定位 if (isvertival) { gridLayoutManager.setOrientation(GridLayoutManager.VERTICAL); } else { gridLayoutManager.setOrientation(GridLayoutManager.HORIZONTAL); } //设置布局管理器 binding.rvStickyNote.setLayoutManager(gridLayoutManager); } ``` 调用showGrid方法即可实现GridView布局的效果,参数isvertical用于控制垂直布局还是水平布局,可由用户自行选择。 其实现逻辑是:通过创建网格布局管理器并设置好布局方向,最后赋给适配器StickyNoteAdapter的对象rvStickyNote即可。 ### 实现StaggerView布局效果 ```java // 用于实现瀑布流布局 private void showStagger(boolean isVertical) { //创建布局管理器 int ORIENTATION = isVertical ? StaggeredGridLayoutManager.VERTICAL : StaggeredGridLayoutManager.HORIZONTAL; StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, ORIENTATION); //设置布局管理器 binding.rvStickyNote.setLayoutManager(staggeredGridLayoutManager); } ``` 调用showStagger方法即可实现StaggerView布局的效果,参数isvertical用于控制垂直布局还是水平布局 其实现逻辑是:通过创建瀑布流布局管理器并设置好布局方向,最后赋给适配器StickyNoteAdapter的对象rvStickyNote即可。 ## 图标与 UI 图标与界面布局使用figma绘制,通过矢量图导入为xml文件。 # 数据操作 ## 添加便签 通过在activity_main.xml里面添加一个悬浮按钮(floatingActionButton),当点击按钮时会通过Intent跳转到AddActivity 通过编辑activity_add.xml来实现数据的添加 ## 删除便签 实现思路为:为每一个便签Item添加长按点击事件,当用户长按某一个Item时将弹出一个删除窗口,点击即可从数据库中删除该Item,并更新UI显示界面 ### 在适配器StickyNoteAdapter中定义接口 ```java // 定义长按事件接口 public interface onItemLongClickListener { void onItemLongClick(View view, long id); } public void setOnItemLongClickListener(onItemLongClickListener onItemLongClickListener) { this.onItemLongClickLitener = onItemLongClickListener; } ``` 用户可直接在外部调用适配器的setOnItemLongClickListener方法并重写接口里方法即可自定义长按事件,接着在Item的长按事件中 调用该接口里的方法即可。如下: ```java //为当前View设置长按事件 itemView.setOnLongClickListener(v -> { if (onItemLongClickLitener != null) { // 获取当前选中的 long id = selectedStickyNoteBean.getId(); onItemLongClickLitener.onItemLongClick(v, id); // 调用定义的接口的事件函数 } return true; }); ``` ### 利用PopupMenu实现弹出窗口并删除数据 ```java // Sticky Note 适配器 stickyNoteAdapter = new StickyNoteAdapter(new StickyNoteDiffCallback()); stickyNoteAdapter.setOnItemLongClickListener((view, id) -> { //长按事件 //为该View设置弹出框 PopupMenu popupMenu = new PopupMenu(MainActivity.this, view); popupMenu.getMenuInflater().inflate(R.menu.delete_menu, popupMenu.getMenu()); // 设置样式 popupMenu.show(); // 显示 //设置点击事件 popupMenu.setOnMenuItemClickListener(item -> { switch (item.getItemId()){ case R.id.delete_it: // 删除该item ToastUtil.toast("删除成功!"); App.database.deleteOneData(id); // 更新显示数据 mainViewModel.updateStickyNoteBeans(); break; } return true; }); }); ``` 在外部调用适配器的setOnItemLongClickListener方法重写接口方法时,实例化一个PopupMenu对象,并将自定义的view样式赋给该对象。 接着为该对象设置点击事件,在点击事件中实现删除Item的功能并更新UI显示界面。 ## 修改编辑便签 通过调用数据库中的更新数据函数,更改便签相关内容,并进行保存。 ```java // save 函数保存数据 private void save() { if (stickyNoteBean != null) { String title = binding.etTitle.getText().toString(); String content = binding.etContent.getText().toString(); if (TextUtils.isEmpty(title)) { ToastUtil.toast( "标题不能为空!"); return; } stickyNoteBean.setTitle(title); stickyNoteBean.setText(content); long id=stickyNoteBean.getId(); long rowId = App.database.updateOneData(id,stickyNoteBean); if (rowId != -1) { ToastUtil.toast("修改成功!"); this.finish(); } else { ToastUtil.toast("修改失败!"); } } else { ToastUtil.toast("当前编辑的标签不存在!"); } } ``` ```java // 调用数据库中updata函数 stickyNoteBean.setTitle(title); stickyNoteBean.setText(content); long id=stickyNoteBean.getId(); long rowId = App.database.updateOneData(id,stickyNoteBean); if (rowId != -1) { ToastUtil.toast("修改成功!"); this.finish(); } else { ToastUtil.toast("修改失败!"); } ``` ## 搜索便签 通过 MainActivity ivSearch 打开 SearchActivity 。 为 EditText 添加 TextWatcher ,感知每次的文本变化,调用 SearchViewModel 的搜索功能,就不用用户自行点击搜索按钮。 ```java public void search(String keyword) { List searchResult = new ArrayList<>(); for (int i = 0; i < repoStickyNoteBeans.size(); i++) { StickyNoteBean stickyNoteBean = repoStickyNoteBeans.get(i); Log.d(TAG, "i = " + i + ", title = " + stickyNoteBean.getTitle() + ", text = " + stickyNoteBean.getText()); if (stickyNoteBean.getTitle().contains(keyword) || stickyNoteBean.getText().contains(keyword)) { searchResult.add(stickyNoteBean); } } _stickyNoteBeans.setValue(searchResult); } ``` 这儿并未采取通过读取数据库的方式,因为搜索可能是个高频操作,这样的内存搜索读取可能更快(多次文本输入判断),不过当数据量很大的化可能还是需要通过访问数据库的方式,是后续的一个优化点。