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

效果演示

1. 概述

分页控件是数据展示类应用中不可或缺的导航元素,它允许用户在大量数据中进行有序浏览。本教程将详细讲解如何使用HarmonyOS NEXT的Row组件创建一个功能完善的分页控件,实现页码显示与前后翻页按钮的完美结合。

分页控件在各类应用场景中广泛应用,如电子商城的商品列表、新闻应用的文章列表、图库应用的图片浏览等。通过合理的设计和交互,可以提升用户的浏览体验和数据访问效率。

2. 分页控件的设计原则

在设计分页控件时,需要遵循以下设计原则:

  1. 简洁明了:控件布局应简洁明了,用户能够一目了然地理解其功能。
  2. 状态清晰:当前页码和可用操作应有明确的视觉区分。
  3. 反馈及时:用户操作后应提供及时的视觉反馈。
  4. 边界处理:在首页和末页时,相应的翻页按钮应有适当的状态变化。
  5. 适应性强:控件应能适应不同的屏幕尺寸和方向。

3. 案例分析:分页控件

本案例展示了如何创建一个包含上一页按钮、页码显示和下一页按钮的分页控件。

3.1 完整代码

@Component
export struct Pagination {
    @State currentPage: number = 1
    @State totalPages: number = 10

    build() {
        Row() {
            Button('上一页',{stateEffect: this.currentPage <= 1 ? false : true })
                .opacity(this.currentPage <= 1 ? 0.4:1)
                .backgroundColor(0x317aff)
                .onClick(() => {
                    if (this.currentPage > 1) this.currentPage--
                })

            Text(`第 ${this.currentPage} 页 / 共 ${this.totalPages} 页`)
                .margin({ left: 12, right: 12 })

            Button('下一页',{stateEffect:this.currentPage === this.totalPages ? false : true })
            .opacity(this.currentPage === this.totalPages ? 0.4:1)
                .backgroundColor(0x317aff)
                .onClick(() => {
                    if (this.currentPage < this.totalPages) this.currentPage++
                })
        }
        .height(40)
        .justifyContent(FlexAlign.Center) // 居中对齐
        .margin(24)
    }
}

3.2 代码详解

3.2.1 组件声明与状态定义

@Component
export struct Pagination {
    @State currentPage: number = 1
    @State totalPages: number = 10

这部分代码声明了一个名为Pagination的自定义组件,并定义了两个状态变量:

状态变量类型说明默认值
currentPagenumber当前页码1
totalPagesnumber总页数10

使用@State装饰器定义的状态变量会在值变化时自动触发UI更新,这对于实现分页控件的交互非常重要。

3.2.2 Row容器设置

Row() {
    // 子组件
}
.height(40)
.justifyContent(FlexAlign.Center) // 居中对齐
.margin(24)

这部分代码创建了一个Row容器,作为分页控件的根容器。Row容器的属性设置如下:

属性说明
height40设置容器高度为40vp,提供足够的点击区域
justifyContentFlexAlign.Center设置子组件在主轴(水平方向)上居中对齐
margin24设置外边距为24vp,与其他元素保持适当距离

这些设置使分页控件具有合适的高度和间距,并且子组件在水平方向上居中排列,创造平衡的视觉效果。

3.2.3 上一页按钮设置

Button('上一页',{stateEffect: this.currentPage <= 1 ? false : true })
    .opacity(this.currentPage <= 1 ? 0.4:1)
    .backgroundColor(0x317aff)
    .onClick(() => {
        if (this.currentPage > 1) this.currentPage--
    })

这部分代码创建了一个"上一页"按钮,并设置了其样式和交互行为:

属性/方法值/实现说明
文本'上一页'按钮显示的文本
stateEffectthis.currentPage <= 1 ? false : true当前页为第一页时禁用按钮状态效果
opacitythis.currentPage <= 1 ? 0.4 : 1当前页为第一页时降低按钮透明度,视觉上表示不可用
backgroundColor0x317aff设置按钮背景色为蓝色
onClick回调函数点击时将当前页码减1,但不小于1

这些设置使"上一页"按钮在当前页为第一页时呈现禁用状态(透明度降低且无状态效果),提示用户已经到达首页,无法继续向前翻页。

3.2.4 页码显示设置

Text(`第 ${this.currentPage} 页 / 共 ${this.totalPages} 页`)
    .margin({ left: 12, right: 12 })

这部分代码创建了一个Text组件,用于显示当前页码和总页数:

属性说明
文本模板字符串显示"第 X 页 / 共 Y 页"格式的文本,X为当前页码,Y为总页数
margin{ left: 12, right: 12 }设置左右外边距为12vp,与两侧按钮保持适当距离

页码显示采用模板字符串动态生成文本内容,当currentPagetotalPages状态变量变化时,文本内容会自动更新。

3.2.5 下一页按钮设置

Button('下一页',{stateEffect:this.currentPage === this.totalPages ? false : true })
.opacity(this.currentPage === this.totalPages ? 0.4:1)
    .backgroundColor(0x317aff)
    .onClick(() => {
        if (this.currentPage < this.totalPages) this.currentPage++
    })

这部分代码创建了一个"下一页"按钮,并设置了其样式和交互行为:

属性/方法值/实现说明
文本'下一页'按钮显示的文本
stateEffectthis.currentPage === this.totalPages ? false : true当前页为最后一页时禁用按钮状态效果
opacitythis.currentPage === this.totalPages ? 0.4 : 1当前页为最后一页时降低按钮透明度,视觉上表示不可用
backgroundColor0x317aff设置按钮背景色为蓝色
onClick回调函数点击时将当前页码加1,但不大于总页数

这些设置使"下一页"按钮在当前页为最后一页时呈现禁用状态(透明度降低且无状态效果),提示用户已经到达末页,无法继续向后翻页。

4. 分页控件的实现原理

分页控件的实现基于状态管理和条件渲染,通过状态变量控制UI的变化,实现交互效果。

4.1 状态管理

本案例使用两个状态变量管理分页控件的状态:

