HarmonyOS 评论回复弹窗最佳实践
前言
在移动应用开发中,评论回复功能是一个常见且重要的交互场景。本文将详细介绍如何在 HarmonyOS 中实现一个功能完善的评论回复弹窗,包括弹窗选型、富文本编辑、软键盘适配等关键技术点。
功能概述
我们要实现的评论回复弹窗具备以下功能:
- 支持文字输入
- 支持表情选择
- 支持@好友功能
- 软键盘与表情面板无缝切换
- 良好的用户体验
技术选型分析
弹窗组件选型
在开始开发之前,我们需要选择合适的弹窗实现方案。HarmonyOS 提供了多种弹窗实现方式,我们对比了三种主要方案:
通过对CustomDialog自定义弹窗、bindSheet半模态弹窗、Navigation Dialog三种弹窗方案进行尝试,发现自定义弹窗和半模态弹窗有一定规格限制,会产生一些无法避免的问题,最终选用Navigation Dialog方案实现评论模块弹窗。以下对三种方案优劣势进行一个详细的说明。
方案一:CustomDialog 自定义弹窗
CustomDialog 是 HarmonyOS 提供的标准弹窗组件。
优势:
- ✅ 开箱即用,无需实现弹窗交互逻辑
- ✅ 自动避让软键盘,使用简单
- ✅ 系统级组件,稳定性好
劣势:
- ❌ 软键盘避让行为无法自定义配置
- ❌ 表情面板切换时会出现短暂的布局跳动
- ❌ 无法获取软键盘动画信息,难以实现平滑过渡
问题演示: 当用户点击表情按钮时,软键盘收起过程中表情面板会短暂显示在错误位置,影响用户体验。
注意: PromptAction.openCustomDialog 与 CustomDialog 效果相同,存在同样的问题。
方案二:bindSheet 半模态弹窗
bindSheet 是 HarmonyOS 提供的半模态弹窗组件,常用于底部弹出的交互场景。
优势:
- ✅ 开箱即用,无需实现弹窗交互逻辑
- ✅ 可以解决 CustomDialog 中的软键盘顶起问题
- ✅ 支持手势拖拽,交互体验较好
劣势:
- ❌ 高度自适应时内部滚动行为难以控制
- ❌ 即使禁用拖拽条,仍可拖动弹窗
- ❌ 拖动过程中可能暴露表情面板,影响视觉效果
问题演示: 当禁用拖拽条后,用户仍可以拖动弹窗,这会在拖动过程中暴露底层的表情面板区域。
方案三:Navigation Dialog(推荐方案)
Navigation Dialog 基于 Navigation 路由系统实现的弹窗方案。
优势:
- ✅ 完美解决前两种方案的所有问题
- ✅ 基于路由栈管理,弹窗与 UI 完全解耦
- ✅ 可精确控制软键盘避让行为
- ✅ 支持复杂的弹窗层级管理
劣势:
- ❌ 需要手动实现遮罩层和点击关闭逻辑
- ❌ 开发复杂度相对较高
重要提醒: Navigation Dialog 的 z 轴层级较低,如果项目中同时使用多种弹窗方案,建议统一使用 Navigation Dialog 以避免层级冲突。
最终选择
经过综合对比,我们选择 Navigation Dialog 作为最终方案,主要原因:
- 完美的软键盘控制:可以精确控制软键盘避让行为,实现平滑的切换动画
- 良好的架构设计:基于路由的设计更符合现代应用架构理念
- 可扩展性强:便于后续功能扩展和维护
虽然开发复杂度稍高,但带来的用户体验提升是值得的。
编辑区域组件选型
评论输入框需要支持多种内容类型:
- 📝 文字输入:普通文本内容
- 😊 表情符号:图片形式的表情
- 👥 @好友功能:特殊样式的用户标签
RichEditor 组件介绍
对于这种图文混排的需求,HarmonyOS 提供的 RichEditor 组件是最佳选择。它支持:
- 富文本编辑:文字、图片、自定义组件混合编辑
- 灵活的内容管理:通过不同的 Span 类型管理内容
- 丰富的交互事件:输入、删除、选择等事件监听
内容类型与实现方法
RichEditor 提供了三种主要的内容添加方法:
内容类型 | 实现方法 | 用途 |
---|---|---|
文字 | addTextSpan | 普通文本内容 |
图片 | addImageSpan | 表情图片 |
自定义组件 | addBuilderSpan | @好友标签 |
术语说明: 为方便理解,我们将通过这三种方法添加的内容分别称为
textSpan
、imageSpan
、builderSpan
。
@好友功能实现方案对比
对于 @好友功能,我们有两种实现方案可选:
方案一:使用 addTextSpan 实现
将 @好友 作为普通文本处理。
问题分析:
- ❌ 文本合并问题:前后输入的文字会自动与 @好友 文本合并,破坏标签的独立性
- ❌ 交互复杂:需要手动处理光标定位和整体删除逻辑
- ❌ 数据关联困难:只能获取昵称文本,无法关联用户的完整信息(如 ID、头像等)
方案二:使用 addBuilderSpan 实现(推荐)
将 @好友 作为自定义组件处理。
优势分析:
- ✅ 独立性好:不会与前后文字合并,保持标签完整性
- ✅ 交互简单:系统自动处理光标和删除逻辑
- ✅ 数据丰富:可以维护完整的用户信息,便于后续处理
注意事项:
- 需要手动维护 builderSpan 的信息,但这也带来了更大的灵活性
最终选择
我们选择 addBuilderSpan 方案,主要考虑:
- 更好的用户体验:@好友 标签作为整体,交互更自然
- 更强的扩展性:可以轻松添加头像、样式等丰富元素
- 更可靠的数据管理:完整的用户信息便于业务处理
核心功能实现
1. 弹窗显示实现
功能流程
评论弹窗的显示流程如下:
- 用户在视频页面点击消息按钮
- 弹出评论列表页面
- 用户点击写评论按钮
- 弹出评论输入弹窗
技术实现要点
1. Navigation 配置
// 主页面 Navigation 配置
Navigation() {// 页面内容
}
.mode(NavigationMode.Stack) // 设置为栈模式
.hideTitleBar(true) // 隐藏标题栏
2. 弹窗组件结构
// 弹窗页面组件
@Component
struct CommentDialog {build() {NavDestination() {Stack() {// 遮罩层Column().width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)').onClick(() => {// 点击遮罩关闭弹窗router.back()})// 弹窗内容Column() {// 评论输入组件}.backgroundColor(Color.White).borderRadius(12)}}.mode(NavDestinationMode.DIALOG) // 设置为弹窗模式.expandSafeArea([SafeAreaType.KEYBOARD]) // 不避让软键盘}
}
3. 关键配置说明
配置项 | 作用 | 重要性 |
---|---|---|
NavigationMode.Stack | 启用路由栈管理 | ⭐⭐⭐ |
NavDestinationMode.DIALOG | 设置为弹窗类型 | ⭐⭐⭐ |
expandSafeArea([SafeAreaType.KEYBOARD]) | 不避让软键盘 | ⭐⭐⭐ |
遮罩层点击事件 | 提供关闭交互 | ⭐⭐ |
弹窗管理策略
- 弹出:通过
router.pushUrl()
进入路由栈 - 关闭:通过
router.back()
退出路由栈 - 层级:路由栈的顺序决定弹窗层级关系
2. 软键盘和表情面板切换适配
功能需求
在评论弹窗中,用户需要能够在软键盘和表情面板之间无缝切换,提供良好的输入体验。
技术实现方案
1. 自定义键盘控制
本文选择自定义键盘来控制软键盘和表情面板的切换:
- 显示表情面板:设置 RichEditor.customKeyboard 为表情面板组件的构建函数
EmojiKeyboard
- 显示软键盘:设置
customKeyboard
属性为undefined
- 焦点管理:通过这种方式切换时无需手动处理 RichEditor 焦点
2. 高度适配策略
为保证切换过程中评论模块整体高度不变,需要实现以下逻辑:
软键盘高度监听:
// 监听软键盘高度变化
window.on('keyboardHeightChange', (height: number) => {if (height > 0) {this.keyboardHeight = height;}
});
高度计算规则:
- 表情面板高度 = 常用表情列表高度 + 软键盘高度
- 占位元素高度 = 当前显示组件的高度(软键盘或表情面板)
3. 布局适配实现
由于弹窗设置了不避让软键盘,需要通过占位元素来控制布局:
// 占位元素高度控制
@State placeholderHeight: number = 0;// 切换到软键盘时
this.placeholderHeight = this.keyboardHeight;// 切换到表情面板时
this.placeholderHeight = this.emojiPanelHeight + this.keyboardHeight;
注意事项
- ⚠️ 内存管理:组件销毁前必须取消键盘高度监听事件
- ⚠️ 高度变化:软键盘高度可能被用户手动调整,需要实时监听
- ⚠️ 时序控制:切换过程中要确保高度设置的时序正确
3. @好友功能实现
功能概述
@好友功能允许用户在评论中提及其他用户,被@的用户会收到通知,这是社交应用中的重要功能。
触发方式
用户可以通过两种方式触发@好友功能:
- 点击@按钮:直接点击编辑区域的@按钮
- 键盘输入:在软键盘上输入@符号
实现流程
1. 触发@功能
点击@按钮时:
// 添加@符号并显示好友列表
this.richEditorController.addTextSpan('@', {style: {fontColor: Color.Blue}
});
this.showFriendList = true;
监听键盘输入:
// 监听输入事件,统一处理@符号
.aboutToIMEInput((value: RichEditorInsertValue) => {if (value.insertValue === '@') {// 触发@好友逻辑this.showFriendList = true;return true; // 阻止默认输入}return false;
})
通过 RichEditorController.addTextSpan 添加@符号,并显示好友列表。同时监听 RichEditor.aboutToIMEInput 事件,统一处理点击@按钮和键盘输入@的逻辑。
在好友列表中点击好友头像时,通过RichEditorController.getSpans可以获取光标前一个span的内容,若光标前一个span是内容为@的textSpan,则先删除,然后通过RichEditorController.addBuilderSpan将“@[好友昵称]”以指定的样式作为一个整体添加到编辑区域中。
4. 内容删除功能
功能需求
在删除@好友内容时,需要实现智能删除:第一次点击删除键时选中整个@好友组件,第二次点击时整体删除,而不是逐字符删除。
实现方案
// 监听删除事件
.aboutToDelete((value: RichEditorDeleteValue) => {// 获取要删除的span信息const spans = this.richEditorController.getSpans(value.offset, value.offset + value.length);if (spans.length > 0) {const span = spans[0];// 如果是builderSpan(@好友)且未被选中if (span.spanType === 'builderSpan' && !this.isSpanSelected(span)) {// 第一次删除:选中整个@好友组件this.richEditorController.setSelection(span.start, span.end);return false; // 阻止默认删除行为}}return true; // 允许默认删除行为
})
技术要点
功能 | API | 说明 |
---|---|---|
删除监听 | aboutToDelete() | 监听删除操作,可阻止默认行为 |
内容选中 | setSelection() | 选中指定范围的内容 |
获取内容 | getSpans() | 获取指定位置的span信息 |
通过 RichEditor.aboutToDelete 事件监听删除操作,使用 RichEditorController.setSelection 实现@好友组件的整体选中和删除。
5. 内容获取与展示
功能概述
当用户完成评论编辑后,需要获取编辑区域的所有内容(文字、表情、@好友),并进行统一的数据处理和展示。
数据类型映射
通过 RichEditorController.getSpans 获取编辑区域内容,返回值包含 RichEditorTextSpanResult 和 RichEditorImageSpanResult 两种类型。
不同内容类型与数据类型的对应关系:
textSpan可通过RichEditorTextSpanResult.value获取文字内容。imageSpan可通过RichEditorImageSpanResult.valueResourceStr获取图片资源。但是builderSpan在RichEditorImageSpanResult中获取不到任何相关的内容信息,所以在点击好友头像添加@好友内容时需要手动将这些builderSpan进行维护。
实际开发中编辑区域不同类型的内容往往需要一种统一的数据结构来表达,方便传输和存储。该数据结构需要不仅能对编辑区域内容进行记录,也需要有携带一些额外信息的能力,比如携带@好友相关的用户信息。本文定义为RichEditorSpan。(实际开发中需要的属性字段根据需求灵活调整)。
使用RichEditorSpan[]类型的数组builderSpans来维护@好友时的builderSpan,需要注意的是要保证每个builderSpan在数组中的顺序要与实际内容中出现的顺序一致。在添加builderSpan时,通过计算当前光标位置前面builderSpan的个数,来确定添加到builderSpans数组中的位置,并把需要携带的好友信息放入data属性中。
发送评论时,将获取到的内容用RichEditorSpan[]类型的数组richEditorSpans进行统一地表达。通过getSpans获取所有内容,如果是textSpan,通过value属性取出文字内容,设置RichEditorSpan.type为text,如果是imageSpan,通过valueResourceStr属性获取图片资源,设置RichEditorSpan.type为image。如果是builderSpan,按顺序从数组builderSpans中获取,并将他们按顺序添加到richEditorSpans中。
最终生成的richEditorSpans数据格式如下:
当需要展示评论内容时,只需要对richEditorSpans进行遍历,根据type属性,分别对文字、表情、@好友进行展示逻辑的处理。具体展示形式开发者根据实际需求确定。
-
选择图片
点击图片按钮拉起系统相册,选择本地图片进行上传。该功能使用场景相对独立,本文不详细介绍。开发者需要进一步了解详情,可参考以下sample。
- 选择并查看文档和媒体文件
- 文件管理
- 发布图片评论