HarmonyOS 里做聊天消息、文章详情、通知卡片——只要内容里既有文字又有图片就绕不开图文混排。RichText、Span、ImageSpan、ContainerSpan 这几套 API 各有分工组合起来能覆盖绝大多数场景这篇把富文本与图文混排的完整方案讲清楚。RichText 基础用法RichText 组件接收 HTML 字符串直接渲染富文本内容。适合服务端下发的 HTML 内容省去手动拆分 Span 的麻烦。EntryComponentstruct RichTextPage{StatehtmlContent:stringh2鸿蒙开发指南/h2p这是一段b加粗/b和i斜体/i混排的文字。/pp图片演示/pimg srchttps://example.com/cover.png width200/p更多内容请a hrefhttps://developer.huawei.com点击这里/a/pbuild(){Column(){// RichText 直接渲染 HTML支持常见标签RichText(this.htmlContent).width(100%).padding(16).onReady((){console.info(富文本渲染完成)})}.width(100%).height(100%)}}RichText 的优势在于拿来即用——后端返回 HTML 直接塞进去就能渲染不需要手动拆分。但它的交互能力有限点击链接需要通过onReady后注册事件来处理复杂交互场景还得靠 Span 体系。注意RichText 不支持自定义组件嵌套所有内容必须在 HTML 字符串内描述。Span 与 ImageSpan 行内混排Text 组件里用 Span 写文字、ImageSpan 插图片是图文混排最灵活的方式——每一块内容都能独立设置样式和交互。interfaceChatMessage{avatar:ResourceStr name:stringcontentParts:ArraySpan|ImageSpan}Componentstruct ChatBubble{Statemessage:ChatMessage{avatar:$r(app.media.avatar),name:小明,contentParts:[]}aboutToAppear(){// 动态构建混排内容文字 表情图 文字this.message.contentParts[newSpan(今天天气真不错),newImageSpan($r(app.media.emoji_sun)).width(20).height(20).verticalAlign(ImageSpanAlignment.CENTER),newSpan( 一起去公园吧),newImageSpan($r(app.media.emoji_park)).width(20).height(20).verticalAlign(ImageSpanAlignment.CENTER),]}build(){Column(){Text(){Span(this.message.name).fontSize(12).fontColor(#999999)Span(\n)// 混排的核心Span 和 ImageSpan 按顺序拼接ForEach(this.message.contentParts,(part:Span|ImageSpan,index:number){if(partinstanceofImageSpan){ImageSpan(part)}else{Span(part)}})}.fontSize(16).maxLines(20).textOverflow({overflow:TextOverflow.Ellipsis})}}}Span 和 ImageSpan 的拼接方式就像串珠子——一个个按顺序放进去系统自动排版。ImageSpan 的verticalAlign很关键不设置的话图片默认底部对齐文字会显得飘在上面。关键区别ImageSpan 是 Text 的子组件不是独立容器它会跟随文字的行流自动换行。ContainerSpan 分组与样式隔离当一段混排内容需要统一背景色、圆角、字号时用 ContainerSpan 把多个 Span/ImageSpan 包起来一组设置样式。Componentstruct TaggedMessage{StateshowContent:booleantrueBuildertaggedText(){Text(){// 普通文字部分Span(这是一条消息).fontSize(16).fontColor(#333333)// 标签组用 ContainerSpan 包裹统一背景色ContainerSpan(){Span(#热门).fontSize(14).fontColor(#FFFFFF)Span( )Span(#推荐).fontSize(14).fontColor(#FFFFFF)}.backgroundStyle({color:#FF6B35,radius:8}).padding({left:4,right:4,top:2,bottom:2})// 后续文字Span( 内容精彩不容错过).fontSize(16).fontColor(#333333)}}build(){Column(){this.taggedText()}.padding(16).width(100%)}}ContainerSpan 就像一个样式括号——把内部的 Span 们归到一组统一加背景、圆角、内边距。没有它的话每个 Span 只能单独设文字样式背景色这种集体属性就没办法了。注意ContainerSpan 的 backgroundStyle 目前只支持纯色渐变背景需要用其他方案。图文混排的聊天消息实战把前面的知识组合起来做一个完整的聊天消息组件——头像、名称、文字、图片、表情全部混排。interfaceMessageItem{id:stringname:stringavatar:ResourceStr textBeforeImage:stringimageContent:ResourceStr textAfterImage:stringtimestamp:string}EntryComponentstruct ChatMessagePage{Statemessages:MessageItem[][{id:1,name:阿花,avatar:$r(app.media.avatar1),textBeforeImage:看这张照片 ,imageContent:$r(app.media.photo1),textAfterImage: 是不是超美,timestamp:10:30},{id:2,name:大壮,avatar:$r(app.media.avatar2),textBeforeImage:周末去爬山吧,imageContent:$r(app.media.mountain),textAfterImage: 就是这个地方,timestamp:10:35}]build(){List({space:12}){ForEach(this.messages,(msg:MessageItem){ListItem(){Row(){Image(msg.avatar).width(40).height(40).borderRadius(20)Column(){Text(msg.name).fontSize(12).fontColor(#999999)Text(){Span(msg.textBeforeImage).fontSize(15).fontColor(#333333)ImageSpan(msg.imageContent).width(120).height(80).verticalAlign(ImageSpanAlignment.CENTER).borderRadius(6)Span(msg.textAfterImage).fontSize(15).fontColor(#333333)}.margin({top:4})}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({left:8})}.alignItems(VerticalAlign.Top).width(100%).padding(12)}})}.width(100%).height(100%).padding(16)}}聊天消息的核心就是 Row 放头像 Column 放内容Column 里的 Text 用 Span ImageSpan 混排。图片宽度控制在 120vp 左右比较适合聊天场景太大会撑破布局。注意ImageSpan 的图片如果是网络图需要先确保网络权限和 HTTPS 配置已开启。超链接与手势交互富文本里的超链接不能只靠 RichText 的 HTML a 标签——用 Span 配合 Gesture 手势可以实现更精细的跳转逻辑。import{router}fromkit.ArkUIinterfaceLinkItem{text:stringurl:string}Componentstruct HyperlinkText{Statelinks:LinkItem[][{text:鸿蒙官方文档,url:pages/DocPage},{text:API参考,url:pages/ApiRefPage},{text:示例代码,url:pages/SamplePage}]build(){Text(){Span(相关资源).fontSize(16).fontColor(#333333)ForEach(this.links,(link:LinkItem,index:number){Span(link.text).fontSize(16).fontColor(#1890FF).decoration({type:TextDecorationType.Underline,color:#1890FF}).gesture(TapGesture().onAction((){// 点击链接跳转页面router.pushUrl({url:link.url})}))if(indexthis.links.length-1){Span( | ).fontSize(16).fontColor(#CCCCCC)}})}.padding(16)}}Span 上挂 TapGesture 是实现伪超链接的标准做法——视觉上用蓝色下划线模拟链接交互上用 Gesture 捕获点击。这种方式比 RichText 的 a 标签灵活得多可以加埋点、加确认弹窗、甚至做长按复制。关键区别Span 的 gesture 是附加在文字片段上的只点击那几个字才触发不会误触整行。可复制的富文本内容聊天记录、文章详情经常需要长按复制功能——Text 组件开启 copyOption 后用户长按即可选中并复制文本。EntryComponentstruct CopyableRichText{StatearticleContent:string鸿蒙NEXTHarmonyOS NEXT是华为推出的纯血鸿蒙系统完全抛弃了安卓内核采用自研微内核架构。build(){Column(){Text(){Span(this.articleContent).fontSize(16).fontColor(#333333)ImageSpan($r(app.media.highlight)).width(16).height(16).verticalAlign(ImageSpanAlignment.CENTER)Span( 标注重点内容).fontSize(16).fontColor(#FF0000)}.copyOption(CopyOptions.LocalDevice).width(100%).padding(16).backgroundColor(#F5F5F5).borderRadius(8)Text(长按上方文字可复制).fontSize(12).fontColor(#999999).margin({top:8})}.width(100%).padding(16)}}copyOption(CopyOptions.LocalDevice)允许在本设备复制如果设成CrossDevice还能跨设备粘贴。不过要注意ImageSpan 的图片不会被复制到剪贴板只有文字部分会——这是系统的限制。注意copyOption 开启后长按手势可能与自定义 gesture 冲突需要用 priorityGesture 处理优先级。文章详情页综合实战把所有混排能力整合到一个文章详情页里——标题、正文、内嵌图片、标签、超链接、复制功能一应俱全。interfaceArticleData{title:stringauthor:stringtags:Array{text:string;color:string}paragraphs:Arraystringimages:Array{src:ResourceStr;position:number;caption:string}relatedLinks:Array{text:string;url:string}}EntryComponentstruct ArticleDetailPage{Statearticle:ArticleData{title:鸿蒙NEXT开发实战,author:老王,tags:[{text:鸿蒙,color:#FF6B35},{text:ArkUI,color:#1890FF},{text:入门,color:#52C41A}],paragraphs:[鸿蒙NEXT采用了全新的ArkUI框架,基于声明式开发范式,让UI开发效率提升了30%以上。],images:[{src:$r(app.media.banner1),position:1,caption:ArkUI架构图},{src:$r(app.media.banner2),position:3,caption:开发工具界面}],relatedLinks:[{text:官方入门教程,url:pages/TutorialPage},{text:API参考文档,url:pages/ApiRefPage}]}build(){Scroll(){Column(){// 标题Text(this.article.title).fontSize(24).fontWeight(FontWeight.Bold).width(100%)// 作者Text(作者this.article.author).fontSize(14).fontColor(#999999).margin({top:8})// 标签行Text(){ForEach(this.article.tags,(tag:{text:string;color:string}){ContainerSpan(){Span(tag.text).fontSize(12).fontColor(#FFFFFF)}.backgroundStyle({color:tag.color,radius:10}).padding({left:8,right:8,top:2,bottom:2})Span( )})}.margin({top:12})// 正文 内嵌图片Text(){ForEach(this.article.paragraphs,(para:string,idx:number){Span(para).fontSize(16).fontColor(#333333)// 在指定位置插入图片ForEach(this.article.images,(img:{src:ResourceStr;position:number;caption:string}){if(img.positionidx){ImageSpan(img.src).width(100%).height(180).verticalAlign(ImageSpanAlignment.CENTER).borderRadius(8)}})})}.copyOption(CopyOptions.LocalDevice).margin({top:16}).width(100%)// 相关链接Text(){Span(相关资源).fontSize(14).fontColor(#666666)ForEach(this.article.relatedLinks,(link:{text:string;url:string},index:number){Span(link.text).fontSize(14).fontColor(#1890FF).decoration({type:TextDecorationType.Underline,color:#1890FF}).gesture(TapGesture().onAction((){router.pushUrl({url:link.url})}))if(indexthis.article.relatedLinks.length-1){Span( · ).fontSize(14).fontColor(#CCCCCC)}})}.margin({top:20})}.padding(16)}.width(100%).height(100%)}}这个综合页面把 Span 文字、ImageSpan 图片、ContainerSpan 标签、Gesture 超链接、copyOption 复制全用上了。实际项目中文章内容一般从接口获取后动态构建 Span 数组逻辑一样只是数据源换成网络请求。注意超长文章不要把所有内容塞进一个 Text应该按段落拆分多个 Text 组件避免单次渲染压力过大。踩坑清单问题原因解决ImageSpan 图片不显示资源路径错误或网络图未配置权限检查 $r 路径和网络权限图片和文字对不齐verticalAlign 未设置设 ImageSpanAlignment.CENTERRichText 内链接无法跳转HTML a 标签点击事件需单独处理用 onReady 注册事件或改用 SpanGestureContainerSpan 背景没圆角backgroundStyle 里漏了 radius补上 radius 属性长按复制和 TapGesture 冲突两个手势同时响应用 priorityGesture 控制优先级ImageSpan 宽高设百分比无效ImageSpan 不支持百分比宽高用具体数值或 vp 单位Span 换行不生效直接写 \n 可能被忽略用单独的 Span(‘\n’) 换行富文本内容超长卡顿单个 Text 组件内容太多按段落拆分多个 TextImageSpan 图片不可复制剪贴板只支持文本截图方案或放弃图片复制ContainerSpan 内边距不生效padding 写法与普通组件不同用 .padding() 链式调用