无名之辈的Android之路收藏

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

2022-02-15  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读