开源地址:todolist卓版本reminder(提醒事项):https://gitee.com/kjiskyz/reminder
请开发一个Android的计划管理APP。要求对计划进行全面的管理,包括列表、查询、订制、编辑和删除。一个计划包括开始时间、结束时间、文字描述、图片、附件、重要程度、完成情况等。APP会提示用户即将开始的计划,超过计划结束时间后要求用户输入完成情况。计划可以提前完成,也可以延期。数据保存在本地的sqlite数据库中,图片和附件以文件的形式保存于手机文件系统中。请合理使用组件进行开发,并要求使用Androidx的组件。开发语言要求使用Kotlin。
目录
计划管理APP
功能
列表功能和数据库实现
数据库实现
列表功能
查询功能
订制功能/新建提醒事项与编辑功能
提醒功能
左滑删除
展示/隐藏已完成
计划管理APP
功能
列表
上拉查询(数据保存在本地的sqlite数据库)
订制
编辑(开始时间、结束时间、文字描述、图片、附件、重要程度、完成情况)
左滑删除
提醒
显示/隐藏已完成
列表功能和数据库实现
数据库实现
定义数据类
使用了@Parcelize注解,这意味着它可以被自动地转换为Parcelable,从而可以在Intent中传递
imageurl和fileurl用于保存图片和附件在手机文件系统中的地址
@Parcelize
data class detail(
var title: String, //标题
val description: String? =null, //描述
var isChecked: Boolean = false, //是否完成
var begindate: Long? = null,
var enddate: Long?=null,
var begintime:Long?=null,
var endtime:Long?=null,
var priority:Int?=3, //默认无优先级
var id:Int?=null,
var imageUrl:String?=null,
var fileUrl:String?=null,
): Parcelable
创建数据库
数据库要实现查询,更新,添加,删除等功能
首先,它们都需要一个Context对象来创建DatabaseHelper,然后通过DatabaseHelper获取SQLiteDatabase对象。然后,它们都会根据需要对数据库进行查询、插入、更新或删除操作。
通过游标cursor实现查询功能,通过添加/覆盖contentvalues的值实现添加和更新功能
列表功能
使用recyclerview进行列表展示,首先定义两种type,第一种为正常的列表展示listitem,第二种为底部footview用于添加新事项,现在适配器的getitemviewtype中定义类型,数据的底部定义为footview。
如果视图类型是TYPE_ITEM,那么就从listitem.xml布局文件中创建一个新的视图;如果视图类型是TYPE_FOOTER,那么就从footview.xml布局文件中创建一个新的视图,并将这个视图传递给FooterViewHolder的构造函数,创建一个FooterViewHolder实例。
override fun getItemViewType(position: Int): Int {
return if (position == DetailList.size) {
TYPE_FOOTER
} else {
TYPE_ITEM
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == TYPE_ITEM) {
val view = LayoutInflater.from(parent.context).inflate(R.layout.listitem, parent, false)
ViewHolder(view)
} else {
val view = LayoutInflater.from(parent.context).inflate(R.layout.footview, parent, false)
FooterViewHolder(view)
}
}
在onbindviewholder实现具体逻辑在mainactivity绑定recyclerview
首先获取数据库的所有数据,定义列表为线性布局注册一个用于处理从Activity返回的结果的回调。如果返回的结果码是RESULT_OK,则调用updateRecyclerView()函数更新RecyclerView。创建一个新的Recycleadapter,将查询到的数据、结果处理器和一个函数引用(用于设置FloatingActionButton的可见性)传递给它。
private fun setupRecyclerView() {
val detailList = queryDataFromDatabase(this)
val layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
// registerForActivityResult()方法的第一个参数是一个ActivityResultContract对象,这个对象负责创建Intent,
// 当Activity结束时,会将Intent传递给onActivityResult()方法。
resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
updateRecyclerView()
}
}
// Recycleadapter是自己写的一个类,用于将数据和视图绑定,这里的adapter就是Recycleadapter的一个实例
val adapter =
Recycleadapter(detailList, resultLauncher, ::setFloatingActionButtonVisibility)
recyclerView.adapter = adapter
// ItemTouchHelper是一个辅助类,用于实现RecyclerView的拖拽和滑动删除功能
val itemTouchHelperCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
// onMove()方法用于实现拖拽功能,这里不需要,所以返回false
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
// onSwiped()方法用于实现滑动删除功能
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
detailList.removeAt(position)
recyclerView.adapter!!.notifyItemRemoved(position)
// 删除数据库中的数据
deleteDataFromDatabase(this@MainActivity, position)
}
}
// 将ItemTouchHelper和RecyclerView绑定
val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
查询功能
使用NestedScrollView将主界面的layout包裹起来,实现上滑搜索设置NestedScrollView的滚动监听。当滚动位置(scrollY)小于等于0时,searchView可见,checkmore按钮不可见。当滚动位置大于100时,searchView不可见,checkmore按钮可见。这样可以在滚动时改变这两个控件的可见性。设置searchView的查询文本监听。当查询文本改变时,调用updateRecyclerView(newText)函数来更新RecyclerView。这样可以实现搜索功能,即根据用户在searchView中输入的文本来过滤RecyclerView中的数据。updateRecyclerView调用setData(filteredList)方法将过滤后的数据设置到适配器中,设置showFooter属性的值为filter是否为空或空字符串,然后调用notifyDataSetChanged()方法通知RecyclerView数据已更改
private fun setupSearchView() {
val nestedScrollView = findViewById
nestedScrollView.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, _ ->
if (scrollY <= 0) {
searchView.visibility = View.VISIBLE
checkmore.visibility = View.GONE
} else if (scrollY > 100) {
searchView.visibility = View.GONE
checkmore.visibility = View.VISIBLE
}
})
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
updateRecyclerView(newText)
return false
}
})
}
private fun updateRecyclerView(filter: String? = null) {
val detailList = queryDataFromDatabase(this)
// 如果filter不为空,就将detailList中的数据过滤,只保留包含filter的数据
// 用于查询功能,不为空就只保存输入框里的内容进行查询
val filteredList = if (filter != null) {
detailList.filter { it.title.contains(filter) }
} else {
detailList
}
(recyclerView.adapter as Recycleadapter).apply {
setData(filteredList)
showFooter = filter.isNullOrEmpty()
notifyDataSetChanged()
}
}
订制功能/新建提醒事项与编辑功能
方法一:在列表最下方有footview,输入标题后点击最右边的图标,会进入到编辑页面;方法二:通过主页面的底部的悬浮按钮添加新事项当点击addbutton时,执行以下操作: 获取addtext中的文本,作为标题。如果标题为空,显示一个Toast消息"标题不能为空"。如果DetailList中存在一个未完成的任务,其标题与输入的标题相同,显示一个Toast消息"标题已存在",并清空addtext。否则,创建一个新的detail对象,将其添加到数据库和DetailList中,通知RecyclerView插入了一个新的item,然后清空addtext。点击事项右侧的图标后(或通过悬浮按钮),跳转到detail activity中,在detail activity中获取到detail实例后进行初始化
如果有标题、开始日期、结束日期、开始时间、结束时间、图片、附件的,则初始化显示通过DatePickerDialog设置开始日期和结束日期的日期选择功能通过TimePickerDialog设置开始时间和结束时间的选择功能通过Spinner设置优先级的选择,首先创建一个array,存放要展示的列表,创建一个ArrayAdapter,用于将数组和spinner绑定,获取用户选择的优先级后更新数据库
// 设置优先级
val priority = findViewById
// 创建一个ArrayAdapter,用于将数组和spinner绑定
val adapter_spinner = ArrayAdapter.createFromResource(
this,
R.array.planets_array,
android.R.layout.simple_spinner_item
)
// 设置下拉列表的风格
adapter_spinner.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
priority.adapter = adapter_spinner
// 设置优先级的默认值
priority.setSelection(detail?.priority ?: 3)
priority.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>,
view: View?, position: Int, id: Long
) {
parent as Spinner
detail?.priority = position // 保存优先级
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
添加图像与图片,添加附件。
用户选择拍照/选择图片,如果用户选择"拍照",会检查应用是否有相机权限。如果没有,会请求相机权限。如果有,会创建一个新的文件用于存储拍照后的图片,然后启动相机应用。拍照后的图片会保存到刚刚创建的文件中。 如果用户选择"选择图片",代码会启动一个用于选择图片的应用。用户可以从这个应用中选择一张图片,然后这个图片的URI会作为结果返回。 当这两个应用结束后,会调用当前Activity的onActivityResult方法,可以在这个方法中获取拍照后的图片或用户选择的图片。当用户点击添加附件按钮时,会创建一个新的Intent,并设置其动作为Intent.ACTION_GET_CONTENT。这意味着这个Intent将用于获取用户选择的文件。然后,设置Intent的类型为"/",这意味着这个Intent可以选择任何类型的文件。最后,使用startActivityForResult方法启动这个Intent,并等待用户选择文件。当用户选择了文件后,会调用当前Activity的onActivityResult方法,可以在这个方法中获取用户选择的文件。在用户选择完照片/拍照/上传完附件后,打开一个输入流,用于读取用户拍摄的照片。 创建一个新的文件,用于保存用户拍摄的照片。打开一个输出流,用于写入用户拍摄的照片。 将输入流的内容复制到输出流,即将用户拍摄的照片写入到文件中。关闭输入流和输出流。
if (requestCode == 1 && resultCode == RESULT_OK) {
val userimage = findViewById
// 从文件中读取图片
userimage.setImageURI(imageUri)
userimage.visibility = View.VISIBLE
// 随机生成文件名
val filename = "userimage${System.currentTimeMillis()}.jpg"
val inputStream = contentResolver.openInputStream(imageUri) //获取图片的输入流
val file = File(filesDir, filename) //创建文件
val outputStream = FileOutputStream(file) //创建输出流
inputStream?.copyTo(outputStream) //将输入流复制到输出流
inputStream?.close() //关闭输入流
outputStream.close() //关闭输出流
// 将文件名保存
val detail = intent.getParcelableExtra
if (detail != null) {
detail.imageUrl = filename
// 更新数据库
val id = queryIdFromDatabase(this, detail)
updateDatabase(this, detail, id)
}
}
提醒功能
使用worker进行后台处理,提醒事项的即将开始,和结束。首先从数据库中查询所有的计划,然后过滤出即将开始和已经结束的计划。对于每个即将开始的计划,发送一个"你的计划即将开始"的通知;对于每个已经结束的计划,发送一个"你的计划已经结束,请输入完成情况"的通知。在mainactivity启动后台任务
override fun doWork(): Result {
val allPlans = queryDataFromDatabase(applicationContext)
val currenttime = System.currentTimeMillis()
val upcomingPlans = allPlans.filter { plan ->
plan.begindate?.let { it != 0.toLong() && it - currenttime < 86400000 && it > currenttime && !plan.isChecked }
?: false
}
val overPlans = allPlans.filter { plan ->
plan.enddate?.let { it != 0.toLong() && it < currenttime && !plan.isChecked } ?: false
}
upcomingPlans.forEach {
showNotification(
applicationContext,
it,
"你的${it.title}计划即将开始"
)
}
overPlans.forEach {
showNotification(
applicationContext,
it,
"你的${it.title}计划已经结束,请输入完成情况"
)
}
return Result.success()
}
左滑删除
创建一个ItemTouchHelper.SimpleCallback对象,这个对象定义了当用户在RecyclerView的item上执行拖拽或滑动操作时应该执行的操作。当一个item被滑动时,会获取这个item的位置,然后从detailList中移除这个item,通知适配器这个item已经被移除,并从数据库中删除这个item
private fun setupRecyclerView() {
val detailList = queryDataFromDatabase(this)
val layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
// ItemTouchHelper是一个辅助类,用于实现RecyclerView的拖拽和滑动删除功能
val itemTouchHelperCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
// onMove()方法用于实现拖拽功能,这里不需要,所以返回false
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
// onSwiped()方法用于实现滑动删除功能
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
detailList.removeAt(position)
recyclerView.adapter!!.notifyItemRemoved(position)
// 删除数据库中的数据
deleteDataFromDatabase(this@MainActivity, position)
}
}
// 将ItemTouchHelper和RecyclerView绑定
val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
展示/隐藏已完成
当用户点击"showfinished"菜单项时,会切换clickshowfinished的值,然后获取RecyclerView的适配器,并将适配器的setShowFinished属性设置为clickshowfinished的值。然后,根据clickshowfinished的值来更新"showfinished"菜单项的标题。最后,通知适配器数据已更改
// mainactivity
private fun setupCheckMoreButton() {
var clickshowfinished = false
var showFinishedTitle = "显示已完成的任务"
checkmore.setOnClickListener {
val popupMenu = PopupMenu(this, it)
popupMenu.menuInflater.inflate(R.menu.popup_menu, popupMenu.menu)
popupMenu.menu.findItem(R.id.showfinished).title = showFinishedTitle
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.showfinished -> {
clickshowfinished = !clickshowfinished
val adapter = recyclerView.adapter as Recycleadapter
adapter.setShowFinished = clickshowfinished
showFinishedTitle =
if (clickshowfinished) "隐藏已完成的任务" else "显示已完成的任务"
adapter.notifyDataSetChanged()
true
}
else -> false
}
}
popupMenu.show()
}
}
//adapter
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder) {
val detail = DetailList[position]
// 默认隐藏已经完成的item,只有点击查看已完成的任务才会显示
if (setShowFinished) {
holder.itemView.visibility = View.VISIBLE
holder.itemView.layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
) //设置布局参数,宽度为match_parent,高度为wrap_content
}
文章来源
发表评论