我們一起聊聊鴻蒙富文本實踐
1.鴻蒙中的文本展示-Text組件
Text 組件的普通用法和其他語言一樣,可以直接使用字符串Text('我是一段文本')
通過點語法設(shè)置文本樣式:
Text('我是超長文本,超出的部分顯示省略號。I am an extra long text, with ellipses displayed for any excess。')
.width(250)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(1)
.fontSize(12)
.border({ width: 1 })
.padding(10)
也可以將 Text 組件作為容器,添加 Span 、ImageSpan,針對每段文本設(shè)置不同的樣式并且統(tǒng)一換行,ImageSpan 可以展示本地圖片與網(wǎng)絡(luò)圖片:
Text() {
Span('我是Span1,').fontSize(16).fontColor(Color.Grey)
.decoration({ type: TextDecorationType.LineThrough, color: Color.Red })
Span('我是Span2').fontColor(Color.Blue).fontSize(16)
.fontStyle(FontStyle.Italic)
.decoration({ type: TextDecorationType.Underline, color: Color.Black })
Span(',我是Span3').fontSize(16).fontColor(Color.Grey)
.decoration({ type: TextDecorationType.Overline, color: Color.Green })
}
.borderWidth(1)
.padding(10)
圖片
// xxx.ets
@Entry
@Component
struct SpanExample {
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Text() {
Span('This is the Span and ImageSpan component').fontSize(25).textCase(TextCase.Normal)
.decoration({ type: TextDecorationType.None, color: Color.Pink })
}.width('100%').textAlign(TextAlign.Center)
Text() {
ImageSpan($r('app.media.icon'))
.width('200px')
.height('200px')
.objectFit(ImageFit.Fill)
.verticalAlign(ImageSpanAlignment.CENTER)
Span('I am LineThrough-span')
.decoration({ type: TextDecorationType.LineThrough, color: Color.Red }).fontSize(25)
ImageSpan($r('app.media.icon'))
.width('50px')
.height('50px')
.verticalAlign(ImageSpanAlignment.TOP)
Span('I am Underline-span')
.decoration({ type: TextDecorationType.Underline, color: Color.Red }).fontSize(25)
ImageSpan($r('app.media.icon'))
.size({ width: '100px', height: '100px' })
.verticalAlign(ImageSpanAlignment.BASELINE)
Span('I am Underline-span')
.decoration({ type: TextDecorationType.Underline, color: Color.Red }).fontSize(25)
ImageSpan($r('app.media.icon'))
.width('70px')
.height('70px')
.verticalAlign(ImageSpanAlignment.BOTTOM)
Span('I am Underline-span')
.decoration({ type: TextDecorationType.Underline, color: Color.Red }).fontSize(50)
}
.width('100%')
.textIndent(50)
}.width('100%').height('100%').padding({ left: 0, right: 0, top: 0 })
}
}
圖片
這樣通過 Span、ImageSpan 混排就實現(xiàn)了基礎(chǔ)的圖文混排。
2.表情圖片自動匹配
對于實際中的實操,往往不是固定寫好的混排代碼,而是需要針對后端下發(fā)的數(shù)據(jù)自動進(jìn)行轉(zhuǎn)譯。比如帶表情的文本。
項目中的自定義表情一般會以配置表的形式進(jìn)行管理:
{"imageName":"icon_emotion_1","description":"[微笑]","id":44},{"imageName":"icon_emotion_2","description":"[嘻嘻]","id":43}
用特殊格式的字符串,如:"[微笑]"、"[嘻嘻]",來分別對應(yīng)相應(yīng)的表情圖片。
而后端下發(fā)的數(shù)據(jù)則只是一段包含表情關(guān)鍵字的普通文本,并且沒有額外下發(fā)表情文本的位置信息,如:"第一次見面她幫他出頭,他被她拽拽的氣質(zhì)吸引,對她一見鐘情,多年后重逢續(xù)寫故事[色]這該死的羈絆~",轉(zhuǎn)換后為:
圖片
這樣就需要將文本切塊,把本文中的表情關(guān)鍵字提取出來。上面的文本就轉(zhuǎn)換為:"第一次見面她幫他出頭,他被她拽拽的氣質(zhì)吸引,對她一見鐘情,多年后重逢續(xù)寫故事"、"[色]"、"這該死的羈絆~"。切塊的過程采用正則匹配方式。
首先,需要將表情配置表中的所有關(guān)鍵字整合加工為正則匹配字符串:"[微笑]|[嘻嘻]|[哭笑]...",這樣只要目標(biāo)文本中包含任意一個關(guān)鍵字,都可以獲得匹配結(jié)果。在加工正則字符串的過程中,同時將表情關(guān)鍵字與圖片名組裝為鍵值對表:["[微笑]":"icon_emotion_1","[嘻嘻]":"icon_emotion_2",...],用以后續(xù)獲取轉(zhuǎn)譯圖片。
鴻蒙中的正則匹配代碼:
let reg = RegExp(EmoticonManager.getInstance().emojiRegExpStr,'g') //正則匹配串
let result: IterableIterator<RegExpMatchArray> = content.matchAll(reg)//可迭代匹配結(jié)果
let next = result.next()//第一個結(jié)果
while (next.done == false) {
let matchArr: RegExpMatchArray = next.value
//分割字符串
next = result.next() //下一個結(jié)果
}
注意項:RegExp(xxx,'g') ,'g' 代表貪婪模式,會返回所有匹配結(jié)果,不然只會獲取第一個匹配結(jié)果。
最終將文本 "第一次見面她幫他出頭,他被她拽拽的氣質(zhì)吸引,對她一見鐘情,多年后重逢續(xù)寫故事[色]這該死的羈絆~" 轉(zhuǎn)換為數(shù)據(jù)模型數(shù)組:
[
[
content: "第一次見面她幫他出頭,他被她拽拽的氣質(zhì)吸引,對她一見鐘情,多年后重逢續(xù)寫故事",
type: Text,
resource: NULL
],
[
content: "[色]",
type: Emoji,
resource: xxxx
],
[
content: "這該死的羈絆~",
type: Text,
resource: NULL
]
]
再在 Text 組件中遍歷組裝:
Text(){
ForEach(this.model.getDecodedContentArr(), (element: CommentTextModel) => {
if (element.type == CommentTextType.Text) {
Span(element.content)
.onClick(() => {
this.openCommentInput()
})
}
if (element.type == CommentTextType.Emoji && element.resource != null) {
ImageSpan(element.resource)
.width(EmoticonManager.emojiSideLengthForFontSize(this.contentFontSize))
.height(EmoticonManager.emojiSideLengthForFontSize(this.contentFontSize))
.verticalAlign(ImageSpanAlignment.CENTER)
.onClick(() => {
this.openCommentInput()
})
}
})
}
這樣就完成了字符串到帶圖富文本的自動轉(zhuǎn)換。
2.1 表情配置管理
想要實現(xiàn)表情圖片自動匹配,首先就需要先有表情圖片,要有對應(yīng)的表情配置表,項目中的表情配置分程序內(nèi)置默認(rèn)配置與線上后臺下發(fā)配置。
2.1.1 本地配置
我們采用字符串的形式,將配置表保存到管理類的靜態(tài)屬性中:
export class LocalEmoticon {
static readonly data = '{"emoticons":[{"imageName":"icon_emotion_1","description":"[微笑]","id":44},{"imageName":"icon_emotion_2","description":"[嘻嘻]","id":43},{"imageName":"icon_emotion_3","description":"[笑哭]","id":42}, ... ]}'
}
這樣讀取字符串后直接映射就可以得到序列化好的類型數(shù)據(jù):
let jsonString = LocalEmoticon.data
const model = plainToClass(EmoticonConfigModel,JSON.parse(jsonString))
表情圖片文件直接放置在 resources -> base -> media 文件夾中,獲取的時候直接通過:
let str = 'app.media.' + imageName
let resourceStr = $r(str)
獲取 ResourceStr ,這里選取 media 文件夾是因為 運(yùn)行中生成的字符串不生效,但是r 可以
圖片
2.1.2 線上配置
線上配置會下發(fā)配置表數(shù)據(jù)以及對應(yīng)的圖片壓縮包下載地址,配置表和本地配置一樣讀取并序列化就可以,但是圖片文件需要先下載到沙盒文件夾中再訪問。
鴻蒙中下載文件需要使用 request.downloadFile ,下載到指定的文件夾目錄后再使用 zlib.decompressFile 解壓縮到圖片存儲目錄。
讀取的時候拼接出文件地址,再通過 fileUri 獲取文件 uri 字符串就可以:
let path = EmoticonManager.getInstance().folderPath() + '/' + imageName
return fileUri.getUriFromPath(path)
這樣無論是本地配置還是線上配置獲取到的圖片資源都統(tǒng)一成了 ResourceStr 類型,直接丟給 ImageSpan 就可以加載出對應(yīng)圖片。
3.富文本輸入框
輸入框選用 RichEditor 組件。RichEditor 通過綁定 RichEditorController 來控制布局樣式和插入富文本內(nèi)容。
圖片
圖片
3.1 自定義表情面板
通過 customKeyboard 傳入自定義組件,并在點擊事件中通過 RichEditorController 來控制輸入框的插入和刪除:
RichEditor({controller:this.editorController})
.key(this.editorKey)
.customKeyboard(this.useCustomKeyboard ? this.emojiKeyboard():undefined)
//自定義鍵盤實體
@Builder emojiKeyboard() {
EmojiKeyboard({
currentWidth:this.currentWidth,
emojiOnClick: (model) => { this.emojiOnClick(model) },
deleteOnClick: () => { this.emojiOnDelete() }})
.backgroundColor('#F2F5F7')
.width('100%')
.height(this.emojiKeyboardHeight)
.onAreaChange((oldValue:Area,newValue:Area) => {
if (typeof newValue.width === 'number') {
this.currentWidth = newValue.width as number
}
})
}
//插入圖片
emojiOnClick(model: EmoticonModel) {
this.editorController.addImageSpan(
EmoticonManager.getInstance().getPixelMap(model.description),
{imageStyle: {
size:[EmoticonManager.emojiSideLengthForFontSize(this.fontSize),
EmoticonManager.emojiSideLengthForFontSize(this.fontSize)],
verticalAlign:ImageSpanAlignment.CENTER },
offset: this.editorController.getCaretOffset() })
}
//刪除按鈕點擊
emojiOnDelete() {
const currentIndex = this.editorController.getCaretOffset() //獲取光標(biāo)位置
if (currentIndex > 0) {
this.editorController.deleteSpans({start:currentIndex-1,end:currentIndex})//從光標(biāo)位置向前刪除
}
}
圖片
3.2 獲取已輸入內(nèi)容
想要將輸入的富文本作為評論發(fā)送出去,還需要拿到轉(zhuǎn)譯之前的原始字符串,但是鴻蒙不是像 iOS 一樣給文字掛載樣式掛載圖片。iOS 這種給圖文掛載樣式的實現(xiàn)原始文字一直都在,直接獲取就可以了。鴻蒙的輸入框中的文字是文字組件,圖片是圖片組件,而且圖片組件里面只有圖片相關(guān)的屬性,沒有關(guān)聯(lián)文字的地方。只能先獲取所有組件,再反向轉(zhuǎn)譯。獲取全部組件是通過 RichEditorController 的 getSpans API 。
圖片
但是從上面的截圖可以看到 getSpans 所獲取到的數(shù)組,內(nèi)容物是聯(lián)合類型,轉(zhuǎn)譯之前就需要先判斷。鴻蒙沒有像iOS isKindOfClass 一樣的判斷方法,由于 RichEditorImageSpanResult 比 RichEditorTextSpanResult 多了一個 imageStyle 屬性,鴻蒙官方推薦使用判斷該屬性是不是 undefined 的方式來區(qū)分類型:
if (typeof (element as RichEditorImageSpanResult)['imageStyle'] != 'undefined')
區(qū)分出類型之后,文字組件 RichEditorTextSpanResult 直接獲取 .value 就可以獲取到文字。圖片組件 RichEditorImageSpanResult 就麻煩一些,首先通過 .valueResourceStr 可以獲取到圖片的資源路徑 resource:///icon_emotion_8.png ,刨除協(xié)議頭 resource:/// 與文件后綴 .png ,就得到了圖片名 icon_emotion_8,通過配置表可以匹配到對應(yīng)的表情關(guān)鍵字"[鼓掌]"。這樣按照順序?qū)⑽淖纸M裝起來,就實現(xiàn)了富文本的反向轉(zhuǎn)譯。