  1. currentPage:记录当前页码,初始值为1。
  2. totalPages:记录总页数,初始值为10。

这两个状态变量使用@State装饰器定义,确保它们的变化会自动触发UI更新。

4.2 条件渲染

通过条件表达式,根据当前状态动态调整UI元素的样式和行为:

  1. 上一页按钮

    • currentPage <= 1时,按钮透明度降低且禁用状态效果,表示不可用。
    • 点击时,只有当currentPage > 1时才减少页码。
  2. 下一页按钮

    • currentPage === totalPages时,按钮透明度降低且禁用状态效果,表示不可用。
    • 点击时,只有当currentPage < totalPages时才增加页码。

4.3 事件处理

通过onClick事件处理函数,响应用户的点击操作:

  1. 上一页按钮:点击时检查当前页码是否大于1,如果是则将currentPage减1。
  2. 下一页按钮:点击时检查当前页码是否小于总页数,如果是则将currentPage加1。

这种实现方式确保了分页控件的正确行为,防止页码超出有效范围。

5. 分页控件的样式优化

为了提升分页控件的视觉效果和用户体验,我们可以进行以下样式优化:

5.1 按钮样式优化

Button('上一页', { stateEffect: this.currentPage <= 1 ? false : true })
    .opacity(this.currentPage <= 1 ? 0.4 : 1)
    .backgroundColor(0x317aff)
    .fontColor(Color.White)
    .fontSize(14)
    .height(36)
    .borderRadius(18)
    .padding({ left: 16, right: 16 })
    .onClick(() => {
        if (this.currentPage > 1) this.currentPage--
    })

这些样式设置使按钮更加美观:

  1. 字体颜色:设置为白色,与蓝色背景形成鲜明对比,提高可读性。
  2. 字体大小:设置为14fp,适合按钮文本的显示。
  3. 高度:设置为36vp,提供合适的点击区域。
  4. 圆角:设置为18vp(高度的一半),使按钮呈现为胶囊形状。
  5. 内边距:设置左右内边距,使文本与按钮边缘保持适当距离。

5.2 页码文本样式优化

Text(`第 ${this.currentPage} 页 / 共 ${this.totalPages} 页`)
    .fontSize(14)
    .fontColor('#333333')
    .fontWeight(FontWeight.Medium)
    .margin({ left: 12, right: 12 })

这些样式设置使页码文本更加清晰:

  1. 字体大小:设置为14fp,与按钮文本保持一致。
  2. 字体颜色:设置为深灰色,提高可读性。
  3. 字体粗细:设置为中等粗细,使当前页码更加突出。

5.3 容器样式优化

Row() {
    // 子组件
}
.height(40)
.justifyContent(FlexAlign.Center)
.backgroundColor('#F5F5F5')
.borderRadius(20)
.padding({ left: 8, right: 8 })
.margin(24)

这些样式设置使分页控件整体更加美观:

