Android实战:手机笔记App(一)
Android实战:手机笔记App(二)
接着前面的内容
创建AddEditNote模块
image.pngAddNote界面触发事件的封装
将对输入标题、标题栏是否有输入聚焦、输入内容、内容栏是否有输入聚焦、改变颜色、保存这些触发事件封装成一类触发事件。
sealed class AddEditNoteEvent{
data class EnteredTitle(val value:String):AddEditNoteEvent()
data class ChangeTitleFocus(val focusState: FocusState):AddEditNoteEvent()
data class EnteredContent(val value: String):AddEditNoteEvent()
data class ChangeContentFocus(val focusState: FocusState):AddEditNoteEvent()
data class ChangeColor(val color:Int):AddEditNoteEvent()
object SaveNote:AddEditNoteEvent()
}
文本类的封装
isHintVisible:确定hint是否可视
data class NoteTextFieldState(
val text:String = "",
val hint:String = "",
val isHintVisible:Boolean = true
)
AddEditNoteViewModel
savedStateHandle:可接受导航传递来的argument参数
image.png
UiEvent:反馈给UI界面的事件,如下图所示,当保存失败时会弹出提示,当保存成功时会导航回上一页。
剩下的不用介绍了,和之前的NoteViewModel差不多。
为什么val _eventFlow = MutableSharedFlow<UiEvent>()此处使用SharedFlow:我们的需求是只希望SnackBar弹出一次,如果使用State那么当我们旋转屏幕的时候,SnackBar将会再次弹出。
提示:注意NoteTextFieldState中的hint与text这两个属性,小编不小心将其中一个hint敲成了text导致后面出现了bug——每次点击+时Title中的hint内容都需要手动删除不会点击后自动消失,弄的我头痛啊!!!找了半天最后无意间才发现的。
@HiltViewModel
class AddEditNoteViewModel @Inject constructor(
private val noteUseCases: NoteUseCases,
savedStateHandle: SavedStateHandle
):ViewModel() {
private val _noteTitle = mutableStateOf(NoteTextFieldState(
hint = "Enter title..."
))
val noteTitle:State<NoteTextFieldState> = _noteTitle
private val _noteContent = mutableStateOf(NoteTextFieldState(
hint = "Enter some content"
))
val noteContent:State<NoteTextFieldState> = _noteContent
private val _noteColor = mutableStateOf(Note.noteColors.random().toArgb())
val noteColor:State<Int> = _noteColor
private val _eventFlow = MutableSharedFlow<UiEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private var currentNoteId:Int ?= null
init {
savedStateHandle.get<Int>("noteId")?.let {noteId ->
if (noteId!=-1){
viewModelScope.launch {
noteUseCases.getNote(noteId)?.also {note ->
currentNoteId = note.id
_noteTitle.value = noteTitle.value.copy(
text = note.title,
isHintVisible = false
)
_noteContent.value = noteContent.value.copy(
text = note.content,
isHintVisible = false
)
_noteColor.value = note.color
}
}
}
}
}
fun onEvent(event:AddEditNoteEvent){
when(event){
is AddEditNoteEvent.EnteredTitle ->{
_noteTitle.value = noteTitle.value.copy(
text = event.value
)
}
is AddEditNoteEvent.ChangeTitleFocus ->{
_noteTitle.value = noteTitle.value.copy(
isHintVisible = !event.focusState.isFocused &&
noteTitle.value.text.isBlank()
)
}
is AddEditNoteEvent.EnteredContent ->{
_noteContent.value = noteContent.value.copy(
text = event.value
)
}
is AddEditNoteEvent.ChangeContentFocus -> {
_noteContent.value = noteContent.value.copy(
isHintVisible = !event.focusState.isFocused &&
noteContent.value.text.isBlank()
)
}
is AddEditNoteEvent.ChangeColor -> {
_noteColor.value = event.color
}
is AddEditNoteEvent.SaveNote ->{
viewModelScope.launch {
try {
noteUseCases.addNote(
Note(
title = noteTitle.value.text,
content = noteContent.value.text,
timestamp = System.currentTimeMillis(),
color = noteColor.value,
id = currentNoteId
)
)
_eventFlow.emit(UiEvent.SaveNote)
}catch (e:InvalidNoteException){
_eventFlow.emit(
UiEvent.ShowSnackBar(
message = e.message?:"Couldn't save note"
))
}
}
}
}
}
sealed class UiEvent{
data class ShowSnackBar(val message:String):UiEvent()
object SaveNote:UiEvent()
}
}
创建AddNote页面UI
image.png我们来分析一下AddNote页面的组件组成:
首先,屏幕顶部有五个圆形色盘,选中那种颜色,文本背景就会变成那种颜色,且色盘也会有黑色的边框表示选中状态。这个的实现很简单,只需要遍历Note.kt准备的颜色资源创建Box()即可,每次点击都会触发viewModel.onEvent(AddEditNoteEvent.ChangeColor(colorInt)),与五种颜色盘比较,相同的则是选中状态。
然后是标题与内容输入框,这俩几乎一样,只是所占据的空间不同而已。没有内容时会有hint提示,当点击弧或有内容时hint则会消失。这个实现也简单:BasicTextField与Text,当没有内容时显示Text,Text里面的内容是hint。
最后就是右下角的保存悬浮按钮,那我们就需要Scaffold,在点击的时候执行viewModel.onEvent(AddEditNoteEvent.SaveNote)
自定义文本样式TransparentHintTextField(封装输入框)
当添加新的Note时,hint会显示,有输入内容时hint隐藏。前面已经对文本输入内容进行封装并带有isHintVisible为参数,如果isHintVisible为true会显示Text(),否则会显示BasicTextField()。
@Composable
fun TransparentHintTextField(
text:String,
hint:String,
modifier: Modifier = Modifier,
isHintVisible:Boolean = true,
onValueChange:(String) ->Unit,
textStyle: TextStyle = TextStyle(),
singleLine:Boolean = false,
onFocusChange:(FocusState) -> Unit
) {
Box(
modifier = modifier
) {
BasicTextField(
value = text,
onValueChange = onValueChange,
singleLine = singleLine,
textStyle = textStyle,
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
onFocusChange(it)
}
)
if (isHintVisible){
Text(text = hint, style = textStyle, color = Color.DarkGray)
}
}
}
创建AddEditNoteScreen作为AddNote的compose组件集合
noteBackgroundAnimatable:文本背景颜色,切换颜色时还存在动画效果。
Row(...)内创建顶部的颜色盘并实现动画切换效果。
/**
*@Description
*@Author PC
*@QQ 1578684787
*/
@Composable
fun AddEditNoteScreen(
navController: NavController,
noteColor:Int,
viewModel: AddEditNoteViewModel = hiltViewModel()
) {
val titleState = viewModel.noteTitle.value
val contentState = viewModel.noteContent.value
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
//具有切换效果的背景动画
val noteBackgroundAnimatable = remember{
Animatable(
Color(if (noteColor != -1) noteColor else viewModel.noteColor.value)
)
}
LaunchedEffect(key1 = true){
viewModel.eventFlow.collectLatest { event ->
when(event){
is AddEditNoteViewModel.UiEvent.ShowSnackBar ->{
scaffoldState.snackbarHostState.showSnackbar(
message = event.message
)
}
is AddEditNoteViewModel.UiEvent.SaveNote ->{
navController.navigateUp()
}
}
}
}
Scaffold(
floatingActionButton ={
FloatingActionButton(
onClick = {
viewModel.onEvent(AddEditNoteEvent.SaveNote)
},
backgroundColor =MaterialTheme.colors.primary
) {
Icon(imageVector = Icons.Default.Save, contentDescription = "Save note")
}
},
scaffoldState = scaffoldState
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(noteBackgroundAnimatable.value)
.padding(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
//绘制顶部颜色选择按钮
Note.noteColors.forEach {color ->
val colorInt = color.toArgb()
Box(
modifier = Modifier
.size(50.dp)
.shadow(15.dp, CircleShape)
.clip(CircleShape)
.background(color)
.border(
width = 3.dp,
color = if (viewModel.noteColor.value == colorInt) {
Color.Black
} else {
Color.Transparent
},
shape = CircleShape
)
.clickable {
scope.launch {
noteBackgroundAnimatable.animateTo(
targetValue = Color(colorInt),
animationSpec = tween(
500
)
)
}
//改变Note的文本颜色
viewModel.onEvent(AddEditNoteEvent.ChangeColor(colorInt))
}
)
}
}
Spacer(modifier = Modifier.height(16.dp))
TransparentHintTextField(
text = titleState.text,
hint = titleState.hint,
isHintVisible = titleState.isHintVisible,
onValueChange = {
viewModel.onEvent(AddEditNoteEvent.EnteredTitle(it))
},
onFocusChange ={
viewModel.onEvent(AddEditNoteEvent.ChangeTitleFocus(it))
},
textStyle =MaterialTheme.typography.h5,
singleLine = true
)
Spacer(modifier = Modifier.height(16.dp))
TransparentHintTextField(
text = contentState.text,
hint = contentState.hint,
isHintVisible = contentState.isHintVisible,
onValueChange = {
viewModel.onEvent(AddEditNoteEvent.EnteredContent(it))
},
onFocusChange = {
viewModel.onEvent(AddEditNoteEvent.ChangeContentFocus(it))
},
textStyle = MaterialTheme.typography.body1,
modifier = Modifier.fillMaxHeight()
)
}
}
}
添加导航
封装导航路径
image.png用密封类对两个页面的路径进行封装,不仅增强代码的可读性,而且编码的重用性也增强了。
sealed class Screen(val route:String){
object NotesScreen:Screen("notes_screen")
object AddEditNoteScreen:Screen("add_edit_note_screen")
}
AddEditNoteScreen需要接收两个参数,noteId和noteColor,compose路径拼接参数的方式与网络请求的参数拼接方式相似
"?noteId={noteId}¬eColor={noteColor}"
相信大家在前面的代码中看到了不少-1,没错就是这里的defaultValue的值,当无数据时传递的值。当然defaultValue的值不是唯一的,根据自己定义的type类型,这里小编用的Int(Navtype.IntType)类型,也可以定义其他类型如String。
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NoteAppTheme {
Surface(
color = MaterialTheme.colors.background
) {
val navController = rememberNavController()
NavHost(
navController =navController,
startDestination = Screen.NotesScreen.route
){
composable(route = Screen.NotesScreen.route){
NotesScreen(navController = navController)
}
composable(route = Screen.AddEditNoteScreen.route +
"?noteId={noteId}¬eColor={noteColor}",
arguments = listOf(
navArgument(
name ="noteId"
){
type = NavType.IntType
defaultValue = -1
},
navArgument(
name = "noteColor"
){
type = NavType.IntType
defaultValue = -1
}
)
){
val color = it.arguments?.getInt("noteColor")?:-1
AddEditNoteScreen(
navController = navController,
noteColor = color
)
}
}
}
}
}
}
}
用到导航的区域
image.png用到导航的区域
image.png
网友评论