美文网首页无名之辈的Android之路收藏
Android实战:手机笔记App(三)

Android实战:手机笔记App(三)

作者: 搬码人 | 来源:发表于2022-02-15 18:32 被阅读0次

    Android实战:手机笔记App(一)

    Android实战:手机笔记App(二)

    接着前面的内容


    创建AddEditNote模块

    image.png

    AddNote界面触发事件的封装

    将对输入标题、标题栏是否有输入聚焦、输入内容、内容栏是否有输入聚焦、改变颜色、保存这些触发事件封装成一类触发事件。

    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参数
    UiEvent:反馈给UI界面的事件,如下图所示,当保存失败时会弹出提示,当保存成功时会导航回上一页。
    剩下的不用介绍了,和之前的NoteViewModel差不多。
    为什么val _eventFlow = MutableSharedFlow<UiEvent>()此处使用SharedFlow:我们的需求是只希望SnackBar弹出一次,如果使用State那么当我们旋转屏幕的时候,SnackBar将会再次弹出。
    提示:注意NoteTextFieldState中的hint与text这两个属性,小编不小心将其中一个hint敲成了text导致后面出现了bug——每次点击+时Title中的hint内容都需要手动删除不会点击后自动消失,弄的我头痛啊!!!找了半天最后无意间才发现的。

    image.png
    @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

    我们来分析一下AddNote页面的组件组成:
    首先,屏幕顶部有五个圆形色盘,选中那种颜色,文本背景就会变成那种颜色,且色盘也会有黑色的边框表示选中状态。这个的实现很简单,只需要遍历Note.kt准备的颜色资源创建Box()即可,每次点击都会触发viewModel.onEvent(AddEditNoteEvent.ChangeColor(colorInt)),与五种颜色盘比较,相同的则是选中状态。
    然后是标题与内容输入框,这俩几乎一样,只是所占据的空间不同而已。没有内容时会有hint提示,当点击弧或有内容时hint则会消失。这个实现也简单:BasicTextField与Text,当没有内容时显示Text,Text里面的内容是hint。
    最后就是右下角的保存悬浮按钮,那我们就需要Scaffold,在点击的时候执行viewModel.onEvent(AddEditNoteEvent.SaveNote)

    image.png

    自定义文本样式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}&noteColor={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}&noteColor={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

    全部源码地址:https://github.com/gun-ctrl/NoteApp

    相关文章

      网友评论

        本文标题:Android实战:手机笔记App(三)

        本文链接:https://www.haomeiwen.com/subject/zdcllrtx.html