在记事本App中实现多选模式并在弹框中勾选对应的分类的功能,是一种常见的UI设计模式。现在移动端和Web开发模式都支持多选功能的实现,可通过复选框或列表项的多选模式来实现。
用户可以通过长按面板方式点击进入多选模式,选中内容后点击“分类”按钮弹出分类选择弹框。弹框的设计可以避免页面跳转,减少用户操作路径,提升用户体验。而且用户可以通过多选模式一次性选择多条内容,然后在弹框中快速分类,避免逐条操作的繁琐。另外,多选模式和弹框是用户熟悉的交互模式,学习成本低,易于上手。
一、设计流程
以下内容部分,实现多选的示例流程,具体如下:
- 进入主界面,用户长按某条内容进入多选模式,选中条目高亮显示或显示为勾选模式。
- 点击“分类”按钮,弹出分类选择弹框,在弹框中勾选一个分类进行移动。
- 点击选择分类后,校验结果并作出相应提示,关闭弹框;如果流程正常,退出多选模式,否则继续等待。
以下为弹框的中,选择分类信息的设计流程,具体如下:
- 分类列表:弹框中显示分类列表,点击分类后选择该分类结果。
- 确认按钮:弹框底部添加“确认”按钮,用户选择分类后点击确认完成操作。
二、ArkTS组件
2.1、LongPressGesture
在HarmonyOS ArkTS中,可以通过LongPressGesture来触发长按事件。本项目中,将在onActionEnd回调事件中,记录长按状态,并显示底部操作栏界面。
LongPressGesture参数:
- fingers:触发长按的最少手指数,默认为 1。
- repeat:是否连续触发事件回调,默认为 false。
- duration:触发长按的最短时间,单位为毫秒,默认为 500ms。
事件回调:
- onAction:长按手势识别成功时触发的回调。
- onActionEnd:长按手势识别成功后,手指抬起时触发的回调。
- onActionCancel:长按手势识别成功后,接收到触摸取消事件时触发的回调。
2.2、ActionSheet
在HarmonyOS ArkTS中,ActionSheet是一种用于显示列表选择的弹框组件,支持多种配置和交互方式。
ActionSheet的基本用法:
ActionSheet 通过 ActionSheet.show() 方法弹出,支持标题、副标题、消息、选项列表等配置。以下是常用的配置参数:
参数名称 | 类型 | 必填 | 说明 |
---|---|---|---|
title | string | 是 | 弹窗标题 |
subtitle | string | 否 | 弹窗副标题 |
message | string | 是 | 弹窗消息 |
autoCancel | boolean | 否 | 点击遮罩层是否关闭弹窗,默认为 true |
confirm | { value: string, action: () => void } | 否 | 确认按钮的文本和回调 |
cancel | () => void | 否 | 点击遮罩层关闭时的回调 |
alignment | DialogAlignment | 否 | 弹窗对齐方式,默认为 DialogAlignment.Bottom |
sheets | Array<{ title: string, action: () => void }> | 是 | 选项列表 |
onWillDismiss | (dismissDialogAction: DismissDialogAction) => void | 否 | 关闭前的回调,可用于拦截关闭 |
三、界面开发
3.1、显示操作面板
打开src/main/ets/pages/Index.ets主界面文件,定义@State状态变量isLongTouch,记录内容面板是长按事件状态,为true时,显示底部“移动”按钮的操作面板;为flase时,隐藏其操作面板。
此篇是接长两篇内容的补充部分,前面讲述过部分就不再阐述了。
地址一:HarmonyOS开发 - 记事本实例一(界面搭建)-CSDN博客
地址二:HarmonyOS开发 - 记事本实例二(关系型数据库数据存储)-CSDN博客
界面效果如下图:
主界面在之前基础上,内容面板作了一些调整,自定义构建函数listItem()中,修改为了左右两列,当长按状态开启时,显示右侧复选框部分。并且增加checkboxList和isLongTouch状态变量,复选框为true时,将当前行内容数据记录在checkboxList容器中。底部操作面板定义在构建函数BottomCard()中,代码如下:
import Header from '../components/Header'
import { ClassifyIndexInfo, NotesInfo } from '../types/types'
import { formatDate } from '../utils/utils'
import router from '@ohos.router'
import { NotesModal } from '../model/Notes'
@Entry
@Component
struct Index {
@State notes: Array<NotesInfo> = []
private classifyActiveId: number = -1 //分类ID
private keyword: string = '' // 搜索关键词
@State checkboxList: Map<number, NotesInfo> = new Map() // 选中项
@State isLongTouch: boolean = false // 是否长按面板
/**
* 获取记事本内容
*/
async updateNotes(){
const result = await NotesModal.getAllRows(this.classifyActiveId, this.keyword)
if (Array.isArray(result) && result.length > 0) {
this.notes = result
} else {
this.notes = []
}
console.log('update notes list:', JSON.stringify(result))
}
aboutToAppear(): void {
this.updateNotes()
const context = getContext(this)
// 监听分类索引变化
context.eventHub.on('classifyIndexChange', (data: ClassifyIndexInfo) => {
this.keyword = ''
this.classifyActiveId = data.currentId // 记录选中的分类ID
this.isLongTouch = false // 取消长按事件,还原列表状态
this.updateNotes() // 重新更新记事本内容
console.log('classifyIndexChange', JSON.stringify(data))
})
// 监听内容变化
context.eventHub.on('contentChange', () => {
this.keyword = ''
this.updateNotes() // 重新更新记事本内容
console.log('update content', JSON.stringify(this.notes))
})
// 监听搜索关键词
context.eventHub.on('searchChange', (e: string) => {
this.keyword = e
this.updateNotes()
})
}
// 自定义面板panel item
@Builder listItem(item: NotesInfo){
Column(){
Row(){
Column(){
Row(){
Text(item.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.padding({ right: 10 }) // 右侧内填充10,与日期相隔10个间距
Text(formatDate(new Date(item.create_time as number), 'YYYY/MM/DD'))
.fontSize(12)
.fontColor(Color.Gray)
}.width('100%')
.justifyContent(FlexAlign.SpaceBetween) // 两边对齐
.alignItems(VerticalAlign.Top) // 内容顶部对齐
// 判断描述内容是否存在
if (item.content) {
Text(item.content.substring(0, 50) + (item.content.length>50?'...':''))
.fontSize(12)
.fontColor(Color.Gray)
.width('100%')
.align(Alignment.Start)
.margin({top: 10})
}
}.layoutWeight(1)
// 长按事件被触发后,显示复选框
if (this.isLongTouch) {
Column(){
Checkbox().onChange(e => {
this.checkboxList.set(item.id as number, item)
})
}.padding({left: 15})
}
}.width('100%').justifyContent(FlexAlign.SpaceBetween)
}.padding(15)
.backgroundColor(Color.White)
.borderRadius(10)
.shadow({color: '#00000050', radius: 10})
// 列表信息 点击跳转到编辑界面
.onClick(() => {
// 如果为长按钮模式,禁止跳转
if (this.isLongTouch) return
router.pushUrl({
url: 'pages/CreateNote',
params: { item, cid: this.classifyActiveId }
})
})
// 长按事件
.gesture(
LongPressGesture({repeat: true}).onAction(() => {
console.log('long event')
}).onActionEnd(() => {
this.isLongTouch = true
console.log('long event end')
})
)
}
/**
* 底部卡片
*/
@Builder BottomCard(){
Row({space: 20}){
Button(){
Column(){
Image($rawfile('move_in.png')).width(20).margin({bottom: 5})
Text('移动').fontSize(14)
}
}.backgroundColor(Color.Transparent)
Button(){
Column(){
Image($rawfile('delete.png')).width(20).margin({bottom: 5})
Text('删除').fontSize(14)
}
}.backgroundColor(Color.Transparent)
}.width('100%')
.justifyContent(FlexAlign.SpaceAround)
.padding({top: 10})
}
build() {
Row() {
Column() {
Header() // 顶部组件
Divider() //分割线
// List容器
List(){
// 循环输出笔记列表内容
ForEach(this.notes, (item: NotesInfo) => {
ListItem(){
this.listItem(item) // 渲染面板内容
}.border({color: Color.Gray, style: BorderStyle.Dashed})
.padding({ top: 10, bottom: 10 })
})
}.width('100%')
.layoutWeight(6)
.padding({ left: 10, right: 10, top: 10, bottom: 10 })
.backgroundColor('#f1f1f1')
// 添加按钮
Button(){
Image($rawfile('add.png'))
.width(40)
}.position({ right: 10, bottom: 60 })
.backgroundColor(Color.Transparent)
// 新增按钮 点击跳转到编辑界面
.onClick(() => {
// 如果为长按钮模式,禁止跳转
if (this.isLongTouch) return
router.pushUrl({
url: 'pages/CreateNote',
params: { cid: this.classifyActiveId }
})
})
// 当长按事件被触发,显示底部操作按钮面板
if (this.isLongTouch) {
this.BottomCard()
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.height('100%').alignItems(VerticalAlign.Top)
}
}
3.2、还原复选状态
在主界面的struct中定义restoreCheckStatus()函数,在分类选项重选时,或在其他操作中需要返取消复选框编辑状态时,调用执行restoreCheckStatus()函数。代码如下:
// 还原选择状态
restoreCheckStatus(){
if (this.isLongTouch) this.isLongTouch = false
this.checkboxList.clear()
}
如在分类选项重选时,取消编辑状态。如下图:
3.3、缓存勾选行数据
在主界面的struct中,找到listItem()构建函数,在复选框数据发生变更时,记录或移除当前行数据;当回调函数返回为true时,将当前行数据追加到状态变量checkboxList中;当回调函数返回false时,将状态变量checkboxList中存在的当前行数据移除掉。代码如下图:
3.4、弹框界面
ArkTS组件中介绍了ActionSheet,这里就用它完成弹框分类列表的展示。在主界面的struct中定义记事本分类修改函数,点击“移动”按钮调用noteChangeClassify()函数,显示弹框分类列表。
// 记事本修改分类项
async noteChangeClassify(){
// 获取分类信息
const result = await ClassifyModal.getAllRows()
// 判断是否有选择的移动项
if(this.checkboxList.size == 0) {
promptAction.showToast({message: '请选择要移动的内容后,再操作~'})
return;
}
// 获取分类信息
ActionSheet.show({
title: '更改分类',
message: '选择需要更改的分类',
autoCancel: true,
sheets: result.map(item => {
const sheetItem: SheetInfo = {
title: item.name,
icon: $rawfile('dot.png'),
action: async () => {
console.log('change', item.id)
}
}
return sheetItem
}),
confirm: {
value: '知道了',
action: () => {}
}
})
}
页面效果如下:
3.5、model增加变更分类函数
打开src/main/ets/model/Notes.ets文件,在Notes类中新增changeClassify()函数,用于更新记事本信息的行数据。注意的是,这里是批量处理,所以增加了事务处理机制,当循环提交中某环节出现错误时,回滚结果。代码如下:
/**
* 修改分类ID
* @param ids 修改的记事行数据ID
* @param classifyId 更改的分类ID
*/
async changeClassify(ids: Array<number>, classifyId: number) {
store.beginTransaction() // 开始事务
try {
// 循环提交数据
ids.forEach(async (_id) => {
// 组合更新数据
const dynamicObj: Record<string, number> = {}
dynamicObj[this._CLASSIFY_ID] = classifyId
// 构造查询条件
const predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo(this._ID, _id)
// 执行数据更新
await store.update(dynamicObj, predicates)
})
store.commit() // 提交
console.log('update success...')
} catch (e) {
store.rollBack() // 回滚
console.log('update error, roll back...')
}
//
}
3.6、执行分类更改
当用户选择某个分类时,将记事本信息列表中勾选项的ID提取出来,以及获取当前选择的分类的ID,执行NotesModal.changeClassify()函数更新分类ID。更新后,重新获取记事本信息的行数据,并取消编辑状态。代码如下:
代码如下:
// 记事本修改分类项
async noteChangeClassify(){
// 获取分类信息
const result = await ClassifyModal.getAllRows()
// 判断是否有选择的移动项
if(this.checkboxList.size == 0) {
promptAction.showToast({message: '请选择要移动的内容后,再操作~'})
return;
}
// 获取分类信息
ActionSheet.show({
title: '更改分类',
message: '选择需要更改的分类',
autoCancel: true,
sheets: result.map(item => {
const sheetItem: SheetInfo = {
title: item.name,
icon: $rawfile('dot.png'),
action: async () => {
// 获取选中的ID值
const checkedIds = Array.from(this.checkboxList.keys())
// 执行分类更改
await NotesModal.changeClassify(checkedIds, item.id)
// 更新记事本信息行数据
this.updateNotes()
// 还原状态
this.restoreCheckStatus()
}
}
return sheetItem
}),
confirm: {
value: '知道了',
action: () => {}
}
})
}
在“移动”按钮位置,绑定变更分类ID的事件函数。如下图:
上述操作完成后,就可以修改记事本信息的分类ID了。现在将“全部”状态的“日记五”,更新到“默认”分类中。如下图:
当选择“默认”分类后,切换到“默认”分类项中后,“日记五”这篇内容也在该分类下展示出来了。
四、批量删除操作
4.1、model增加批量删除函数
打开src/main/ets/model/Notes.ets文件,继续增加批量删除的函数,将形参中传入的ids循环处理并执行删除操作。代码如下:
/**
* 多行数据删除
* @param ids
*/
async deleteMultiRowData(ids: Array<number>) {
store.beginTransaction() // 开始事务
try {
ids.forEach(async (_id) => {
const predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo(this._ID, _id)
await store.delete(predicates)
})
store.commit() // 提交
}catch (e) {
store.rollBack() // 回滚
}
}
4.2、执行删除操作
既然勾选多条数据可以修改分类,也可以勾选多条数据进行删除操作。同样,在主界面的struct中定义多行删除函数deleteMultiNotesInfo(),删除操作完成后,更新记事本内容数据,并且退出编辑状态。代码如下:
/**
* 删除多条记事本内容
*/
deleteMultiNotesInfo() {
AlertDialog.show({
title: '提示',
message: `确认要删除选中的信息吗?`,
alignment: DialogAlignment.Center,
buttons: [
{
value: '删除',
action: async () => {
// 获取选中的ID值
const checkedIds = Array.from(this.checkboxList.keys())
// 批量删除行数据
await NotesModal.deleteMultiRowData(checkedIds)
// 更新记事本信息行数据
this.updateNotes()
// 还原状态
this.restoreCheckStatus()
}
},
{
value: '取消',
action: () => {}
}
]
})
}
同样,在“删除”按钮的点击事件处绑定删除事件。如下图:
此时,我们在记事本中添加一条“test”测试数据,使用批量删除功能将它移除掉。
长按几秒"test"内容面板松开,待为编辑状态时,勾选“test”项,点击“删除”按钮将它删除掉。如下图:
此篇就讲到这了,希望对大家会有所帮助。感谢阅读,期待能有更多关于HarmonyOS的探索和新的总结经验。