167.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局进阶篇

2025-06-30 22:45:24
104次阅读
0个评论

[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局进阶篇

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

效果演示

image.png

1. 引言

在上一篇教程中,我们介绍了HarmonyOS NEXT中可滚动网格布局的基础知识和实现方法。本篇教程将深入探讨可滚动网格布局的进阶技巧,包括多列网格布局、动态列模板、高级滚动控制、自定义网格项样式等内容,帮助开发者构建更加灵活、美观的网格界面。

2. 多列网格布局

2.1 列模板设置

在基础教程中,我们使用了单列布局(columnsTemplate('1fr'))。在实际应用中,多列布局更为常见,特别是在平板等大屏设备上。下面介绍如何实现多列网格布局:

Grid(this.scroller) {
    // 网格内容
}
.columnsTemplate('1fr 1fr') // 双列布局
.columnsGap(16) // 列间距
.rowsGap(16) // 行间距

通过设置columnsTemplate为'1fr 1fr',我们创建了一个双列布局,每列占据可用空间的一份。同时,使用columnsGap设置列间距为16。

2.2 多列布局的响应式调整

为了适应不同屏幕尺寸,我们可以根据屏幕宽度动态调整列数:

@State gridColumns: string = '1fr'

aboutToAppear() {
    // 获取屏幕宽度
    const screenWidth = px2vp(window.getWindowWidth())
    
    // 根据屏幕宽度设置列数
    if (screenWidth >= 840) {
        this.gridColumns = '1fr 1fr 1fr' // 大屏设备,三列布局
    } else if (screenWidth >= 520) {
        this.gridColumns = '1fr 1fr' // 中等屏幕,双列布局
    } else {
        this.gridColumns = '1fr' // 小屏设备,单列布局
    }
}

// 在Grid中使用动态列模板
Grid(this.scroller) {
    // 网格内容
}
.columnsTemplate(this.gridColumns)

这样,当应用在不同尺寸的设备上运行时,网格布局会自动调整列数,提供最佳的显示效果。

2.3 列宽比例设置

除了等分列宽,我们还可以设置不同的列宽比例:

// 第一列占1份,第二列占2份
.columnsTemplate('1fr 2fr')

// 固定宽度与弹性宽度混合
.columnsTemplate('200px 1fr')

// 多列不等宽
.columnsTemplate('1fr 1.5fr 1fr')

通过灵活设置列模板,可以创建出各种复杂的网格布局效果。

3. 高级滚动控制

3.1 滚动事件与回调

除了基础教程中介绍的onScrollIndex事件外,Grid还支持其他滚动相关事件:

Grid(this.scroller) {
    // 网格内容
}
// 滚动开始事件
.onScrollBegin(() => {
    console.log('开始滚动')
})
// 滚动停止事件
.onScrollStop(() => {
    console.log('停止滚动')
})
// 滚动边缘事件
.onReachStart(() => {
    console.log('到达顶部')
})
.onReachEnd(() => {
    console.log('到达底部')
    // 可以在这里加载更多数据
    this.loadMoreApps()
})

这些事件可以帮助我们实现更加精细的滚动控制,如滚动到底部加载更多数据、滚动时显示/隐藏UI元素等。

3.2 编程式滚动控制

使用Scroller控制器,我们可以实现编程式滚动控制:

// 滚动到指定位置
scrollToPosition() {
    this.scroller.scrollTo({ xOffset: 0, yOffset: 200 })
}

// 滚动到指定索引的网格项
scrollToItem(index: number) {
    this.scroller.scrollToIndex(index)
}

// 滚动到顶部/底部
scrollToTop() {
    this.scroller.scrollEdge(Edge.Top)
}

scrollToBottom() {
    this.scroller.scrollEdge(Edge.Bottom)
}

// 按页滚动
scrollNextPage() {
    this.scroller.scrollPage({ next: true })
}

scrollPrevPage() {
    this.scroller.scrollPage({ next: false })
}

这些方法可以在特定场景下使用,如点击按钮滚动到顶部、切换分类时滚动到特定位置等。

3.3 滚动动画与效果

为了提升用户体验,我们可以为滚动添加动画效果:

// 带动画的滚动
scrollWithAnimation() {
    this.scroller.scrollTo({
        xOffset: 0,
        yOffset: 500,
        animation: {
            duration: 300, // 动画持续时间,单位毫秒
            curve: Curve.EaseOut // 动画曲线
        }
    })
}

通过设置animation参数,可以使滚动过程更加平滑自然,提升用户体验。

4. 自定义网格项样式

4.1 网格项布局与样式

在基础教程中,我们为每个GridItem创建了基本的布局和样式。下面介绍一些进阶的网格项样式技巧:

GridItem() {
    Column() {
        // 网格项内容
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .shadow({
        radius: 8,
        color: 'rgba(0, 0, 0, 0.1)',
        offsetX: 0,
        offsetY: 2
    })
    // 添加渐变背景
    .backgroundImage({
        gradient: {
            angle: 90,
            colors: [['#FFFFFF', 0.0], ['#F8F8F8', 1.0]]
        }
    })
    // 添加边框
    .border({
        width: 1,
        color: '#E0E0E0',
        style: BorderStyle.Solid
    })
    // 添加过渡动画
    .transition({
        type: TransitionType.All,
        opacity: 0.2
    })
}

这些样式设置可以使网格项更加美观,提升整体视觉效果。

4.2 网格项交互效果

为了提升用户体验,我们可以为网格项添加交互效果:

@State pressedItemId: number = -1

GridItem() {
    Column() {
        // 网格项内容
    }
    .scale(this.pressedItemId === app.id ? 0.95 : 1.0) // 按下时缩小
    .opacity(this.pressedItemId === app.id ? 0.8 : 1.0) // 按下时降低透明度
}
.gesture(
    LongPressGesture()
        .onAction(() => {
            // 长按操作
            this.showAppOptions(app.id)
        })
)
.onTouch((event: TouchEvent) => {
    if (event.type === TouchType.Down) {
        this.pressedItemId = app.id
    } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
        this.pressedItemId = -1
    }
})

通过监听触摸事件和手势,我们可以实现按下反馈、长按菜单等交互效果,提升用户体验。

4.3 网格项动画效果

为了使网格布局更加生动,我们可以为网格项添加动画效果:

@State animatedItems: number[] = []

onPageShow() {
    // 页面显示时,逐个显示网格项
    this.animatedItems = []
    for (let i = 0; i < this.featuredApps.length; i++) {
        setTimeout(() => {
            this.animatedItems.push(this.featuredApps[i].id)
        }, i * 100) // 每隔100毫秒显示一个
    }
}

GridItem() {
    Column() {
        // 网格项内容
    }
    .opacity(this.animatedItems.includes(app.id) ? 1.0 : 0.0)
    .translate({
        x: this.animatedItems.includes(app.id) ? 0 : 50,
        y: 0
    })
    .transition({
        type: TransitionType.All,
        opacity: 0.3,
        translate: 0.3
    })
}

这段代码实现了网格项的逐个淡入动画效果,使页面加载过程更加生动。

5. 高级网格布局技巧

5.1 网格项跨行跨列

Grid组件支持网格项跨行跨列,可以创建更加复杂的布局效果:

Grid() {
    // 占据2行2列的大网格项
    GridItem() {
        // 内容
    }
    .rowStart(0)
    .rowEnd(2)
    .columnStart(0)
    .columnEnd(2)
    
    // 普通网格项
    GridItem() {
        // 内容
    }
    
    // 跨2行的网格项
    GridItem() {
        // 内容
    }
    .rowStart(0)
    .rowEnd(2)
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')

通过设置rowStart、rowEnd、columnStart、columnEnd属性,可以控制网格项的跨行跨列,创建出更加丰富的布局效果。

5.2 网格区域命名

Grid组件支持网格区域命名,可以更加直观地定义复杂布局:

Grid() {
    GridItem() {
        // 头部内容
    }
    .gridArea('header')
    
    GridItem() {
        // 主要内容
    }
    .gridArea('main')
    
    GridItem() {
        // 侧边栏内容
    }
    .gridArea('sidebar')
    
    GridItem() {
        // 底部内容
    }
    .gridArea('footer')
}
.areasTemplate([
    ['header', 'header'],
    ['sidebar', 'main'],
    ['footer', 'footer']
])
.columnsTemplate('1fr 2fr')
.rowsTemplate('auto 1fr auto')

通过areasTemplate定义网格区域,并使用gridArea将网格项放置到指定区域,可以更加灵活地控制布局。

5.3 网格自适应布局

结合媒体查询,我们可以实现更加复杂的自适应网格布局:

@StorageLink('windowWidth') windowWidth: number = 0
@State gridLayout: GridLayoutConfig = { columns: '1fr', areas: [] }

aboutToAppear() {
    this.updateGridLayout()
}

@Watch('windowWidth')
updateGridLayout() {
    if (this.windowWidth >= 840) {
        // 大屏布局
        this.gridLayout = {
            columns: '1fr 1fr 1fr',
            areas: [
                ['featured', 'featured', 'sidebar'],
                ['content', 'content', 'sidebar']
            ]
        }
    } else if (this.windowWidth >= 520) {
        // 中屏布局
        this.gridLayout = {
            columns: '1fr 1fr',
            areas: [
                ['featured', 'featured'],
                ['content', 'sidebar']
            ]
        }
    } else {
        // 小屏布局
        this.gridLayout = {
            columns: '1fr',
            areas: [
                ['featured'],
                ['content'],
                ['sidebar']
            ]
        }
    }
}

// 在Grid中使用动态布局配置
Grid() {
    // 网格内容
}
.columnsTemplate(this.gridLayout.columns)
.areasTemplate(this.gridLayout.areas)

这段代码根据窗口宽度动态调整网格布局,包括列数和区域分布,实现了真正的响应式布局。

6. 数据管理与加载

6.1 分页加载

对于大量数据,分页加载是一种常见的优化策略:

@State featuredApps: FeaturedApp[] = []
@State loading: boolean = false
@State hasMore: boolean = true
@State currentPage: number = 1
@State pageSize: number = 10

aboutToAppear() {
    this.loadApps()
}

async loadApps() {
    if (this.loading || !this.hasMore) return
    
    this.loading = true
    
    try {
        // 模拟网络请求
        await new Promise(resolve => setTimeout(resolve, 1000))
        
        // 模拟获取数据
        const newApps = this.getAppsData(this.currentPage, this.pageSize)
        
        // 添加到现有数据
        this.featuredApps = [...this.featuredApps, ...newApps]
        
        // 更新分页信息
        this.currentPage++
        this.hasMore = newApps.length === this.pageSize
    } finally {
        this.loading = false
    }
}

// 在Grid中添加加载更多逻辑
Grid(this.scroller) {
    // 应用列表
    ForEach(this.featuredApps, (app:FeaturedApp) => {
        GridItem() {
            // 网格项内容
        }
    })
    
    // 加载更多指示器
    if (this.loading) {
        GridItem() {
            LoadingProgress()
                .width(24)
                .height(24)
        }
        .justifyContent(FlexAlign.Center)
    }
}
.onReachEnd(() => {
    this.loadApps() // 滚动到底部时加载更多
})

这段代码实现了滚动到底部加载更多数据的功能,适用于大量数据的展示场景。

6.2 下拉刷新

结合Refresh组件,我们可以实现下拉刷新功能:

@State refreshing: boolean = false

refreshData() {
    this.refreshing = true
    
    // 模拟刷新数据
    setTimeout(() => {
        this.currentPage = 1
        this.featuredApps = []
        this.hasMore = true
        this.loadApps()
        this.refreshing = false
    }, 1000)
}

build() {
    Column() {
        // 其他UI元素
        
        Refresh({
            refreshing: $$this.refreshing,
            onRefresh: () => this.refreshData()
        }) {
            Grid(this.scroller) {
                // 网格内容
            }
            // Grid属性设置
        }
    }
}

通过Refresh组件包装Grid,可以实现下拉刷新功能,提升用户体验。

7. 高级交互与动效

7.1 网格项拖拽排序

通过结合手势和动画,我们可以实现网格项的拖拽排序功能:

@State draggingItemId: number = -1
@State itemPositions: Map<number, Position> = new Map()

GridItem() {
    Column() {
        // 网格项内容
    }
    .position({
        x: this.itemPositions.get(app.id)?.x || 0,
        y: this.itemPositions.get(app.id)?.y || 0
    })
    .zIndex(this.draggingItemId === app.id ? 1 : 0)
    .opacity(this.draggingItemId === app.id ? 0.8 : 1.0)
    .animation({
        duration: this.draggingItemId === app.id ? 0 : 300,
        curve: Curve.Ease
    })
}
.gesture(
    PanGesture()
        .onActionStart(() => {
            this.draggingItemId = app.id
        })
        .onActionUpdate((event: GestureEvent) => {
            if (this.draggingItemId === app.id) {
                // 更新拖拽项位置
                const position = this.itemPositions.get(app.id) || { x: 0, y: 0 }
                this.itemPositions.set(app.id, {
                    x: position.x + event.offsetX,
                    y: position.y + event.offsetY
                })
                
                // 检测与其他项的交换
                this.checkItemSwap(app.id, event)
            }
        })
        .onActionEnd(() => {
            if (this.draggingItemId === app.id) {
                // 重置位置并完成排序
                this.itemPositions.set(app.id, { x: 0, y: 0 })
                this.draggingItemId = -1
            }
        })
)

这段代码实现了网格项的拖拽排序功能,用户可以通过拖拽调整网格项的顺序。

7.2 网格项展开/折叠

通过状态管理和动画,我们可以实现网格项的展开/折叠效果:

@State expandedItemId: number = -1

GridItem() {
    Column() {
        // 基本信息
        Row() {
            // 应用图标和基本信息
        }
        .onClick(() => {
            this.expandedItemId = this.expandedItemId === app.id ? -1 : app.id
        })
        
        // 详细信息(展开时显示)
        if (this.expandedItemId === app.id) {
            Column() {
                // 详细信息内容
            }
            .height(this.expandedItemId === app.id ? 200 : 0)
            .opacity(this.expandedItemId === app.id ? 1.0 : 0.0)
            .transition({
                type: TransitionType.All,
                opacity: 0.3,
                height: 0.3
            })
        }
    }
}

这段代码实现了网格项的展开/折叠效果,点击网格项时可以显示更多详细信息。

8. 总结

在下一篇教程中,我们将探讨更多高级主题,包括自定义网格布局算法、复杂交互模式、性能优化策略等,敬请期待!

收藏00

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