本文原创发布在华为开发者社区。
介绍
本示例为文本场景的整合demo,主要包括六个场景示例:多行文本只展示一行、跑马灯案例、展示全文、Text实现部分文本高亮和超链接样式、文本高亮标记功能、文字截断。
效果预览

使用说明
点击对应模式按钮即可展示对应功能。
实现思路
- 多行文本展示一行: 使用measureText(options: MeasureOptions): number接口计算指定文本单行布局下的宽度; 使用getDefaultDisplaySync(): Display获取屏幕宽度; 使用屏幕宽度减去padding等需要减去的宽度获取屏幕可用宽度。
typescript
@State isShowSheet: boolean = false;
@State message: string = 'Hello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello WorldHello World'
@State textWidth : number = measure.measureText({
textContent: this.message,
fontSize: 12
})
@State getScreenAble: number = 0
aboutToAppear(){
let windowWidth = px2vp(display.getDefaultDisplaySync().width);
this.getScreenAble = windowWidth - 70;
}
typescript
ext(this.message)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(1)
.constraintSize({ maxWidth: this.getScreenAble })
if (this.textWidth >= this.getScreenAble) {
Image($r('app.media.right'))
.fillColor(Color.Black)
.size({width: ('12vp')})
.bindSheet(this.isShowSheet,this.myDialog(),{
height: 400,
dragBar: false,
onDisappear: () => {
this.isShowSheet = !this.isShowSheet;
}
})
.onClick(() => {
this.isShowSheet = !this.isShowSheet;
})
}
- 跑马灯案例:Text组件外层包裹一层Scroll组件,Scroll组件设置一定的百分比宽度值,并获取当前文本内容宽度和Scroll组件宽度。 文本宽度大于 Scroll组件宽度时,通过添加判断显示同样的文本,在偏移过程中可实现文本接替并显示在同一显示区的效果。 页面进来执行文本滚动函数scrollAnimation(),在指定的时间内完成文本的偏移,当循环一次之后,通过定时器setTimeout来实现停滞操作。
typescript
Scroll() {
Row() {
Text(this.tripDataItem.ticketEntrance)
.onAreaChange((oldValue, newValue) => {
logger.info(`TextArea oldValue:${JSON.stringify(oldValue)},newValue:${JSON.stringify(newValue)}`);
// 获取当前文本内容宽度
this.ticketCheckTextWidth = Number(newValue.width);
})
if (this.ticketCheckTextWidth >= this.ticketCheckScrollWidth) {
Blank()
.width(Constants.BLANK_SPACE)
Text(this.tripDataItem.ticketEntrance)
}
}.offset({ x: this.ticketCheckTextOffset })
}
.width(display.isFoldable() ? $r('app.string.marquee_scroll_phone_width') : $r('app.string.marquee_scroll_tablet_width'))
.id('marquee')
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: 'ticketEntrance', align: HorizontalAlign.End }
typescript
scrollAnimation() {
// 文本宽度小于Scroll组件宽度,不执行滚动操作
if (this.ticketCheckTextWidth < this.ticketCheckScrollWidth) {
return;
}
animateTo({
duration: Constants.ANIMATION_DURATION,
curve: Curve.Linear,
delay: this.delay,
onFinish: () => {
setTimeout(() => {
// 初始化文本偏移量
this.ticketCheckTextOffset = 0;
// 执行动画函数
this.scrollAnimation();
}, Constants.DELAY_TIME)
}
}, () => {
// 文本偏离量
this.ticketCheckTextOffset = -(this.ticketCheckTextWidth + Constants.BLANK_SPACE)
})
}
- 展示全文:用"屏幕宽度 * 最大行数 * 组件宽度比例"的结果和"实际文本宽度"进行比较大小,判断是否需要"...展开全文"; 当需要"...展开全文"时,只展示maxLines属性设置的固定高度的文本内容,当点击"...展开全文"时将该文本改成"...收起"并添加curves.springMotion曲线动画,同时在animateTo的显示动效的闭包函数中将maxLines属性值修改为-1使得该属性无效;当需要"...收起"时,将collapseText的值改为"...展开全文",并设置收起动画。
typescript
// 长文本状态(展开 or 收起)
@State collapseText: string = '...展开全文'
// 屏幕宽度(单位px)
screenWidth: number = 0;
// 是否需要显示"展开"字样(注:当文本长度较短时就不需要"展开")
@State isExpanded: boolean = false
// 测量文本宽度(单位px)
@State textWidth: number = measure.measureText({
textContent: this.longMessage,
fontSize: 15
})
// 获取当前所有的display对象
promise: Promise<Array<display.Display>> = display.getAllDisplays()
aboutToAppear() {
this.promise.then((data: Array<display.Display>) => {
hilog.info(0x0000,'所有的屏幕信息:%{public}s',JSON.stringify(data))
//单位为像素
this.screenWidth = data[0].width
// 屏幕宽度 * 最大行数 * 组件宽度比例 和 文字测量宽度
this.isExpanded = this.screenWidth * this.lines * 0.8 <= this.textWidth
}).catch((err: BusinessError) => {
hilog.error(0x0000,'Failed to obtain all the display objects. Code:%{public}s',JSON.stringify(err))
})
}
typescript
.onClick(() => {
if (this.collapseText === '...展开全文') {
this.collapseText = '...收起';
// 展开动画
animateTo({
duration: 150,
curve: curves.springMotion(0.5, 0.8),
}, () => {
this.lines = -1; // 使得设置的最大行属性无效
})
} else {
this.collapseText = '...展开全文';
// 收起动画
animateTo({
duration: 100,
curve: Curve.Friction,
}, () => {
this.lines = 3; // 只显示3行
})
}
})
- Text实现部分文本高亮和超链接样式:使用Text组件结合ForEach方法遍历spans中的CustomSpan对象,根据不同的Span类型生成不同样式和功能的Span组件;对于Hashtag、Mention和DetailLink类型的Span,在TextLinkSpan组件中添加带有超链接功能的Span组件,根据CustomSpan的类型和内容,实现对应的样式和交互功能,例如显示提示信息或执行其他操作;对于VideoLink类型的Span,使用VideoLinkSpan组件添加图标和超链接功能,在点击事件中显示提示信息或执行跳转视频页操作。
typescript
Text() {
ForEach(this.spans, (item: CustomSpan) => {
if (item.type === CustomSpanType.Normal) {
Span(item.content)
.fontSize($r('app.string.ohos_id_text_size_body1'))
} else if (item.type === CustomSpanType.Hashtag ||
item.type === CustomSpanType.Mention ||
item.type === CustomSpanType.DetailLink) {
TextLinkSpan({ item: item })
} else {
VideoLinkSpan({ item: item })
}
})
}
typescript
Span(this.myItem.content)
.fontColor($r('app.color.styled_text_link_font_color'))// 超链接字体颜色
.fontSize($r('app.string.ohos_id_text_size_body1'))
.textBackgroundStyle({ color: this.linkBackgroundColor })
.onClick(() => {
this.linkBackgroundColor = $r('app.color.styled_text_link_clicked_background_color'); // 点击后的背景色
setTimeout(() => {
this.linkBackgroundColor = Color.Transparent;
}, BACKGROUND_CHANGE_DELAY)
// 根据文本超链接的类型做相应处理
if (this.myItem.type === CustomSpanType.Hashtag) {
promptAction.showToast({
message: $r('app.string.styled_text_hashtag_toast_message')
});
} else if (this.myItem.type === CustomSpanType.Mention) {
promptAction.showToast({
message: $r('app.string.styled_text_user_page_toast_message')
});
} else {
promptAction.showToast({
message: $r('app.string.styled_text_content_details_toast_message')
});
}
})
typescript
ContainerSpan() {
ImageSpan($r('app.media.styled_text_ic_public_video'))
.height($r('app.integer.styled_text_video_link_icon_height'))
.verticalAlign(ImageSpanAlignment.CENTER)
Span(this.myItem.content)
.fontColor($r('app.color.styled_text_link_font_color'))
.fontSize($r('app.string.ohos_id_text_size_body1'))
.onClick(() => {
this.linkBackgroundColor = $r('app.color.styled_text_link_clicked_background_color');
setTimeout(() => {
this.linkBackgroundColor = Color.Transparent;
}, BACKGROUND_CHANGE_DELAY)
promptAction.showToast({
message: $r('app.string.styled_text_video_function_message')
});
})
}
.textBackgroundStyle({ color: this.linkBackgroundColor })
- 文本高亮标记功能:通过 Text 和 Span 组件,配合实现文本高亮标记和清除,并且通过键对数据库实现标记数据的持久化。
typescript
markHighlight(highlight: boolean) {
if (this.selectionStart !== -1 && this.selectionEnd !== -1) {
let leftMarks: HighlightMark[] = []; // 区域左侧的文本样式数据
let rightMarks: HighlightMark[] = []; // 区域右侧的文本样式数据
// 当前需要更改样式的区域
let currentSelection: HighlightMark = {
start: this.selectionStart,
end: this.selectionEnd,
color: highlight ? this.colorList[this.currentColor] : Color.Transparent
}
this.markList.forEach((mark) => {
// 如果既有样式区间开头在当前选中区域开头之前
if (mark.start < currentSelection.start) {
// 如果既有样式区间结尾在当前选中区域开头之前,即两个区间无交集
if (mark.end < currentSelection.start) {
leftMarks.push({
start: mark.start,
end: mark.end,
color: mark.color
});
} else if (mark.color === currentSelection.color) {
// 两个区间有交集且样式一致,合并入当前选中区域
currentSelection.start = mark.start;
} else {
// 样式不一致,截断区间
leftMarks.push({
start: mark.start,
end: currentSelection.start,
color: mark.color
});
}
}
// 既有样式区间在当前选中区域末尾之后
if (mark.end > currentSelection.end) {
// 如果既有样式区间开头在当前选中区域开头之后,即两个区间无交集
if (mark.start > currentSelection.end) {
rightMarks.push({
start: mark.start,
end: mark.end,
color: mark.color
});
} else if (mark.color === currentSelection.color) {
// 两个区间有交集且样式一致,合并入当前选中区域
currentSelection.end = mark.end;
} else {
// 样式不一致,截断区间
rightMarks.push({
start: currentSelection.end,
end: mark.end,
color: mark.color
});
}
}
});
// 合并当前与左右文本样式数据
this.markList = leftMarks.concat(currentSelection, rightMarks)
LOGGER.info(JSON.stringify(this.markList));
}
}
- 展示全文:使用measureText(options: MeasureOptions): number接口计算,监听到截断时显示。
typescript
TextInput({ text: this.text, placeholder: 'input your word...', controller: this.controller })
.placeholderColor(Color.Grey)
.placeholderFont({ size: 14, weight: 400 })
.caretColor(Color.Blue)
.width(400)
.height(40)
.margin(20)
.fontSize(14)
.fontColor(Color.Black)
.onChange((value: string) => {
this.text = value
let textSizeShow1 : SizeOptions = measure.measureTextSize({
textContent: this.text,
constraintWidth: 100,
fontSize: 14,
overflow: TextOverflow.Ellipsis,
maxLines: 2
})
let textSizeShow2 : SizeOptions = measure.measureTextSize({
textContent: this.text + ' ',
constraintWidth: 100,
fontSize: 14,
overflow: TextOverflow.Ellipsis,
maxLines: 2000000
})
if (textSizeShow2 &&
textSizeShow1 &&
textSizeShow2?.height &&
textSizeShow1?.height &&
(textSizeShow2?.height > textSizeShow1?.height)) {
this.truncatedHint = '文本截断'
} else {
this.truncatedHint = '文本未截断'
}
})