  1. 背景色:设置为浅灰色,使控件在页面中更加突出。
  2. 圆角:设置为20vp,使控件呈现为胶囊形状。
  3. 内边距:设置左右内边距,使子组件与容器边缘保持适当距离。

6. 分页控件的交互优化

为了提升用户体验,可以为分页控件添加更多交互效果:

6.1 按钮过渡动画

Button('上一页', { stateEffect: this.currentPage <= 1 ? false : true })
    .opacity(this.currentPage <= 1 ? 0.4 : 1)
    .backgroundColor(0x317aff)
    // 其他样式...
    .transition({ type: TransitionType.All, opacity: 0.2 }) // 添加过渡动画
    .onClick(() => {
        if (this.currentPage > 1) this.currentPage--
    })

添加过渡动画使按钮状态变化更加平滑,提升视觉体验。

6.2 页码变化动画

@Component
export struct Pagination {
    @State currentPage: number = 1
    @State totalPages: number = 10
    @State isAnimating: boolean = false
    
    build() {
        Row() {
            // 上一页按钮...
            
            Stack() {
                Text(`第 ${this.currentPage} 页 / 共 ${this.totalPages} 页`)
                    .fontSize(14)
                    .fontColor('#333333')
                    .fontWeight(FontWeight.Medium)
                    .opacity(this.isAnimating ? 0 : 1) // 动画期间隐藏
                    .transition({ type: TransitionType.All, opacity: 0.2 })
            }
            .width(120)
            .height(36)
            .alignContent(Alignment.Center)
            .margin({ left: 12, right: 12 })
            
            // 下一页按钮...
        }
        // 容器样式...
    }
    
    private animatePageChange() {
        this.isAnimating = true
        setTimeout(() => {
            this.isAnimating = false
        }, 200)
    }
    
    private prevPage() {
        if (this.currentPage > 1) {
            this.animatePageChange()
            this.currentPage--
        }
    }
    
    private nextPage() {
        if (this.currentPage < this.totalPages) {
            this.animatePageChange()
            this.currentPage++
        }
    }
}

这个优化版本添加了页码变化动画,当页码变化时,文本会先淡出再淡入,提供更好的视觉反馈。

7. 分页控件的扩展功能

基于本案例的基本结构,我们可以扩展更多功能:

7.1 页码跳转

@Component
export struct PaginationWithJump {
    @State currentPage: number = 1
    @State totalPages: number = 10
    @State inputPage: string = '1'
    
    build() {
        Row() {
            // 上一页按钮...
            
            // 页码显示...
            
            // 下一页按钮...
            
            // 页码跳转
            TextInput({ text: this.inputPage })
                .width(50)
                .height(36)
                .backgroundColor(Color.White)
                .borderRadius(4)
                .margin({ left: 12 })
                .type(InputType.Number)
                .onChange((value) => {
                    this.inputPage = value
                })
                
            Button('跳转')
                .height(36)
                .backgroundColor(0x317aff)
                .fontColor(Color.White)
                .fontSize(14)
                .borderRadius(4)
                .margin({ left: 8 })
                .onClick(() => {
                    const page = parseInt(this.inputPage)
                    if (!isNaN(page) && page >= 1 && page <= this.totalPages) {
                        this.currentPage = page
                    }
                })
        }
        // 容器样式...
    }
}

这个扩展功能添加了页码跳转功能,用户可以输入页码并点击跳转按钮直接跳转到指定页面。

7.2 页码选择器

@Component
export struct PaginationWithSelector {
    @State currentPage: number = 1
    @State totalPages: number = 10
    @State showSelector: boolean = false
    
