定义消息行(MessageLine)组件定义于 entry/…/src/ui/ui.cj 文件中。其工作机制与前述 Switch 自定义组件相似。

  在entry/…/src/ui.cj文件内,包ohos_app_cangjie_entry.ui里,可见自定义MessageLine组件的相关代码。在聊天机器人应用中,MessageLine用于显示一条对话消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class MessageLine {
@Prop var content: String //消息内容, @Prop语义与@State相同,但必须使用父组件提供的变量进行初始化
@Prop var reason: String //消息中的思维链内容
@Prop var self: Bool //self为true: 用户消息行,false: AI消息行
@State var showReason: Bool = true //是否显示思维链内容,点击相应的图标可以收起/展开思维链内容

func build() {
if (self) { //self为真时说明是用户消息
userMessageLine() //生成用户消息行
} else {
aiMessageLine() //生成AI消息行
}
}
//... 此处有大量节略
}

▶第1行:Component宏装饰类型MessageLine,使其成为可被复用的自定义UI组件。其他程序文件从ohos_…_entry.ui包导入MessageLine类型以后,便可使用MessageLine创建消息行组件。

▶第2行:为确保类型能在包外访问,将其声明为公有(public)。

▶第3 ~ 5行:content为消息内容,reason为消息中的思维链内容; self为真是说明消息行为用户消息行,否则为AI消息行(由AI生成的消息)。如第3行注释所述,content, reason, self均使用Prop宏修饰,语义与@State类似,但必须使用父组件提供的变量进行初始化。即,使用MessageLine组件的组件并需为MessageLine提供上述三个“构造参数”。

▶第6行:使用@State宏修饰的showReason成员表示是否显示思维链内容。当showReason发生变化时,相关UI组件会自动刷新以反应该变化。

▶第8 ~ 14行:build函数用于构建MessageLine组件的UI实体。当self为真时,执行userMessageLine函数生成用户消息行,否则生成AI消息行。

  如图1所示,用户消息行和AI消息行的显示格式(左/右, 底色 … )有所区别,分成两个函数来生成存在其合理性。

image-20250619155807335

图1 用户消息行与AI消息行

  我们先讨论用户消息行生成函数userMessageLine。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 //... 此处有节略
public class MessageLine {
//... 此处有节略
@Builder
func userMessageLine() { //生成用户消息行
Column(3) {
Row {
Row {
Text(content).fontColor(0x000000).fontSize(16).lineHeight(26) //Text显示内容
}.constraintSize(maxWidth: 90.percent)
.padding(top: 7, bottom: 7, right: 16, left: 16)
.backgroundColor(0xebf3fe).borderRadius(20)
Row {
ChatAvatar(src: @r(app.media.user), size: 36) //ChatAvatar显示发送方图标(在右)
}.margin(left: 10)
}.justifyContent(FlexAlign.End).alignItems(VerticalAlign.Top).width(100.percent)
}.width(100.percent).padding(right: 5, left: 5)
}
//... 此处有节略
}

image-20250619162322575

图2 用户消息行的UI结构

  图2展示了用户消息行的UI结构。如图所示,一个Column布局容器内包含一个Row布局容器,Row布局内又包含左右两个Row布局。左边的Row布局内有标签组件Text(content),用于显示消息内容; 右边的Row布局内有ChatAvatar(…),用于显示用户图标(@r(app.media.user))。稍后我们将看到,ChatAvatar是一个用@Builder宏修饰的可以生成UI组件的函数。

▶第4行:使用Builder宏修饰的成员函数,可以得行声明式UI描述。

▶第12行:标签组件的背景色被设置为0xebf3fe,即图2中“晚上好!”处的浅蓝色。

  接下来讨论AI消息行生成函数aiMessageLine。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
 //... 此处有节略
