> 技术文档 > 154.[HarmonyOS NEXT 实战案例十一 :List系列] 自定义内容列表 - 进阶篇

154.[HarmonyOS NEXT 实战案例十一 :List系列] 自定义内容列表 - 进阶篇


[HarmonyOS NEXT 实战案例十一 :List系列] 自定义内容列表 - 进阶篇

文章目录

  • [HarmonyOS NEXT 实战案例十一 :List系列] 自定义内容列表 - 进阶篇
    • 效果演示
    • 1. 概述
    • 2. 高级内容交互与动画
      • 2.1 内容展开与折叠
      • 2.2 图片浏览与缩放
      • 2.3 视频内容的高级处理
    • 3. 高级布局与自适应设计
      • 3.1 响应式布局
      • 3.2 瀑布流布局
    • 4. 内容过滤与个性化推荐
      • 4.1 内容分类与标签
      • 4.2 内容搜索功能
    • 5. 高级状态管理
      • 5.1 使用AppStorage进行状态管理
      • 5.2 使用PersistentStorage持久化状态
    • 6. 手势交互与操作
      • 6.1 滑动操作菜单
      • 6.2 双击点赞
    • 7. 内容分享与社交功能
      • 7.1 分享菜单
      • 7.2 评论功能
    • 8. 常见问题与解决方案
      • 8.1 列表性能优化
      • 8.2 图片加载优化
      • 8.3 手势冲突处理
    • 9. 总结与扩展

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

154.[HarmonyOS NEXT 实战案例十一 :List系列] 自定义内容列表 - 进阶篇

1. 概述

在基础篇中,我们学习了如何创建一个基本的自定义内容列表,展示不同类型的社交媒体内容。在这个进阶篇中,我们将深入探讨更高级的功能和优化技巧,使自定义内容列表更加完善和专业。

1.1 进阶功能概览

  • 内容交互与动画效果
  • 高级布局与自适应设计
  • 内容过滤与个性化推荐
  • 高级状态管理
  • 手势交互与操作
  • 内容分享与社交功能

2. 高级内容交互与动画

2.1 内容展开与折叠

对于长文本内容,我们可以实现展开与折叠功能,提升用户体验:

@State isExpanded: boolean = false@State showExpandButton: boolean = false@BuilderAdvancedTextContent(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  }) }) } }}

2.2 图片浏览与缩放

为图片内容添加点击预览和缩放功能:

@State currentPreviewImage: Resource | null = null@State showImagePreview: boolean = false@BuilderAdvancedImageContent(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) }}

2.3 视频内容的高级处理

实现视频内容的自动播放、暂停和进度控制:

@State isPlaying: boolean = false@State currentProgress: number = 0@State videoDuration: number = 60 // 假设视频时长为60秒@BuilderAdvancedVideoContent(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方法 }}

3. 高级布局与自适应设计

3.1 响应式布局

实现根据屏幕尺寸自动调整的响应式布局:

@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 } }}

3.2 瀑布流布局

实现瀑布流布局,使内容展示更加丰富多样:

@BuilderWaterfallLayout(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)}@BuilderListFooter() { 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)}

4. 内容过滤与个性化推荐

4.1 内容分类与标签

实现内容分类和标签过滤功能:

// 内容分类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@BuilderCategorySelector() { 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)}

4.2 内容搜索功能

实现内容搜索功能:

@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@BuilderSearchBar() { 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 })}

5. 高级状态管理

5.1 使用AppStorage进行状态管理

// 在应用启动时初始化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) }}

5.2 使用PersistentStorage持久化状态

// 定义持久化存储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>()

6. 手势交互与操作

6.1 滑动操作菜单

实现滑动显示操作菜单的功能:

@State swipedPostId: number | null = null@BuilderSwipeablePostItem(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)}

6.2 双击点赞

实现双击点赞功能:

@BuilderDoubleTapLikeContent(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 }) )}

7. 内容分享与社交功能

7.1 分享菜单

实现内容分享菜单:

@State showShareMenu: boolean = false@State sharePostId: number | null = null// 分享帖子sharePost(post: Post) { this.sharePostId = post.id this.showShareMenu = true}@BuilderShareMenu() { 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%\' }) }}

7.2 评论功能

实现评论功能:

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 = \'\'}@BuilderCommentPanel() { 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 })}

8. 常见问题与解决方案

8.1 列表性能优化

问题:当列表项较多且复杂时,可能导致滚动卡顿和内存占用过高。

解决方案

  • 使用LazyForEach替代ForEach,实现虚拟列表
  • 设置合理的cachedCount值,控制缓存的列表项数量
  • 使用onlineRender属性提高渲染性能
  • 减少列表项的嵌套层级和复杂度

8.2 图片加载优化

问题:大量图片加载可能导致内存占用过高和UI卡顿。

解决方案

  • 使用syncLoad(false)异步加载图片
  • 实现图片懒加载,只在图片进入视口时才加载
  • 使用适当的图片尺寸和压缩比例
  • 实现图片缓存机制,避免重复加载

8.3 手势冲突处理

问题:多个手势可能发生冲突,导致交互体验不佳。

解决方案

  • 使用GestureGroupGestureMode.Exclusive设置手势优先级
  • 合理设置手势的触发条件和范围
  • 使用event.stopPropagation()阻止事件传播

9. 总结与扩展

在本教程中,我们深入探讨了如何实现一个功能丰富、交互良好的自定义内容列表,包括高级内容交互与动画、响应式布局、内容过滤与搜索、高级状态管理、手势交互以及社交功能等方面。