    build() {
        Column() {
            Row() {
                // 上一页按钮...
                
                Text(`第 ${this.currentPage} 页 / 共 ${this.totalPages} 页`)
                    .fontSize(14)
                    .fontColor('#333333')
                    .fontWeight(FontWeight.Medium)
                    .margin({ left: 12, right: 12 })
                    .onClick(() => {
                        this.showSelector = !this.showSelector
                    })
                
                // 下一页按钮...
            }
            .height(40)
            .justifyContent(FlexAlign.Center)
            .margin(24)
            
            if (this.showSelector) {
                // 页码选择器
                Grid() {
                    ForEach(Array.from({ length: this.totalPages }, (_, i) => i + 1), (page) => {
                        GridItem() {
                            Text(`${page}`)
                                .width('100%')
                                .height('100%')
                                .textAlign(TextAlign.Center)
                                .backgroundColor(page === this.currentPage ? 0x317aff : Color.White)
                                .fontColor(page === this.currentPage ? Color.White : '#333333')
                                .borderRadius(4)
                                .onClick(() => {
                                    this.currentPage = page
                                    this.showSelector = false
                                })
                        }
                    })
                }
                .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
                .rowsGap(8)
                .columnsGap(8)
                .padding(16)
                .backgroundColor(Color.White)
                .borderRadius(8)
                .shadow({ radius: 4, color: '#F0F0F0' })
                .width('80%')
                .margin({ top: -16 })
            }
        }
    }
}

这个扩展功能添加了页码选择器,用户可以点击页码文本显示页码网格,然后直接选择要跳转的页码。

7.3 页码范围显示

@Component
export struct PaginationWithRange {
    @State currentPage: number = 1
    @State totalPages: number = 10
    private maxVisiblePages: number = 5
    
    build() {
        Row() {
            // 上一页按钮...
            
            // 页码范围
            Row() {
                // 显示第一页
                if (this.getStartPage() > 1) {
                    Text('1')
                        .fontSize(14)
                        .fontColor('#333333')
                        .width(36)
                        .height(36)
                        .textAlign(TextAlign.Center)
                        .borderRadius(18)
                        .backgroundColor(1 === this.currentPage ? 0x317aff : Color.Transparent)
                        .fontColor(1 === this.currentPage ? Color.White : '#333333')
                        .onClick(() => {
                            this.currentPage = 1
                        })
                    
                    // 显示省略号
                    if (this.getStartPage() > 2) {
                        Text('...')
                            .fontSize(14)
                            .fontColor('#333333')
                            .width(36)
                            .height(36)
                            .textAlign(TextAlign.Center)
                    }
                }
                
                // 显示页码范围
                ForEach(this.getPageRange(), (page) => {
                    Text(`${page}`)
                        .fontSize(14)
                        .width(36)
                        .height(36)
                        .textAlign(TextAlign.Center)
                        .borderRadius(18)
                        .backgroundColor(page === this.currentPage ? 0x317aff : Color.Transparent)
                        .fontColor(page === this.currentPage ? Color.White : '#333333')
                        .onClick(() => {
                            this.currentPage = page
                        })
                })
                
                // 显示最后一页
                if (this.getEndPage() < this.totalPages) {
                    // 显示省略号
                    if (this.getEndPage() < this.totalPages - 1) {
                        Text('...')
                            .fontSize(14)
                            .fontColor('#333333')
                            .width(36)
                            .height(36)
                            .textAlign(TextAlign.Center)
                    }
                    
                    Text(`${this.totalPages}`)
                        .fontSize(14)
                        .width(36)
                        .height(36)
                        .textAlign(TextAlign.Center)
                        .borderRadius(18)
                        .backgroundColor(this.totalPages === this.currentPage ? 0x317aff : Color.Transparent)
                        .fontColor(this.totalPages === this.currentPage ? Color.White : '#333333')
                        .onClick(() => {
                            this.currentPage = this.totalPages
                        })
                }
            }
            .margin({ left: 8, right: 8 })
            
            // 下一页按钮...
        }
        // 容器样式...
    }
    
    private getStartPage(): number {
        return Math.max(1, Math.min(this.currentPage - Math.floor(this.maxVisiblePages / 2), this.totalPages - this.maxVisiblePages + 1))
    }
    
    private getEndPage(): number {
        return Math.min(this.totalPages, this.getStartPage() + this.maxVisiblePages - 1)
    }
    
    private getPageRange(): number[] {
        const start = this.getStartPage()
        const end = this.getEndPage()
        return Array.from({ length: end - start + 1 }, (_, i) => start + i)
    }
}

这个扩展功能显示页码范围,而不是简单的当前页码和总页数,用户可以直接点击页码进行跳转。对于页码较多的情况,会显示省略号,保持界面简洁。

8. 分页控件组件的封装与复用

为了提高代码复用性,可以将分页控件封装为一个独立的组件:

@Component
export struct Pagination {
    // 可自定义的属性
    @Prop currentPage: number = 1
    @Prop totalPages: number = 10
    @Link pageIndex: number // 使用@Link装饰器,实现双向绑定
    onPageChange?: (page: number) => void // 页码变化回调
    buttonStyle?: ButtonStyle // 按钮样式
    textStyle?: TextStyle // 文本样式
    
