Read the original article:How to achieve the WeChat Moments effect
How to achieve the WeChat Moments effect
Requirement Description
How to achieve the WeChat Moments effect?
Background Knowledge
The Listcomponent contains a series of list items of the same width. It is suitable for presenting similar data continuously and in multiple rows, such as images and text.
LazyForEachiterates over data from the data source on demand and creates corresponding components during each iteration. When LazyForEachis used in a scrolling container, the framework creates components on demand based on the visible area of the scrolling container. When components scroll out of the visible area, the framework destroys and recycles them to reduce memory usage.
Refresh is a container component that allows for page pull-down operations and displays refresh animations. @ObservedV2 and @Trace are core capabilities in state management V2. These decorators are used to decorate classes and properties within classes, enabling the decorated classes and properties to have deep observation capabilities:
Implementation Steps
Create a Refreshcomponent on the main page and use LazyForEach within this component to render MomentListItem. Implement the alternating rendering of image and video components in a specific manner, which is the framework functionality of this demo.
In the MomentListItemcomponent, display the user's nickname, avatar, time, text content, comments, and likes. Depending on the data attributes, display images and videos accordingly.
Code Snippet / Configuration
// Moment.ets
import { CommonDataSource } from './CommonDataSource';
import { MomentItem } from './ListModel';
import { MomentListItem } from './MomentListItem';
@Entry
@Component
struct Moments {
@State isRefreshing: boolean = false
private listDataSource = new CommonDataSource<MomentItem>()
aboutToAppear(): void {
this.getListData()
}
getListData() {
let followList: MomentItem[] = []
for (let i = 0; i < 10; i++) {
//The 'itemData value' here is for illustration purposes only.
let itemData: MomentItem = new MomentItem()
itemData.id = 'id_' + i
itemData.userId = 'userId_' + i
itemData.userName = 'userName'
itemData.title = '6666'
itemData.postingTime = '2025/07/09 10:30:00'
itemData.isFollow = false
if (i % 2 === 0) {
itemData.images = [
// Here, 'app.media.xxx' is used as an example only.
$r('app.media.img1'),
$r('app.media.img2'),
$r('app.media.img3'),
$r('app.media.img4'),
$r('app.media.img5'),
$r('app.media.img6'),
$r('app.media.img7'),
$r('app.media.img8'),
$r('app.media.img9'),
]
} else {
// Here, 'video.mp4' is used as an example only.
itemData.videoUrl = $rawfile('video.mp4')
}
itemData.likeCount = i
itemData.favorite = i
followList.push(itemData)
}
this.listDataSource.setData(followList)
}
@Builder
ListContent() {
Refresh({ refreshing: $$this.isRefreshing }) {
List({ space: 20 }) {
LazyForEach(this.listDataSource, (item: MomentItem, index: number) => {
ListItem() {
MomentListItem({
data: item
})
}
.padding({ left: 14, right: 14 })
.backgroundColor(Color.White)
.borderRadius(6)
})
}
.width('100%')
.height('100%')
.padding({ left: 14, right: 14 })
}
.onRefreshing(() => {
setTimeout(() => {
this.isRefreshing = false
}, 1000)
})
.pullToRefresh(true)
}
build() {
Column() {
this.ListContent()
}
.width('100%')
.height('100%')
.backgroundColor('#fff6f3f3')
}
}
// ListModel.ets
@ObservedV2
export class MomentItem {
id: string = ''
userId: string = ''
@Trace userName: string = ''
// Here, 'app.media.startIcon' is used as an example only.
@Trace userIcon: Resource = $r('app.media.startIcon')
@Trace title: string = ''
@Trace postingTime: string = ''
@Trace isFollow: boolean = false
@Trace images: Resource[] = []
@Trace videoUrl: Resource | string = ''
@Trace videoPreviewUrl: Resource | string = ''
@Trace likeCount: number = 0
@Trace shareCount: number = 0
@Trace commentCount: number = 0
favorite: number = 0
}
// MomentListItem.ets
import { MomentItem } from './ListModel'
@Component
export struct MomentListItem {
data: MomentItem | null = null
controller: VideoController = new VideoController();
isPlay: boolean = false
getPostingTime(time: string = ''): string {
let date: Date = new Date(time)
let M: number = date.getMonth() + 1
let D: number = date.getDate()
let h: number = date.getHours()
let m: number = date.getMinutes()
return `${M}-${D} ${h}:${m}`
}
build() {
Column() {
Row() {
Image(this.data?.userIcon).width(30).height(30)
Column() {
Text(this.data?.userName).fontSize(13).fontWeight(FontWeight.Bold)
Text(this.getPostingTime(this.data?.postingTime))
.fontSize(10)
.fontColor('#ffbab8b8')
.margin({ top: 2 })
}
.offset({ x: 8 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
Row() {
Text(this.data?.title)
.fontSize(16)
.fontColor('#191919')
.margin({ top: 12 })
}
.justifyContent(FlexAlign.Start)
.width('100%')
Column() {
if (this.data?.images && this.data.images.length) {
if (this.data.images.length > 2) {
this.imageGridBuilder(this.data.images)
} else {
this.imageBuilder(this.data.images)
}
}
if (this.data?.videoUrl) {
this.videoBuilder(this.data)
}
}
.margin({ top: 8 })
.alignItems(HorizontalAlign.Start)
//Check if there are comments
if (true) {
Column({ space: 10 }) {
ForEach([1, 2, 3], () => {
this.CommentListView()
})
//Check if the number of comments exceeds 3
if (true) {
Text('View more replies')
.fontColor('#007dff')
.fontSize(12)
.margin({ left: 38 })
.onClick(() => {
})
}
}
.padding({
left: 14,
right: 14,
top: 14,
bottom: 12
})
.backgroundColor('#ffeae9e9')
.margin({ top: 10 })
.borderRadius(4)
.alignItems(HorizontalAlign.Start)
}
Row({ space: 40 }) {
//'app.media.xxx' is only an example.
this.cellItemBuilder($r('app.media.comment'),
this.data?.commentCount && this.data.commentCount > 0 ? this.data?.commentCount + '' : 'Comments')
this.cellItemBuilder($r('app.media.thumbup'),
this.data?.likeCount && this.data.likeCount > 0 ? this.data?.likeCount + '' : 'like')
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({
top: 20,
bottom: 20,
})
}
.width('100%')
.height('auto')
.margin({ top: 16 })
}
@Builder
CommentListView() {
Column() {
Row() {
Image(this.data?.userIcon).width(30).height(30)
Column() {
Text(this.data?.userName).fontSize(12).fontColor('#999')
Text(this.getPostingTime(this.data?.postingTime))
.fontSize(10)
.fontColor('#ffb1b0b0')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.offset({ x: 8 })
}
.width('100%')
Text(this.data?.title)
.fontSize(12)
.fontColor('#191919')
.margin({ left: 38 })
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
@Builder
imageBuilder(images: Resource[]) {
Row({ space: 6 }) {
ForEach(images, (item: Resource, index) => {
Image(item).width(`${100 / 2}%`)
})
}
}
@Builder
imageGridBuilder(images: Resource[]) {
Grid() {
ForEach(images, (item: Resource) => {
GridItem() {
Column() {
Image(item)
.borderRadius(4)
.width('calc((100% - 16vp) / 3)')
.aspectRatio(1)
}
.onClick(() => {
})
}
})
}
.width('100%')
.constraintSize({ maxWidth: '100%' })
.maxCount(3)
.columnsGap(8)
.rowsGap(8)
.backgroundColor(Color.White)
}
@Builder
videoBuilder(item: MomentItem) {
Stack() {
Video({ src: item.videoUrl, controller: this.controller })
.width('50%')
.height(220)
.loop(false)
.objectFit(ImageFit.Contain)
.autoPlay(true)
.controls(false)
}
.onClick(() => {
this.isPlay = !this.isPlay
if (this.isPlay) {
this.controller.start()
} else {
this.controller.pause()
}
})
}
@Builder
cellItemBuilder(icon: Resource, text: string) {
Row() {
Image(icon).width(16).height(16)
Text(text).fontSize(12).margin({ left: 2 })
}
}
}
// CommonDataSource.ets
export class CommonDataSource<T> implements IDataSource {
private listeners: DataChangeListener[] = [];
originDataArray: T[] = [];
totalCount(): number {
return this.originDataArray.length;
}
getAllData(): T[] {
return this.originDataArray
}
getData(index: number) {
return this.originDataArray[index];
}
addData(index: number, data: T): void {
this.originDataArray.splice(index, 0, data);
this.notifyDataAdd(index);
}
pushData(data: T): void {
this.originDataArray.push(data);
this.notifyDataAdd(this.originDataArray.length - 1);
}
pushDataArray(...items: T[]): void {
for (let data of items) {
this.originDataArray.push(data);
this.notifyDataAdd(this.originDataArray.length - 1);
}
}
deleteDataUseContent(data: T): void {
let delIndex: number = -1
for (let index = 0; index < this.originDataArray.length; index++) {
const element = this.originDataArray[index];
if (data === element) {
delIndex = index
}
}
if (delIndex !== -1) {
this.deleteData(delIndex)
}
}
deleteData(index: number): void {
this.originDataArray.splice(index, 1);
this.notifyDataDelete(index);
}
clear() {
this.originDataArray.splice(0, this.originDataArray.length)
this.listeners.forEach(listener => {
listener.onDataDelete(0)
})
}
setData(dataArray?: T[]) {
if (dataArray) {
this.originDataArray = dataArray
} else {
this.originDataArray = []
}
this.notifyDataReload()
}
refreshDataByIndex(start: number, end: number, dataArray: T[]) {
this.originDataArray.splice(start, end - start, ...dataArray);
this.notifyDataReload()
}
changeData(index: number, data: T): void {
this.originDataArray.splice(index, 1, data);
this.notifyDataChange(index);
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
notifyDataReload() {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}
notifyDataAdd(index: number) {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}
notifyDataMove(from: number, to: number) {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
})
}
notifyDataDelete(index: number) {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
})
}
notifyDataChange(index: number) {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}
}
Test Results
Limitations or Considerations
This example supports API Version 19 Release and above.
This example supports HarmonyOS 5.1.1 Release SDK and above.
This example requires DevEco Studio 5.1.1 Release and above for compilation.
Top comments (0)