public class MessageLine {
//... 此处有节略
@Builder
func aiMessageLine() { //生成AI消息行
Column(3) {
Row {
Row {
ChatAvatar(src: @r(app.media.system), size: 36) //ChatAvatar显示发送方图标(在左)
}.margin(top: 5)
Row {
if (content == '' && reason == '') { //正在装载中的动图
Row { Image(@r(app.media.loading)).width(25).height(25) }
.padding(top: 7, bottom: 7, right: 16, left: 16).backgroundColor(0xffffff)
}
Column {
if (reason != '') { //如果思维链输出不空
Column {
Row {
Text('已深度思考')
.fontWeight(Bold).fontColor(0x777777).fontSize(14)
.lineHeight(20).margin(right: 10)
//显示收起/展开图像“按钮”
Image(if (showReason)
{ @r(app.media.on) }
else
{ @r(app.media.down) }).width(20).height(20)
}
.padding(top: 7, bottom: 7, right: 16, left: 16)
.width(90.percent)
.justifyContent(FlexAlign.Start)
.onClick {_ => showReason = !showReason}
if (showReason) { //显示思维链输出
Row {
Row().width(10) //思维链输出的左侧留空
Row { //0x777777决定思维链输出的文字是灰色
Text(reason).fontColor(0x777777).fontSize(14).lineHeight(20)
}.padding(top: 7, bottom: 0, right: 16, left: 10)
.borderStyle(BorderStyle.Solid)
.borderWidth(EdgeWidths(left: 2.vp)) //形成思维链输出左侧的灰色边线
.borderColor(0xe5e5e5)
}.width(90.percent)
}
}
}
if (content != '') { //内容不空,显示内容
Row { //0x000000表示0红0绿0蓝,确定内容文本字体为黑色
Text(content).fontColor(0x000000).fontSize(16).lineHeight(26)
}.width(90.percent).padding(top: 7, bottom: 7, right: 16, left: 16)
.backgroundColor(0xffffff)
}
}
}
}.justifyContent(FlexAlign.Start).alignItems(VerticalAlign.Top).width(100.percent)
}.width(100.percent).padding(right: 5, left: 5)
}
}

  AI消息行的UI结构比较复杂,包含了多重嵌套的Column和Row容器。图3中未描绘这些Column以及Row容器的详细结构。

▶第8 ~ 10行:在一个Row容器中使用ChatAvatar函数创建系统图标(见图3①处)。@r(app.media.system)即为图中的“海豚”图标。

▶第12 ~ 15行:如果content和reason都为空,说明客户端正在等待并获取AI的回答,此时,使用Image组件显示一个表示正在加载的动图。@r(app.media.loading)即为那个GIF动图。

▶第17行:如果reason不为空,说明回答包含思维链内容,产生后续UI组件以显示思维链输出。

▶第19 ~ 28行:对应图3中②处。由一个Row容器及其内包含的一个Text标签和Image图像组成。

▶第24 ~ 27行:根据showReason的不同,②处的Image显示不同的图像。

▶第32行:当②处的Row容器被点击时,showReason取非。由于showReason是用@State修饰的状态,其改变将引起UI的刷新。

▶第33 ~ 43行:当showReason为真时,生成并显示思维链内容,见图3中④处。

▶第36 ~ 38行:Row容器内包一个Text组件,以字体颜色0x777777(灰色)显示思维链文本。

▶第40 ~ 41行:在Row容器的左边界形成2.vp宽,颜色为0xe5e5e5的灰色边线,见图3中③处。相较于0x777777, 0xe5e5e5由更多的红、绿、蓝光组成,它是更浅的灰色(即更接近白)。

▶第46 ~ 51行:当content不空时,生成Row容器及Text组件以字体颜色0x000000显示消息内容(content)。0x000000表示0红0绿0蓝,它是黑色。

image-20250619194218223

图3 AI消息行的结构

  在文件entry/…/src/ui.cj文件内,我们找到了ChatAvatar函数的代码:

1
2
3
4
@Builder   //产生消息行中的信息发送方图标
public func ChatAvatar(src!: CJResource, size!: Int64 = 50) {
Image(src).width(size).height(size).borderRadius(size / 2)
}

  在经@Builder宏修饰后,ChatAvatar函数可以生成UI组件,在上述代码的第3行,它生成了一个Image(图像)组件,其内显示由参数src所指定的图像资源。