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}&noteColor=${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))
                }
            }
        }
    }

}

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容