
回复
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
在基础篇中,我们学习了如何创建一个基本的自定义内容列表,展示不同类型的社交媒体内容。在这个进阶篇中,我们将深入探讨更高级的功能和优化技巧,使自定义内容列表更加完善和专业。
对于长文本内容,我们可以实现展开与折叠功能,提升用户体验:
@State isExpanded: boolean = false
@State showExpandButton: boolean = false
@Builder
AdvancedTextContent(content: string, maxLines: number = 3) {
Column() {
Text(content)
.fontSize(16)
.margin({ top: 12, bottom: this.showExpandButton ? 4 : 12 })
.maxLines(this.isExpanded ? undefined : maxLines)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.onlineRender(true) // 提高渲染性能
.onTouch((event) => {
if (event.type === TouchType.Down) {
// 检测文本是否需要展开按钮
// 实际应用中可以通过测量文本高度来判断
this.showExpandButton = content.length > 100
}
return true
})
if (this.showExpandButton) {
Text(this.isExpanded ? '收起' : '展开')
.fontSize(14)
.fontColor('#007AFF')
.margin({ bottom: 12 })
.onClick(() => {
animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
this.isExpanded = !this.isExpanded
})
})
}
}
}
为图片内容添加点击预览和缩放功能:
@State currentPreviewImage: Resource | null = null
@State showImagePreview: boolean = false
@Builder
AdvancedImageContent(images: Resource[]) {
// 基本图片布局代码...
// 为每个图片添加点击事件
.onClick(() => {
this.currentPreviewImage = image
this.showImagePreview = true
})
// 图片预览弹窗
if (this.showImagePreview && this.currentPreviewImage) {
Column() {
// 关闭按钮
Image($r('app.media.close'))
.width(32)
.height(32)
.position({ top: 40, right: 20 })
.onClick(() => {
this.showImagePreview = false
})
// 图片预览,支持手势缩放
Gesture(
GestureGroup(GestureMode.Exclusive,
PinchGesture({ fingers: 2 })
.onActionUpdate((event) => {
// 实现缩放逻辑
}),
PanGesture()
.onActionUpdate((event) => {
// 实现平移逻辑
})
)
) {
Image(this.currentPreviewImage)
.width('100%')
.height('80%')
.objectFit(ImageFit.Contain)
}
}
.width('100%')
.height('100%')
.backgroundColor('#000000E6')
.position({ x: 0, y: 0 })
.zIndex(999)
}
}
实现视频内容的自动播放、暂停和进度控制:
@State isPlaying: boolean = false
@State currentProgress: number = 0
@State videoDuration: number = 60 // 假设视频时长为60秒
@Builder
AdvancedVideoContent(video: Resource) {
Stack({ alignContent: Alignment.Center }) {
// 实际应用中应使用Video组件
Image(video)
.width('100%')
.height(240)
.objectFit(ImageFit.Cover)
.borderRadius(8)
.opacity(this.isPlaying ? 0.8 : 1)
if (!this.isPlaying) {
// 播放按钮
Image($r('app.media.01'))
.width(60)
.height(60)
.onClick(() => {
this.isPlaying = true
// 开始播放视频的逻辑
this.startVideoPlayback()
})
} else {
// 暂停按钮
Image($r('app.media.pause'))
.width(60)
.height(60)
.opacity(0.7)
.onClick(() => {
this.isPlaying = false
// 暂停视频的逻辑
this.pauseVideoPlayback()
})
}
// 视频进度条
if (this.isPlaying) {
Column() {
Slider({
value: this.currentProgress,
min: 0,
max: this.videoDuration,
step: 1,
style: SliderStyle.OutSet
})
.width('90%')
.onChange((value) => {
this.currentProgress = value
// 更新视频播放进度的逻辑
this.updateVideoProgress(value)
})
Text(`${Math.floor(this.currentProgress / 60)}:${Math.floor(this.currentProgress % 60).toString().padStart(2, '0')} / ${Math.floor(this.videoDuration / 60)}:${Math.floor(this.videoDuration % 60).toString().padStart(2, '0')}`)
.fontSize(12)
.fontColor('#FFFFFF')
}
.width('100%')
.position({ y: '85%' })
}
}
.margin({ top: 12, bottom: 12 })
// 模拟视频播放进度更新
// 实际应用中应该使用Video组件的事件
startVideoPlayback() {
// 模拟视频播放进度更新
this.videoTimer = setInterval(() => {
if (this.currentProgress < this.videoDuration) {
this.currentProgress++
} else {
this.isPlaying = false
clearInterval(this.videoTimer)
this.currentProgress = 0
}
}, 1000)
}
pauseVideoPlayback() {
clearInterval(this.videoTimer)
}
updateVideoProgress(value: number) {
// 实际应用中应该调用Video组件的seek方法
}
}
实现根据屏幕尺寸自动调整的响应式布局:
@State screenWidth: number = 0
aboutToAppear() {
// 获取屏幕宽度
this.screenWidth = px2vp(window.getWindowWidth())
// 监听屏幕旋转事件
window.on('resize', () => {
this.screenWidth = px2vp(window.getWindowWidth())
})
}
// 根据屏幕宽度调整布局
getImageLayout() {
if (this.screenWidth < 600) {
// 窄屏布局
return {
columns: 2,
imageHeight: 120
}
} else if (this.screenWidth < 840) {
// 中等屏幕布局
return {
columns: 3,
imageHeight: 160
}
} else {
// 宽屏布局
return {
columns: 4,
imageHeight: 200
}
}
}
实现瀑布流布局,使内容展示更加丰富多样:
@Builder
WaterfallLayout(posts: Post[]) {
WaterFlow({ footer: this.ListFooter }) {
ForEach(posts, (post: Post) => {
FlowItem() {
// 内容卡片
Column() {
// 用户信息
Row() {
Image(post.user.avatar)
.width(32)
.height(32)
.borderRadius(16)
Text(post.user.name)
.fontSize(14)
.margin({ left: 8 })
}
.width('100%')
.margin({ bottom: 8 })
// 内容展示
if (post.contentType === 'image' && post.media) {
Image(post.media[0])
.width('100%')
.aspectRatio(post.id % 3 === 0 ? 1 : (post.id % 3 === 1 ? 4/3 : 3/4))
.objectFit(ImageFit.Cover)
.borderRadius(8)
}
// 文本内容
Text(post.content)
.fontSize(14)
.margin({ top: 8, bottom: 8 })
.maxLines(3)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 互动信息
Row() {
Row() {
Image($r('app.media.heart_outline'))
.width(16)
.height(16)
Text(post.likes.toString())
.fontSize(12)
.margin({ left: 4 })
}
Row() {
Image($r('app.media.note_icon'))
.width(16)
.height(16)
Text(post.comments.toString())
.fontSize(12)
.margin({ left: 4 })
}
.margin({ left: 16 })
}
.width('100%')
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
.width('100%')
})
}
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.layoutWeight(1)
.padding(8)
}
@Builder
ListFooter() {
Column() {
if (this.isLoading) {
LoadingProgress()
.width(24)
.height(24)
Text('加载中...')
.fontSize(14)
.margin({ top: 8 })
} else {
Text('没有更多内容了')
.fontSize(14)
.fontColor('#999999')
}
}
.width('100%')
.padding({ top: 16, bottom: 16 })
.justifyContent(FlexAlign.Center)
}
实现内容分类和标签过滤功能:
// 内容分类
enum ContentCategory {
All = '全部',
Recommended = '推荐',
Following = '关注',
Trending = '热门',
Nearby = '附近'
}
// 内容标签
interface ContentTag {
id: number;
name: string;
}
// 为Post添加分类和标签
interface Post {
// 原有属性...
category: ContentCategory;
tags: ContentTag[];
}
@State currentCategory: ContentCategory = ContentCategory.All
@State selectedTags: number[] = []
// 过滤内容
getFilteredPosts(): Post[] {
return this.posts.filter(post => {
// 分类过滤
if (this.currentCategory !== ContentCategory.All && post.category !== this.currentCategory) {
return false
}
// 标签过滤
if (this.selectedTags.length > 0) {
const hasSelectedTag = post.tags.some(tag => this.selectedTags.includes(tag.id))
if (!hasSelectedTag) {
return false
}
}
return true
})
}
// 分类选择器UI
@Builder
CategorySelector() {
Row() {
ForEach(Object.values(ContentCategory), (category: string) => {
Text(category)
.fontSize(16)
.fontWeight(this.currentCategory === category ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.currentCategory === category ? '#FF5722' : '#333333')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(this.currentCategory === category ? '#FFF3F0' : 'transparent')
.borderRadius(16)
.margin({ right: 8 })
.onClick(() => {
this.currentCategory = category as ContentCategory
})
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.scrollable(ScrollDirection.Horizontal)
}
实现内容搜索功能:
@State searchText: string = ''
@State isSearching: boolean = false
@State searchResults: Post[] = []
// 搜索内容
searchPosts() {
if (this.searchText.trim() === '') {
this.searchResults = []
return
}
// 简单的文本匹配搜索
this.searchResults = this.posts.filter(post => {
return post.content.toLowerCase().includes(this.searchText.toLowerCase()) ||
post.user.name.toLowerCase().includes(this.searchText.toLowerCase())
})
}
// 搜索框UI
@Builder
SearchBar() {
Row() {
if (!this.isSearching) {
Image($r('app.media.search'))
.width(24)
.height(24)
.margin({ right: 8 })
} else {
Image($r('app.media.back'))
.width(24)
.height(24)
.margin({ right: 8 })
.onClick(() => {
this.isSearching = false
this.searchText = ''
this.searchResults = []
})
}
TextInput({ placeholder: '搜索内容', text: this.searchText })
.width('80%')
.height(40)
.backgroundColor('#F5F5F5')
.borderRadius(20)
.padding({ left: 16, right: 16 })
.onChange((value) => {
this.searchText = value
this.searchPosts()
})
.onSubmit(() => {
this.searchPosts()
})
.onClick(() => {
this.isSearching = true
})
if (this.searchText !== '') {
Image($r('app.media.clear'))
.width(24)
.height(24)
.margin({ left: 8 })
.onClick(() => {
this.searchText = ''
this.searchResults = []
})
}
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
}
// 在应用启动时初始化
AppStorage.SetOrCreate('likedPosts', new Set<number>())
AppStorage.SetOrCreate('savedPosts', new Set<number>())
// 在组件中使用
@StorageLink('likedPosts') likedPosts: Set<number> = new Set<number>()
@StorageLink('savedPosts') savedPosts: Set<number> = new Set<number>()
// 点赞操作
toggleLike(id: number) {
if (this.likedPosts.has(id)) {
this.likedPosts.delete(id)
} else {
this.likedPosts.add(id)
}
// 更新UI状态
this.posts = this.posts.map(post => {
if (post.id === id) {
post.isLiked = this.likedPosts.has(id)
post.likes = post.isLiked ? post.likes + 1 : post.likes - 1
}
return post
})
}
// 保存操作
toggleSave(id: number) {
if (this.savedPosts.has(id)) {
this.savedPosts.delete(id)
} else {
this.savedPosts.add(id)
}
}
// 定义持久化存储
PersistentStorage.PersistProp<Set<number>>('likedPosts', new Set<number>())
PersistentStorage.PersistProp<Set<number>>('savedPosts', new Set<number>())
// 在组件中使用
@StorageProp('likedPosts') likedPosts: Set<number> = new Set<number>()
@StorageProp('savedPosts') savedPosts: Set<number> = new Set<number>()
实现滑动显示操作菜单的功能:
@State swipedPostId: number | null = null
@Builder
SwipeablePostItem(post: Post) {
Stack() {
// 操作菜单背景
Row() {
Button() {
Image($r('app.media.share'))
.width(24)
.height(24)
.fillColor('#FFFFFF')
}
.width(80)
.height('100%')
.backgroundColor('#4CAF50')
.onClick(() => {
// 分享操作
this.sharePost(post)
})
Button() {
Image($r('app.media.delete'))
.width(24)
.height(24)
.fillColor('#FFFFFF')
}
.width(80)
.height('100%')
.backgroundColor('#F44336')
.onClick(() => {
// 删除操作
this.deletePost(post.id)
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.End)
// 内容卡片
Column() {
// 帖子内容...
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.translate({ x: this.swipedPostId === post.id ? -160 : 0 })
.gesture(
PanGesture({ direction: PanDirection.Horizontal })
.onActionStart(() => {
// 开始滑动时记录当前帖子ID
this.swipedPostId = post.id
})
.onActionUpdate((event) => {
// 限制最大滑动距离
if (event.offsetX < -160) {
event.offsetX = -160
} else if (event.offsetX > 0) {
event.offsetX = 0
}
})
.onActionEnd((event) => {
// 根据滑动距离决定是否显示操作菜单
if (event.offsetX < -80) {
animateTo({ duration: 300 }, () => {
// 显示完整操作菜单
this.swipedPostId = post.id
})
} else {
animateTo({ duration: 300 }, () => {
// 恢复原位
this.swipedPostId = null
})
}
})
)
}
.width('100%')
.height(post.contentType === 'text' ? 'auto' : 'auto')
.clip(true)
}
实现双击点赞功能:
@Builder
DoubleTapLikeContent(post: Post) {
Stack() {
// 内容展示
// ...
// 点赞动画
if (this.doubleTapLikePostId === post.id) {
Image($r('app.media.heart_filled'))
.width(80)
.height(80)
.fillColor('#FF5722')
.opacity(this.likeAnimationOpacity)
.scale({ x: this.likeAnimationScale, y: this.likeAnimationScale })
.onAppear(() => {
// 播放点赞动画
animateTo(
{ duration: 600, curve: Curve.Ease },
() => {
this.likeAnimationScale = 1.2
this.likeAnimationOpacity = 0
}
)
})
.onDisAppear(() => {
// 重置动画状态
this.likeAnimationScale = 0.5
this.likeAnimationOpacity = 1
this.doubleTapLikePostId = null
})
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.gesture(
TapGesture({ count: 2 })
.onAction(() => {
// 双击点赞
if (!post.isLiked) {
this.toggleLike(post.id)
}
// 显示点赞动画
this.doubleTapLikePostId = post.id
})
)
}
实现内容分享菜单:
@State showShareMenu: boolean = false
@State sharePostId: number | null = null
// 分享帖子
sharePost(post: Post) {
this.sharePostId = post.id
this.showShareMenu = true
}
@Builder
ShareMenu() {
if (this.showShareMenu && this.sharePostId) {
Column() {
// 分享标题
Text('分享到')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.margin({ top: 16, bottom: 16 })
// 分享选项
Grid() {
// 微信
GridItem() {
Column() {
Image($r('app.media.wechat'))
.width(48)
.height(48)
.borderRadius(24)
Text('微信')
.fontSize(14)
.margin({ top: 8 })
}
.onClick(() => {
// 分享到微信
this.shareToWechat()
})
}
// 朋友圈
GridItem() {
Column() {
Image($r('app.media.moments'))
.width(48)
.height(48)
.borderRadius(24)
Text('朋友圈')
.fontSize(14)
.margin({ top: 8 })
}
.onClick(() => {
// 分享到朋友圈
this.shareToMoments()
})
}
// 微博
GridItem() {
Column() {
Image($r('app.media.weibo'))
.width(48)
.height(48)
.borderRadius(24)
Text('微博')
.fontSize(14)
.margin({ top: 8 })
}
.onClick(() => {
// 分享到微博
this.shareToWeibo()
})
}
// 更多选项...
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr')
.columnsGap(16)
.width('100%')
.padding({ left: 16, right: 16 })
// 取消按钮
Button('取消')
.width('90%')
.height(44)
.margin({ top: 24, bottom: 16 })
.backgroundColor('#F5F5F5')
.fontColor('#333333')
.onClick(() => {
this.showShareMenu = false
})
}
.width('100%')
.padding({ top: 16, bottom: 16 })
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.position({ x: 0, y: '70%' })
}
}
实现评论功能:
interface Comment {
id: number;
postId: number;
user: User;
content: string;
time: string;
likes: number;
isLiked: boolean;
replies?: Comment[];
}
@State showComments: boolean = false
@State currentPostId: number | null = null
@State commentText: string = ''
@State comments: Comment[] = [
// 初始评论数据
]
// 打开评论面板
openComments(postId: number) {
this.currentPostId = postId
this.showComments = true
}
// 添加评论
addComment() {
if (this.commentText.trim() === '' || !this.currentPostId) {
return
}
// 创建新评论
const newComment: Comment = {
id: this.comments.length + 1,
postId: this.currentPostId,
user: {
name: '我',
avatar: $r('app.media.avatar_me')
},
content: this.commentText,
time: '刚刚',
likes: 0,
isLiked: false
}
// 添加到评论列表
this.comments.unshift(newComment)
// 更新帖子评论数
this.posts = this.posts.map(post => {
if (post.id === this.currentPostId) {
post.comments++
}
return post
})
// 清空输入框
this.commentText = ''
}
@Builder
CommentPanel() {
if (this.showComments && this.currentPostId) {
Column() {
// 评论标题
Row() {
Text('评论')
.fontSize(18)
.fontWeight(FontWeight.Medium)
Blank()
Image($r('app.media.close'))
.width(24)
.height(24)
.onClick(() => {
this.showComments = false
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
// 评论列表
List() {
ForEach(this.getPostComments(this.currentPostId), (comment: Comment) => {
ListItem() {
Row() {
// 用户头像
Image(comment.user.avatar)
.width(40)
.height(40)
.borderRadius(20)
// 评论内容
Column() {
Text(comment.user.name)
.fontSize(14)
.fontWeight(FontWeight.Medium)
Text(comment.content)
.fontSize(16)
.margin({ top: 4, bottom: 4 })
Row() {
Text(comment.time)
.fontSize(12)
.fontColor('#999999')
Text('回复')
.fontSize(12)
.fontColor('#666666')
.margin({ left: 16 })
Blank()
Row() {
Image(comment.isLiked ? $r('app.media.heart_filled') : $r('app.media.heart_outline'))
.width(16)
.height(16)
.fillColor(comment.isLiked ? '#FF5722' : '#666666')
Text(comment.likes.toString())
.fontSize(12)
.fontColor('#666666')
.margin({ left: 4 })
}
.onClick(() => {
// 点赞评论
this.toggleCommentLike(comment.id)
})
}
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
.layoutWeight(1)
}
.width('100%')
.padding({ top: 12, bottom: 12 })
}
})
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16 })
// 评论输入框
Row() {
TextInput({ placeholder: '添加评论...', text: this.commentText })
.width('80%')
.height(40)
.backgroundColor('#F5F5F5')
.borderRadius(20)
.padding({ left: 16, right: 16 })
.onChange((value) => {
this.commentText = value
})
Button() {
Text('发送')
.fontSize(14)
.fontColor(this.commentText.trim() !== '' ? '#FFFFFF' : '#AAAAAA')
}
.width(60)
.height(40)
.margin({ left: 8 })
.borderRadius(20)
.backgroundColor(this.commentText.trim() !== '' ? '#007AFF' : '#F5F5F5')
.onClick(() => {
this.addComment()
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor('#FFFFFF')
.borderColor('#E5E5E5')
.borderWidth({ top: 1 })
}
.width('100%')
.height('80%')
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.position({ x: 0, y: '20%' })
}
}
// 获取指定帖子的评论
getPostComments(postId: number): Comment[] {
return this.comments.filter(comment => comment.postId === postId)
}
// 点赞评论
toggleCommentLike(commentId: number) {
this.comments = this.comments.map(comment => {
if (comment.id === commentId) {
comment.isLiked = !comment.isLiked
comment.likes = comment.isLiked ? comment.likes + 1 : comment.likes - 1
}
return comment
})
}
问题:当列表项较多且复杂时,可能导致滚动卡顿和内存占用过高。
解决方案:
LazyForEach
替代ForEach
,实现虚拟列表cachedCount
值,控制缓存的列表项数量onlineRender
属性提高渲染性能问题:大量图片加载可能导致内存占用过高和UI卡顿。
解决方案:
syncLoad(false)
异步加载图片问题:多个手势可能发生冲突,导致交互体验不佳。
解决方案:
GestureGroup
和GestureMode.Exclusive
设置手势优先级event.stopPropagation()
阻止事件传播在本教程中,我们深入探讨了如何实现一个功能丰富、交互良好的自定义内容列表,包括高级内容交互与动画、响应式布局、内容过滤与搜索、高级状态管理、手势交互以及社交功能等方面。