154.[HarmonyOS NEXT 实战案例十一 :List系列] 自定义内容列表 - 进阶篇
[HarmonyOS NEXT 实战案例十一 :List系列] 自定义内容列表 - 进阶篇
文章目录
- [HarmonyOS NEXT 实战案例十一 :List系列] 自定义内容列表 - 进阶篇
-
- 效果演示
- 1. 概述
-
- 1.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
效果演示
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 手势冲突处理
问题:多个手势可能发生冲突,导致交互体验不佳。
解决方案:
- 使用
GestureGroup
和GestureMode.Exclusive
设置手势优先级 - 合理设置手势的触发条件和范围
- 使用
event.stopPropagation()
阻止事件传播
9. 总结与扩展
在本教程中,我们深入探讨了如何实现一个功能丰富、交互良好的自定义内容列表,包括高级内容交互与动画、响应式布局、内容过滤与搜索、高级状态管理、手势交互以及社交功能等方面。