Android实战:手机笔记App(二)
Android实战:手机笔记App(一)
接着上一章内容
创建首页界面对应的ViewModel——NotesViewModel
触发事件的封装
这里包括首页的所有操作:指令,删除Note,恢复刚删除的Note、切换指令的可视化
sealed class NotesEvent{
data class Order(val noteOrder: NoteOrder):NotesEvent()
data class DeleteNote(val note:Note):NotesEvent()
object RestoreNote:NotesEvent()
object ToggleOrderSection:NotesEvent()
}
@HiltViewModel与@Inject:Hilt实现自动化依赖注入,使我们能够直接构造NotesViewModel对象,不用再构造noteUseCase类型的参数。
private val _state与val state:state是直接提供给外部使用的数据流,而_state只能内部使用也就是防止外部对其更改,这种数据流方式是目前常用的开发方式,详情可见:https://developer.android.google.cn/kotlin/flow/stateflow-and-sharedflow?hl=zh_cn
onEvent():UI的触发事件,在compose中调用onEvent()可以触发相应的界面变化或数据变化。
NoteEvent.Order:为什么双重判断中的第一层是::class而不可以去掉呢?因为第一层是判断两个类是否一样(我们使用两层密封类封装了指令NoteOrder(val orderType:OrderType)),而第二层才是判断值。再简单讲一下逻辑:如果外部传来的事件的NoteOrder值与state.noteOrder相同就不用调用getNotes再获取了,而不同的话就需要调用getNotes重新获取notes。
@HiltViewModel
class NotesViewModel @Inject constructor(
private val noteUseCases: NoteUseCases
):ViewModel() {
private val _state = mutableStateOf(NotesState())
val state:State<NotesState> = _state
private var recentlyDeleteNote: Note?= null
private var getNotesJob:Job?=null
init {
getNotes(NoteOrder.Date(OrderType.Descending))
}
fun onEvent(event: NotesEvent){
when(event){
is NotesEvent.Order ->{
if (state.value.noteOrder::class == event.noteOrder::class &&
state.value.noteOrder.orderType == event.noteOrder.orderType){
return
}
getNotes(event.noteOrder)
}
is NotesEvent.DeleteNote ->{
viewModelScope.launch {
noteUseCases.deleteNote(event.note)
recentlyDeleteNote = event.note
}
}
is NotesEvent.RestoreNote ->{
viewModelScope.launch {
noteUseCases.addNote(recentlyDeleteNote ?: return@launch)
recentlyDeleteNote = null
}
}
is NotesEvent.ToggleOrderSection ->{
_state.value = state.value.copy(
isOrderSectionVisible = !state.value.isOrderSectionVisible
)
}
}
}
private fun getNotes(noteOrder: NoteOrder){
getNotesJob?.cancel()
getNotesJob = noteUseCases.getNotes(noteOrder)
.onEach {notes ->
_state.value = state.value.copy(
notes = notes,
noteOrder = noteOrder
)
}.launchIn(viewModelScope)
}
}
创建首页compose(即UI)
文件结构
image.png创建NoteItem
NoteItem
NoteItem首先我们先来分析一下这个Item的样式,再对其进行分解
类似纸片的模型(圆角),右上角给人视觉上效果是折叠,右下角有一个删除的图标,文本由标题与内容组成。
圆角简单,CornerRadius就行。
我们可以把折叠拆解为两部分,底部一个缺角矩形,上面再来一层阴影色的矩形即可,毕竟我们的背景色是黑色,其实更简单一点直接把上面那层也用缺角矩形。
文本是两个Text,问题不大。
右下角一个删除的Icon,并且需要传递一个实现删除操作的函数onDeleteClick: () -> Unit
@Composable
fun NoteItem(
note: Note,
modifier: Modifier = Modifier,
cornerRadius: Dp = 10.dp,
cutCornerSize: Dp = 30.dp,
onDeleteClick: () -> Unit
) {
Box(
modifier = modifier
){
//绘制每一个Item的文本样式
Canvas(modifier = Modifier.matchParentSize()){
//底部样式的path
val clipPath = Path().apply {
lineTo(size.width-cutCornerSize.toPx(),0f)
lineTo(size.width,cutCornerSize.toPx())
lineTo(size.width,size.height)
lineTo(0f,size.height)
close()
}
clipPath(clipPath){
//绘制底部样式图案
drawRoundRect(
color = Color(note.color),
size = size,
cornerRadius = CornerRadius(cornerRadius.toPx())
)
//绘制折叠图案阴影
drawRoundRect(
color = Color(
ColorUtils.blendARGB(note.color,0x000000, 0.2f)
),
topLeft = Offset(size.width-cutCornerSize.toPx(),-100f),
size = Size(cutCornerSize.toPx()+100f,cutCornerSize.toPx()+100f),
cornerRadius = CornerRadius(cornerRadius.toPx())
)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.padding(end = 32.dp)
) {
Text(
text = note.title,
style = MaterialTheme.typography.h6,
color = MaterialTheme.colors.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = note.content,
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface,
maxLines = 10,
overflow = TextOverflow.Ellipsis
)
}
IconButton(
onClick = onDeleteClick,
modifier = Modifier.align(Alignment.BottomEnd)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete note",
tint = MaterialTheme.colors.onSurface
)
}
}
}
指令栏的UI
image.png我们需要将其拆解为两部分,第一部分是每一个选中按钮的样式,第二部分是各项指令的集合
DefaultRadioButton:onSelect:()->Unit同样的,需要在外部调用逻辑功能的Compose组件,需要传递高阶函数。
@Composable
fun DefaultRadioButton(
text:String,
selected:Boolean,
onSelect:() -> Unit,
modifier:Modifier = Modifier
) {
Row (
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
){
RadioButton(
selected = selected,
onClick = onSelect,
colors = RadioButtonDefaults.colors(
selectedColor = MaterialTheme.colors.primary,
unselectedColor = MaterialTheme.colors.onBackground
)
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = text, style = MaterialTheme.typography.body1)
}
}
@Composable
fun OrderSection(
modifier:Modifier = Modifier,
noteOrder: NoteOrder = NoteOrder.Date(OrderType.Descending),
onOrderChange:(NoteOrder) ->Unit
) {
Column(
modifier = modifier
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
DefaultRadioButton(
text ="Title",
selected = noteOrder is NoteOrder.Title,
onSelect = { onOrderChange(NoteOrder.Title(noteOrder.orderType)) }
)
Spacer(modifier = Modifier.width(8.dp))
DefaultRadioButton(
text = "Date",
selected = noteOrder is NoteOrder.Date,
onSelect = { onOrderChange(NoteOrder.Date(noteOrder.orderType)) }
)
Spacer(modifier = Modifier.width(8.dp))
DefaultRadioButton(
text = "Color",
selected = noteOrder is NoteOrder.Color,
onSelect = { onOrderChange(NoteOrder.Color(noteOrder.orderType)) }
)
}
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth()
) {
DefaultRadioButton(
text = "Descending",
selected = noteOrder.orderType is OrderType.Descending,
onSelect = {onOrderChange(noteOrder.copy(OrderType.Descending))}
)
DefaultRadioButton(
text = "Ascending",
selected = noteOrder.orderType is OrderType.Ascending,
onSelect = {onOrderChange(noteOrder.copy(OrderType.Ascending))}
)
}
}
}
这里先展示指令选中更新过程
image.png
首先viewModel.onEvent触发选中功能,OrderSection中相应DefaultRadioButton按钮被触发将对应的值通过高阶函数传递给viewModel,viewModel再根据判断执行getNotes方法,getNotes将更新notes和noteColor的值,从而更新state的值。前端中state.noteOrder的值与DefaultRadioButton中的selected值相比较,点亮具有相同selected值的DefaultRadioButton。
创建NoteScreen
NoteScreen集成了首页的所有compose组件
大部分内容都简单,这里提一下 AnimatedVisibility():这里用来实现点击菜单按钮切换隐藏与显示状态,里面放置着指令集成组件OrderSection。
/**
*@Description
*@Author PC
*@QQ 1578684787
*/
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun NotesScreen(
navController: NavController,
viewModel: NotesViewModel = hiltViewModel()
) {
val state = viewModel.state.value
//悬浮按钮的创建需要Scaffold,而Scaffold需要scaffoldState
val scaffoldState = rememberScaffoldState()
//scaffoldState.snackbarHostState.showSnackbar需要 CoroutineScope
val scope = rememberCoroutineScope()
Scaffold(
//添加AddNote的悬浮按钮
floatingActionButton = {
FloatingActionButton(
onClick = {
navController.navigate(Screen.AddEditNoteScreen.route)
},
backgroundColor = MaterialTheme.colors.primary
) {
Icon(imageVector = Icons.Default.Add, contentDescription = "Add Note")
}
},
scaffoldState = scaffoldState
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Your note",
style = MaterialTheme.typography.h4
)
IconButton(
onClick = {viewModel.onEvent(NotesEvent.ToggleOrderSection)}
) {
Icon(
imageVector = Icons.Default.Sort,
contentDescription = "Sort"
)
}
}
AnimatedVisibility(
visible = state.isOrderSectionVisible,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically()
) {
OrderSection(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
noteOrder = state.noteOrder,
onOrderChange = {
viewModel.onEvent(NotesEvent.Order(it))
}
)
}
Spacer(modifier = Modifier.height(16.dp))
LazyColumn(
modifier = Modifier.fillMaxSize()
){
items(state.notes){note ->
NoteItem(
note = note,
modifier = Modifier
.fillMaxWidth()
.clickable {
navController.navigate(
Screen.AddEditNoteScreen.route +
"?noteId=${note.id}¬eColor=${note.color}"
)
},
onDeleteClick = {
viewModel.onEvent(NotesEvent.DeleteNote(note))
//删除操作触发的同时,触发恢复提示操作
scope.launch {
val result = scaffoldState.snackbarHostState.showSnackbar(
message ="Note deleted!",
actionLabel ="Undo"
)
//如果被点击,则恢复刚删除的Note
if (result == SnackbarResult.ActionPerformed){
viewModel.onEvent(NotesEvent.RestoreNote)
}
}
}
)
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
}