[HarmonyOS NEXT 实战案例二] 新闻资讯网格列表(下)

2025-06-06 22:42:33
104次阅读
0个评论

[HarmonyOS NEXT 实战案例二] 新闻资讯网格列表(下)

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

效果演示

img_c318711c.png

1. 引言

在上一篇教程中,我们介绍了如何使用HarmonyOS NEXT的GridRow和GridCol组件实现基本的新闻资讯列表布局。本篇教程将深入探讨如何优化新闻列表布局,并添加更多交互功能,提升用户体验。

2. 新闻列表布局优化

2.1 分类标签栏

在新闻应用中,通常会有不同的新闻分类,如头条、科技、体育等。我们可以在列表顶部添加一个分类标签栏:

@State currentCategory: string = '头条'
private categories: string[] = ['头条', '科技', '体育', '财经', '娱乐']

build() {
    Column() {
        // 分类标签栏
        Scroll({ direction: ScrollDirection.Horizontal }) {
            Row({ space: 16 }) {
                ForEach(this.categories, (category: string) => {
                    Text(category)
                        .fontSize(16)
                        .fontColor(this.currentCategory === category ? '#FF5722' : '#333333')
                        .fontWeight(this.currentCategory === category ? FontWeight.Bold : FontWeight.Normal)
                        .onClick(() => {
                            this.currentCategory = category
                            // 加载对应分类的新闻
                            this.loadNewsByCategory(category)
                        })
                })
            }
            .padding({ left: 12, right: 12, top: 12, bottom: 12 })
        }
        .scrollBar(BarState.Off)
        
        // 新闻列表
        GridRow({ columns: 1 }) {
            // 新闻列表内容
        }
    }
}

2.2 新闻项布局变体

为了增加视觉多样性,我们可以实现多种新闻项布局变体:

2.2.1 左图右文布局(默认)

Row() {
    Image(item.image)
        .width(100)
        .height(80)
        .objectFit(ImageFit.Cover)
        .borderRadius(4)

    Column() {
        Text(item.title)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .margin({ left: 12 })

        Text(item.summary)
            .fontSize(14)
            .margin({ left: 12, top: 4 })
    }
    .alignItems(HorizontalAlign.Start)
}

2.2.2 上图下文布局

Column() {
    Image(item.image)
        .width('100%')
        .height(160)
        .objectFit(ImageFit.Cover)
        .borderRadius(4)

    Text(item.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 8 })
        .width('100%')
        .textAlign(TextAlign.Start)

    Text(item.summary)
        .fontSize(14)
        .margin({ top: 4 })
        .width('100%')
        .textAlign(TextAlign.Start)
}

2.2.3 三图布局

