# android_richtext **Repository Path**: Guguchiken/android_richtext ## Basic Information - **Project Name**: android_richtext - **Description**: 安卓期末作业 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2024-06-02 - **Last Updated**: 2024-11-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Android移动互联网开发 ## 主要功能 **记笔记功能** - 用户可以创建、编辑、删除笔记。 - 笔记可以包含富文本(可以调整文本加粗、斜体、字体大小、字体颜色...)和图片(通过摄像头拍摄或者从本地选择图片)。 - 用户可以为笔记添加标签,也可以根据标签来筛选笔记 - 用户可以置顶笔记 **待办事项功能** - 用户可以创建、编辑、删除待办事项。 - 可以设置待办事项的提醒时间,到时通过通知提醒用户。 ## 组件设计 1. **Activity** - 主Activity:展示主界面,管理笔记和待办事项列表两个fragment。 - 笔记编辑Activity:展示编辑笔记的页面,用于设置笔记的标题和内容 2. **Service** - 用于待办定时提醒功能中,负责在特定的时间启动AlarmManager。 3. **Broadcast Receiver** - 用于待办定时提醒功能中,接收AlarmManager的闹钟事件,并发送通知。 ## 传感器使用 1. 摄像头 - 在笔记编辑Activity中,用户可以通过摄像头拍摄照片并添加到笔记中。 ## 数据库实现 采用Room库来实现,它是Android开发中用于在SQLite数据库上进行简化操作的一种持久性库。 ## 代码解释 ### 富文本编辑器 采用Edit Text + Span的方式来实现,再通过HTML渲染出来。 以文本加粗为例子。 ``` kotlin btnBold.setOnClickListener { val start = editText.selectionStart val end = editText.selectionEnd if (start < end) { val spannableText = SpannableStringBuilder(editText.text) spannableText.setSpan( StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) editText.setText(spannableText) editText.setSelection(end) } } ``` ```kotlin fun toHtml(editText: EditText): String { val spannable = editText.text as Spannable val spanned: Spanned = spannable return Html.toHtml(spanned, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) } ``` 通过使用`Spannable`类,可以方便地对文本进行各种样式的应用,如加粗、斜体、下划线等,并且可以将其转换为HTML格式。 ### 数据库实现 采用room库,以待办事项数据存储(Todo)为例。 实体类 ```kotlin @Entity data class Todo( var title: String?, var todoTime: LocalDateTime, var isFinished: Boolean, ) { @PrimaryKey(autoGenerate = true) var id: Long = 0 } ``` ```kotlin @Dao interface TodoDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun addTodo(todo: Todo): Long @Delete fun deleteTodo(todo: Todo) @Query("select * from Todo ORDER BY todoTime DESC") fun queryTodoAll(): MutableList @Query("select * from Todo where id = :id") fun queryTodoById(id: Long): Todo? } ``` ```kotlin @Database(entities = [Todo::class], version = 1, exportSchema = false) @TypeConverters(Converters::class) abstract class TodoDatabase : RoomDatabase() { companion object { @Volatile private var INSTANCE: TodoDatabase? = null val instance: TodoDatabase get() { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( AppApplication.context!!, TodoDatabase::class.java, "todo_list_database" ) .allowMainThreadQueries() .build() INSTANCE = instance instance } } } abstract fun getTodoDao(): TodoDao } ``` ### 待办定时提醒功能 用户在增加/更改待办时,会启动`NotificationService`设置一个AlarmManager来在特定时间触发。`AlarmReceiver`用于接收AlarmManager的闹钟事件,并发送通知。用户在编辑待办后,会取消对应的旧Service,再启动新的Service。 ```kotlin class NotificationService : Service() { private var requestCode = 0 override fun onBind(intent: Intent?): IBinder? { return null } @RequiresApi(Build.VERSION_CODES.O) override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val dateTimeString = intent?.getStringExtra("dateTime") val todoId = intent?.getStringExtra("todoId") if (dateTimeString != null) { val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME val dateTime = LocalDateTime.parse(dateTimeString, formatter) //检查待办的提醒时间是否早于当前的时间 if (dateTime.isBefore(LocalDateTime.now())) { stopSelf() return START_NOT_STICKY } val calendar = Calendar.getInstance().apply { timeInMillis = dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() } val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager val alarmIntent = Intent(this, AlarmReceiver::class.java).apply { //保证每次通知的请求码不同 requestCode ++ putExtra("requestCode", requestCode) putExtra("todoId", todoId) } val pendingIntent = PendingIntent.getBroadcast(this, requestCode, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT) alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent) } return START_STICKY } } ``` ```kotlin class AlarmReceiver : BroadcastReceiver() { private val todoDao = TodoDatabase.instance.getTodoDao() @RequiresApi(Build.VERSION_CODES.O) override fun onReceive(context: Context, intent: Intent) { val notificationIntent = Intent(context, TodoActivity::class.java) val pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) //获取请求码,待办在数据库中的id val requestCode = intent.getIntExtra("requestCode", 0) val todoId = intent.getStringExtra("todoId") val todoTitle = todoId?.let { todoDao.queryTodoById(it.toLong())?.title } Log.d("AlarmReceiver code", requestCode.toString()) //发送通知 val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel("default", "Default Channel", NotificationManager.IMPORTANCE_DEFAULT) notificationManager.createNotificationChannel(channel) } val notification = NotificationCompat.Builder(context, "default") .setContentTitle("待办通知") .setContentText("待办" + todoTitle +"未完成") .setSmallIcon(R.drawable.ic_notification) .setContentIntent(pendingIntent) .setAutoCancel(true) .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .build() notificationManager.notify(requestCode, notification) } } ``` #### 笔记置顶功能 实体类有一个属性`isTop`来表示是否置顶,每次置顶或者取消置顶,就更新adapter中的数组数据,排序优先把`isTop`的元素置于前面。 ````kotlin private fun toggleTopState(id: Long) { val position = data.indexOfFirst { it.id == id } if (position != -1) { val item = data[position] item.isTop = !item.isTop // 更新数据库中的记录 jobScope.launch { memoDao.addMemo(item) } // 重新排列列表 val oldPosition = position sortData() val newPosition = data.indexOfFirst { it.id == id } notifyItemMoved(oldPosition, newPosition) notifyItemChanged(newPosition) } } private fun sortData() { data.sortWith(compareByDescending { it.isTop }.thenByDescending { it.modifiedTime }) } ```` #### 标签分类功能 标签选择窗口包含一个`RecyclerView`,创建`RecyclerView`的`Adapter`。在`Adapter`中,为`TextView`设置点击事件,并在点击事件中调用回调接口以将点击的项的标题传递给Fragment。 ```kotlin val rvClassificationList = classificationDialog.findViewById(R.id.rv_classification_list) rvClassificationList.layoutManager = LinearLayoutManager(activity) classificationAdapter = ClassificationAdapter(classificationDao.queryClassificationAll()) { //根据tag筛选 val filteredList : MutableList = memoDao.queryMemoAll().filter { memo -> memo.tag == it }.toMutableList() //更新当前Fragment的adapter中的数据 (binding.rvMemoList.adapter as MemoAdapter).setNewData(filteredList) dialog.dismiss() } rvClassificationList.adapter = classificationAdapter ``` ```kotlin binding.classificationTitle.setOnClickListener{ classification.title?.let { it1 -> onItemClick.invoke(it1) } } ``` ## 第三方控件使用 侧滑菜单 https://github.com/aitsuki/SwipeMenuRecyclerView 时间选择器 https://github.com/Gredicer/datetimepicker 底部导航栏 https://github.com/akshay2211/BubbleTabBar/tree/master