    build() {
        Row() {
            Button('上一页', {
                stateEffect: this.pageIndex <= 1 ? false : true,
                type: this.buttonStyle?.type || ButtonType.Normal
            })
                .opacity(this.pageIndex <= 1 ? 0.4 : 1)
                .backgroundColor(this.buttonStyle?.backgroundColor || 0x317aff)
                .fontColor(this.buttonStyle?.fontColor || Color.White)
                .fontSize(this.buttonStyle?.fontSize || 14)
                .height(this.buttonStyle?.height || 36)
                .borderRadius(this.buttonStyle?.borderRadius || 18)
                .padding({ left: 16, right: 16 })
                .onClick(() => {
                    if (this.pageIndex > 1) {
                        this.pageIndex--
                        if (this.onPageChange) {
                            this.onPageChange(this.pageIndex)
                        }
                    }
                })
            
            Text(`第 ${this.pageIndex} 页 / 共 ${this.totalPages} 页`)
                .fontSize(this.textStyle?.fontSize || 14)
                .fontColor(this.textStyle?.fontColor || '#333333')
                .fontWeight(this.textStyle?.fontWeight || FontWeight.Medium)
                .margin({ left: 12, right: 12 })
            
            Button('下一页', {
                stateEffect: this.pageIndex === this.totalPages ? false : true,
                type: this.buttonStyle?.type || ButtonType.Normal
            })
                .opacity(this.pageIndex === this.totalPages ? 0.4 : 1)
                .backgroundColor(this.buttonStyle?.backgroundColor || 0x317aff)
                .fontColor(this.buttonStyle?.fontColor || Color.White)
                .fontSize(this.buttonStyle?.fontSize || 14)
                .height(this.buttonStyle?.height || 36)
                .borderRadius(this.buttonStyle?.borderRadius || 18)
                .padding({ left: 16, right: 16 })
                .onClick(() => {
                    if (this.pageIndex < this.totalPages) {
                        this.pageIndex++
                        if (this.onPageChange) {
                            this.onPageChange(this.pageIndex)
                        }
                    }
                })
        }
        .height(40)
        .justifyContent(FlexAlign.Center)
        .margin(24)
    }
}

// 按钮样式接口
interface ButtonStyle {
    type?: ButtonType
    backgroundColor?: Color | number | string
    fontColor?: Color | number | string
    fontSize?: number
    height?: number
    borderRadius?: number
}

// 文本样式接口
interface TextStyle {
    fontSize?: number
    fontColor?: Color | number | string
    fontWeight?: FontWeight
}

然后在应用中使用这个组件:

@Entry
@Component
struct PaginationDemo {
    @State currentPage: number = 1
    private totalPages: number = 10
    
    build() {
        Column({ space: 20 }) {
            // 默认样式
            Pagination({
                pageIndex: this.$currentPage,
                totalPages: this.totalPages,
                onPageChange: (page) => {
                    console.info(`页码变化:${page}`)
                }
            })
            
            // 自定义样式
            Pagination({
                pageIndex: this.$currentPage,
                totalPages: this.totalPages,
                buttonStyle: {
                    backgroundColor: 0xFF5722,
                    borderRadius: 4
                },
                textStyle: {
                    fontSize: 16,
                    fontColor: '#666666'
                }
            })
            
            // 显示当前数据
            Text(`当前显示:第 ${this.currentPage} 页数据`)
                .fontSize(16)
                .fontWeight(FontWeight.Medium)
                .margin({ top: 20 })
        }
        .width('100%')
        .height('100%')
        .backgroundColor('#F5F5F5')
        .padding({ top: 20 })
    }
}

通过这种方式,可以创建具有不同样式和行为的分页控件,提高代码复用性和开发效率。

9. 总结

本教程详细讲解了如何使用HarmonyOS NEXT的Row组件创建功能完善的分页控件,实现页码显示与前后翻页按钮的完美结合。


全栈若城
1 声望2 粉丝