Column() {
    Text(item.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .textAlign(TextAlign.Start)

    Row({ space: 8 }) {
        ForEach(item.images, (image: ResourceStr) => {
            Image(image)
                .width('0%')
                .aspectRatio(1)
                .objectFit(ImageFit.Cover)
                .borderRadius(4)
                .layoutWeight(1)
        })
    }
    .margin({ top: 8 })

    Text(item.summary)
        .fontSize(14)
        .margin({ top: 8 })
        .width('100%')
        .textAlign(TextAlign.Start)
}

2.3 根据内容类型选择布局

我们可以根据新闻内容类型自动选择合适的布局:

interface NewsItemType {
    title: string;
    summary: string;
    image: ResourceStr;
    images?: ResourceStr[];
    type: 'normal' | 'large' | 'multi';
}

build() {
    Column() {
        GridRow({ columns: 1 }) {
            ForEach(this.newsItems, (item: NewsItemType) => {
                GridCol({ span: 1 }) {
                    if (item.type === 'normal') {
                        // 左图右文布局
                    } else if (item.type === 'large') {
                        // 上图下文布局
                    } else if (item.type === 'multi') {
                        // 三图布局
                    }
                }
            })
        }
    }
}

3. 交互功能实现

3.1 下拉刷新

使用Refresh组件实现下拉刷新功能:

@State refreshing: boolean = false

build() {
    Refresh({ refreshing: this.refreshing, offset: 120, friction: 66 }) {
        Column() {
            // 分类标签栏和新闻列表
        }
    }
    .onRefreshing(() => {
        // 刷新数据
        this.refreshData()
    })
}

private refreshData() {
    this.refreshing = true
    // 模拟网络请求
    setTimeout(() => {
        // 更新新闻数据
        this.refreshing = false
    }, 1000)
}

3.2 加载更多

在列表底部添加加载更多功能:

@State loading: boolean = false
@State hasMore: boolean = true

build() {
    Column() {
        GridRow({ columns: 1 }) {
            // 新闻列表内容
        }
        
        if (this.loading) {
            Row() {
                LoadingProgress()
                    .width(24)
                    .height(24)
                    .color('#999999')
                
                Text('加载中...')
                    .fontSize(14)
                    .fontColor('#999999')
                    .margin({ left: 8 })
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
            .padding(16)
        } else if (this.hasMore) {
            Text('点击加载更多')
                .fontSize(14)
                .fontColor('#666666')
                .width('100%')
                .textAlign(TextAlign.Center)
                .padding(16)
                .onClick(() => {
                    this.loadMore()
                })
        } else {
            Text('没有更多内容了')
                .fontSize(14)
                .fontColor('#999999')
                .width('100%')
                .textAlign(TextAlign.Center)
                .padding(16)
        }
    }
}

private loadMore() {
    this.loading = true
    // 模拟网络请求
    setTimeout(() => {
        // 加载更多新闻
        this.loading = false
        // 判断是否还有更多
        this.hasMore = this.newsItems.length < 20
    }, 1000)
}

3.3 新闻项点击事件

为新闻项添加点击事件,实现跳转到新闻详情页的功能:

Row() {
    // 新闻项内容
}
.onClick(() => {
    // 跳转到新闻详情页
    console.info(`点击新闻: ${item.title}`)
})

3.4 新闻收藏功能

添加新闻收藏功能:

interface NewsItemType {
    title: string;
    summary: string;
    image: ResourceStr;
    type: 'normal' | 'large' | 'multi';
    isCollected: boolean;
}

Row() {
    // 新闻内容
    
    Image(item.isCollected ? $r('app.media.star_filled') : $r('app.media.star_outline'))
        .width(24)
        .height(24)
        .margin({ left: 8 })
        .onClick((event: ClickEvent) => {
            // 阻止事件冒泡
            event.stopPropagation()
            // 切换收藏状态
            this.toggleCollect(item)
        })
}

private toggleCollect(item: NewsItemType) {
    const index = this.newsItems.indexOf(item)
    if (index !== -1) {
        this.newsItems[index].isCollected = !this.newsItems[index].isCollected
    }
}

4. 数据状态管理

4.1 加载状态

使用状态变量管理数据加载状态:

@State dataState: 'loading' | 'success' | 'empty' | 'error' = 'loading'

build() {
    Column() {
        if (this.dataState === 'loading') {
            // 显示加载中状态
            LoadingProgress()
                .width(32)
                .height(32)
                .color('#FF5722')
        } else if (this.dataState === 'empty') {
            // 显示空状态
            Column() {
                Image($r('app.media.empty'))
                    .width(120)
                    .height(120)
                
                Text('暂无新闻')
                    .fontSize(16)
                    .fontColor('#999999')
                    .margin({ top: 16 })
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
            .padding(64)
        } else if (this.dataState === 'error') {
            // 显示错误状态
            Column() {
                Image($r('app.media.error'))
                    .width(120)
                    .height(120)
                
                Text('加载失败,请重试')
                    .fontSize(16)
                    .fontColor('#999999')
                    .margin({ top: 16 })
                
                Button('重新加载')
                    .margin({ top: 16 })
                    .onClick(() => {
                        this.loadData()
                    })
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
            .padding(64)
        } else {
            // 显示新闻列表
            GridRow({ columns: 1 }) {
                // 新闻列表内容
            }
        }
    }
}

4.2 数据过滤

实现新闻数据的过滤功能:

@State searchText: string = ''

build() {
    Column() {
        // 搜索框
        TextInput({ placeholder: '搜索新闻' })
            .width('100%')
            .height(40)
            .backgroundColor('#F5F5F5')
            .borderRadius(20)
            .padding({ left: 16, right: 16 })
            .onChange((value: string) => {
                this.searchText = value
            })
        
        // 新闻列表
        GridRow({ columns: 1 }) {
            ForEach(this.getFilteredNews(), (item: NewsItemType) => {
                // 新闻项
            })
        }
    }
}

private getFilteredNews(): NewsItemType[] {
    if (!this.searchText) {
        return this.newsItems
    }
    
    return this.newsItems.filter(item => 
        item.title.includes(this.searchText) || 
        item.summary.includes(this.searchText)
    )
}

5. 完整代码示例

以下是优化后的新闻资讯列表完整代码示例:

@Component
export struct EnhancedNewsGrid {
    @State refreshing: boolean = false
    @State loading: boolean = false
    @State hasMore: boolean = true
    @State dataState: 'loading' | 'success' | 'empty' | 'error' = 'success'
    @State currentCategory: string = '头条'
    @State searchText: string = ''
    
    private categories: string[] = ['头条', '科技', '体育', '财经', '娱乐']
    
    @State newsItems: NewsItemType[] = [
        { 
            title: 'HarmonyOS 4.0发布', 
            summary: '全新分布式能力升级', 
            image: $r("app.media.big1"),
            type: 'normal',
            isCollected: false
        },
        { 
            title: '华为开发者大会', 
            summary: '2023年HDC即将召开', 
            image: $r("app.media.big31"),
            type: 'large',
            isCollected: true
        },
        { 
            title: '智能家居新趋势', 
            summary: '全屋智能解决方案', 
            image: $r("app.media.big30"),
            images: [$r("app.media.big30"), $r("app.media.big29"), $r("app.media.big28")],
            type: 'multi',
            isCollected: false
        },
        { 
            title: '移动办公新时代', 
            summary: '超级终端带来高效体验', 
            image: $r("app.media.big27"),
            type: 'normal',
            isCollected: false
        }
    ]
    
    private refreshData() {
        this.refreshing = true
        // 模拟网络请求
        setTimeout(() => {
            this.refreshing = false
        }, 1000)
    }
    
    private loadMore() {
        this.loading = true
        // 模拟网络请求
        setTimeout(() => {
            this.loading = false
            this.hasMore = this.newsItems.length < 20
        }, 1000)
    }
    
    private loadNewsByCategory(category: string) {
        this.dataState = 'loading'
        // 模拟网络请求
        setTimeout(() => {
            this.dataState = 'success'
        }, 500)
    }
    
    private toggleCollect(item: NewsItemType) {
        const index = this.newsItems.indexOf(item)
        if (index !== -1) {
            this.newsItems[index].isCollected = !this.newsItems[index].isCollected
        }
    }
    
    private getFilteredNews(): NewsItemType[] {
        if (!this.searchText) {
            return this.newsItems
        }
        
        return this.newsItems.filter(item => 
            item.title.includes(this.searchText) || 
            item.summary.includes(this.searchText)
        )
    }

    build() {
        Refresh({ refreshing: this.refreshing, offset: 120, friction: 66 }) {
            Column() {
                // 搜索框
                TextInput({ placeholder: '搜索新闻' })
                    .width('100%')
                    .height(40)
                    .backgroundColor('#F5F5F5')
                    .borderRadius(20)
                    .padding({ left: 16, right: 16 })
                    .margin({ bottom: 12 })
                    .onChange((value: string) => {
                        this.searchText = value
                    })
                
                // 分类标签栏
                Scroll({ direction: ScrollDirection.Horizontal }) {
                    Row({ space: 16 }) {
                        ForEach(this.categories, (category: string) => {
                            Text(category)
                                .fontSize(16)
                                .fontColor(this.currentCategory === category ? '#FF5722' : '#333333')
                                .fontWeight(this.currentCategory === category ? FontWeight.Bold : FontWeight.Normal)
                                .onClick(() => {
                                    this.currentCategory = category
                                    this.loadNewsByCategory(category)
                                })
                        })
                    }
                    .padding({ left: 12, right: 12, top: 12, bottom: 12 })
                }
                .scrollBar(BarState.Off)
                .margin({ bottom: 12 })
                
                if (this.dataState === 'loading') {
                    // 显示加载中状态
                    LoadingProgress()
                        .width(32)
                        .height(32)
                        .color('#FF5722')
                        .margin(64)
                } else if (this.dataState === 'empty') {
                    // 显示空状态
                    Column() {
                        Image($r('app.media.empty'))
                            .width(120)
                            .height(120)
                        
                        Text('暂无新闻')
                            .fontSize(16)
                            .fontColor('#999999')
                            .margin({ top: 16 })
                    }
                    .width('100%')
                    .justifyContent(FlexAlign.Center)
                    .padding(64)
                } else if (this.dataState === 'error') {
                    // 显示错误状态
                    Column() {
                        Image($r('app.media.error'))
                            .width(120)
                            .height(120)
                        
                        Text('加载失败,请重试')
                            .fontSize(16)
                            .fontColor('#999999')
                            .margin({ top: 16 })
                        
                        Button('重新加载')
                            .margin({ top: 16 })
                            .onClick(() => {
                                this.loadNewsByCategory(this.currentCategory)
                            })
                    }
                    .width('100%')
                    .justifyContent(FlexAlign.Center)
                    .padding(64)
                } else {
                    // 显示新闻列表
                    GridRow({ columns: 1 }) {
                        ForEach(this.getFilteredNews(), (item: NewsItemType) => {
                            GridCol({ span: 1 }) {
                                if (item.type === 'normal') {
                                    // 左图右文布局
                                    Row() {
                                        Image(item.image)
                                            .width(100)
                                            .height(80)
                                            .objectFit(ImageFit.Cover)
                                            .borderRadius(4)

                                        Column() {
                                            Text(item.title)
                                                .fontSize(16)
                                                .fontWeight(FontWeight.Bold)
                                                .margin({ left: 12 })

                                            Text(item.summary)
                                                .fontSize(14)
                                                .margin({ left: 12, top: 4 })
                                        }
                                        .alignItems(HorizontalAlign.Start)
                                        .layoutWeight(1)
                                        
                                        Image(item.isCollected ? $r('app.media.star_filled') : $r('app.media.star_outline'))
                                            .width(24)
                                            .height(24)
                                            .onClick((event: ClickEvent) => {
                                                event.stopPropagation()
                                                this.toggleCollect(item)
                                            })
                                    }
                                    .padding(12)
                                    .borderRadius(8)
                                    .margin({ bottom: 8 })
                                    .backgroundColor('#FFFFFF')
                                    .onClick(() => {
                                        console.info(`点击新闻: ${item.title}`)
                                    })
                                } else if (item.type === 'large') {
                                    // 上图下文布局
                                    Column() {
                                        Image(item.image)
                                            .width('100%')
                                            .height(160)
                                            .objectFit(ImageFit.Cover)
                                            .borderRadius(4)

                                        Row() {
                                            Text(item.title)
                                                .fontSize(16)
                                                .fontWeight(FontWeight.Bold)
                                                .margin({ top: 8 })
                                                .layoutWeight(1)
                                            
                                            Image(item.isCollected ? $r('app.media.star_filled') : $r('app.media.star_outline'))
                                                .width(24)
                                                .height(24)
                                                .margin({ top: 8 })
                                                .onClick((event: ClickEvent) => {
                                                    event.stopPropagation()
                                                    this.toggleCollect(item)
                                                })
                                        }

                                        Text(item.summary)
                                            .fontSize(14)
                                            .margin({ top: 4 })
                                            .width('100%')
                                            .textAlign(TextAlign.Start)
                                    }
                                    .padding(12)
                                    .borderRadius(8)
                                    .margin({ bottom: 8 })
                                    .backgroundColor('#FFFFFF')
                                    .onClick(() => {
                                        console.info(`点击新闻: ${item.title}`)
                                    })
                                } else if (item.type === 'multi') {
                                    // 三图布局
                                    Column() {
                                        Row() {
                                            Text(item.title)
                                                .fontSize(16)
                                                .fontWeight(FontWeight.Bold)
                                                .layoutWeight(1)
                                            
                                            Image(item.isCollected ? $r('app.media.star_filled') : $r('app.media.star_outline'))
                                                .width(24)
                                                .height(24)
                                                .onClick((event: ClickEvent) => {
                                                    event.stopPropagation()
                                                    this.toggleCollect(item)
                                                })
                                        }

                                        Row({ space: 8 }) {
                                            ForEach(item.images, (image: ResourceStr) => {
                                                Image(image)
                                                    .width('0%')
                                                    .aspectRatio(1)
                                                    .objectFit(ImageFit.Cover)
                                                    .borderRadius(4)
                                                    .layoutWeight(1)
                                            })
                                        }
                                        .margin({ top: 8 })

                                        Text(item.summary)
                                            .fontSize(14)
                                            .margin({ top: 8 })
                                            .width('100%')
                                            .textAlign(TextAlign.Start)
                                    }
                                    .padding(12)
                                    .borderRadius(8)
                                    .margin({ bottom: 8 })
                                    .backgroundColor('#FFFFFF')
                                    .onClick(() => {
                                        console.info(`点击新闻: ${item.title}`)
                                    })
                                }
                            }
                        })
                    }
                    
                    // 加载更多
                    if (this.loading) {
                        Row() {
                            LoadingProgress()
                                .width(24)
                                .height(24)
                                .color('#999999')
                            
                            Text('加载中...')
                                .fontSize(14)
                                .fontColor('#999999')
                                .margin({ left: 8 })
                        }
                        .width('100%')
                        .justifyContent(FlexAlign.Center)
                        .padding(16)
                    } else if (this.hasMore) {
                        Text('点击加载更多')
                            .fontSize(14)
                            .fontColor('#666666')
                            .width('100%')
                            .textAlign(TextAlign.Center)
                            .padding(16)
                            .onClick(() => {
                                this.loadMore()
                            })
                    } else {
                        Text('没有更多内容了')
                            .fontSize(14)
                            .fontColor('#999999')
                            .width('100%')
                            .textAlign(TextAlign.Center)
                            .padding(16)
                    }
                }
            }
            .width('100%')
            .padding(12)
        }
        .onRefreshing(() => {
            this.refreshData()
        })
    }
}

interface NewsItemType {
    title: string;
    summary: string;
    image: ResourceStr;
    images?: ResourceStr[];
    type: 'normal' | 'large' | 'multi';
    isCollected: boolean;
}

6. 总结

本教程深入探讨了如何优化HarmonyOS NEXT的新闻资讯列表布局,并添加了丰富的交互功能。通过实现分类标签栏、多种布局变体、下拉刷新、加载更多、新闻收藏等功能,我们打造了一个功能完善、用户体验良好的新闻列表页面。这些技术可以应用于实际的新闻、资讯类应用开发中,提升应用的用户体验和视觉效果。

收藏00

登录 后评论。没有帐号? 注册 一个。