[HarmonyOS NEXT 实战案例:旅行应用] 基础篇 - 水平分割布局打造旅行规划应用

2025-06-11 23:29:59
110次阅读
0个评论

[HarmonyOS NEXT 实战案例:旅行应用] 基础篇 - 水平分割布局打造旅行规划应用

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

效果演示

image.png

组件概述

在本案例中,我们将使用以下HarmonyOS NEXT组件:

组件名称 功能描述
RowSplit 水平分割布局容器,将界面分为左右两部分
Column 垂直布局容器,用于垂直排列子组件
Row 水平布局容器,用于水平排列子组件
Scroll 滚动容器,用于在有限空间内展示更多内容
Image 图片组件,用于显示景点图片和图标
Text 文本组件,用于显示行程信息和描述
Button 按钮组件,用于添加行程、导航和分享操作
ForEach 循环渲染组件,用于渲染行程列表和活动项

数据模型定义

在实现旅行规划应用之前,我们首先定义了两个接口来描述数据结构:

// 类型定义
interface TripDay {
    day: string
    date: string
    location: string
    description: string
    hotel: string
    restaurant: string
    image: Resource
    activities: Activity[]
}

interface Activity {
    time: string
    title: string
    icon: Resource
}

这两个接口定义了旅行规划应用所需的数据结构:

  • TripDay:表示一天的行程信息,包含日期、地点、描述、住宿、餐饮、图片和活动列表
  • Activity:表示一个具体的活动项,包含时间、标题和图标

代码实现

外层容器

我们使用RowSplit组件作为最外层容器,将界面分为左右两部分:

RowSplit() {
    // 左侧行程列表 (40%)
    Column() {
        // 行程列表内容
    }
    .width('40%')
    .backgroundColor('#f1f8e9')
    .padding(10)

    // 右侧详情 (60%)
    Column() {
        // 详情内容
    }
    .padding(15)
    .width('60%')
}
.height(600)
.width('100%')

RowSplit设置了600的高度和100%的宽度,确保整个界面有足够的显示空间。左侧的行程列表区域占总宽度的40%,背景色为浅绿色,设置了10的内边距。右侧的详情区域占总宽度的60%,设置了15的内边距。

左侧行程列表

左侧行程列表区域包含标题、行程列表和添加按钮:

Column() {
    Text('我的行程')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 15 })

    Scroll() {
        Column() {
            ForEach(this.tripDays, (item: TripDay, index: number) => {
                Column() {
                    Row() {
                        Text(item.day)
                            .fontSize(16)
                            .fontWeight(FontWeight.Bold)
                            .layoutWeight(1)

                        Text(item.date)
                            .fontSize(12)
                            .fontColor('#666666')
                    }

                    Text(item.location)
                        .fontSize(14)
                        .fontColor('#4CAF50')
                        .margin({ top: 2 })
                }
                .padding(5)
                .width('100%')
                .backgroundColor(index === this.selectedDay ? '#e8f5e9' : 'transparent')
                .borderRadius(8)
                .onClick(() => this.selectedDay = index)
            })
        }
    }
    .margin({ bottom: 10 })

    Button('添加新行程')
        .width('80%')
        .margin({ bottom: 20 })
}

在这个区域中:

  1. 顶部是一个标题文本"我的行程",设置了字体大小、粗细和边距
  2. 中间是一个Scroll滚动容器,包含一个Column,使用ForEach循环渲染行程列表
  3. 每个行程项包含天数、日期和地点信息,设置了点击事件处理函数,点击时更新selectedDay状态
  4. 底部是一个"添加新行程"按钮

右侧详情区域

右侧详情区域包含顶部操作栏、内容区和底部操作按钮:

Column() {
    // 顶部操作栏
    Row() {
        Text(this.tripDays[this.selectedDay].day + ' · ' + this.tripDays[this.selectedDay].location)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)

        Image(this.showMap ? $r('app.media.big15') : $r('app.media.big14'))
            .width(24)
            .height(24)
            .onClick(() => this.showMap = !this.showMap)
    }
    .padding({ bottom: 15 })

    // 内容区(条件渲染)
    if (this.showMap) {
        // 地图视图
        // ...
    } else {
        // 详情视图
        // ...
    }

    // 底部操作按钮
    Row() {
        Button('导航')
            .width(120)
            .height(40)

        Button('分享行程')
            .width(120)
            .height(40)
            .margin({ left: 10 })
    }
    .margin({ top: 15 })
    .justifyContent(FlexAlign.Center)
}

在这个区域中:

  1. 顶部操作栏显示当前选中的行程信息和一个切换按钮,用于切换地图视图和详情视图
  2. 内容区根据showMap状态条件渲染不同的内容
  3. 底部操作按钮包含"导航"和"分享行程"两个按钮

地图视图

showMaptrue时,显示地图视图:

Column() {
    Image($r('app.media.big13'))
        .width('100%')
        .height(300)
        .objectFit(ImageFit.Cover)
        .borderRadius(8)

    Text('地图位置')
        .fontSize(16)
        .margin({ top: 15 })
}
.height('100%')

地图视图包含一个地图图片和一个位置文本。

详情视图

showMapfalse时,显示详情视图:

Scroll() {
    Column() {
        // 景点图片
        Image(this.tripDays[this.selectedDay].image)
            .width('100%')
            .height(200)
            .objectFit(ImageFit.Cover)
            .borderRadius(8)
            .margin({ bottom: 15 })

        // 行程描述
        Text(this.tripDays[this.selectedDay].description)
            .fontSize(16)
            .lineHeight(24)
            .margin({ bottom: 20 })

        // 活动时间表
        Text('当日行程')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 10 })

        Column() {
            ForEach(this.tripDays[this.selectedDay].activities, (activity: Activity) => {
                Row() {
                    Image(activity.icon)
                        .width(20)
                        .height(20)
                        .margin({ right: 15 })

                    Text(activity.time)
                        .fontSize(14)
                        .width(60)

                    Text(activity.title)
                        .fontSize(16)
                }
                .padding(10)
                .margin({ bottom: 5 })
                .backgroundColor('#f5f5f5')
                .borderRadius(8)
            })
        }
        .margin({ bottom: 20 })

        // 住宿和餐饮
        Column() {
            // 住宿信息
            Row() {
                Image($r('app.media.01'))
                    .width(24)
                    .height(24)
                    .margin({ right: 10 })

                Column() {
                    Text('住宿')
                        .fontSize(12)
                        .fontColor('#666666')
                    Text(this.tripDays[this.selectedDay].hotel)
                        .fontSize(16)
                }
            }
            .padding(10)
            .margin({ bottom: 5 })
            .backgroundColor('#f5f5f5')
            .borderRadius(8)

            // 餐饮信息
            Row() {
                Image($r('app.media.02'))
                    .width(24)
                    .height(24)
                    .margin({ right: 10 })

                Column() {
                    Text('餐饮')
                        .fontSize(12)
                        .fontColor('#666666')
                    Text(this.tripDays[this.selectedDay].restaurant)
                        .fontSize(16)
                }
            }
            .padding(10)
            .backgroundColor('#f5f5f5')
            .borderRadius(8)
        }
    }
}

详情视图包含以下内容:

  1. 景点图片:显示当前选中行程的图片
  2. 行程描述:显示当前选中行程的描述文本
  3. 活动时间表:使用ForEach循环渲染当前选中行程的活动列表
  4. 住宿和餐饮信息:显示当前选中行程的住宿和餐饮信息

布局技巧

1. 使用RowSplit实现左右分栏

RowSplit组件是HarmonyOS NEXT中用于实现水平分割布局的容器组件,它可以将界面分为左右两部分,每部分可以设置不同的宽度比例。在本案例中,我们将左侧行程列表区域设置为总宽度的40%,右侧详情区域设置为总宽度的60%。

2. 使用Scroll处理溢出内容

在左侧行程列表和右侧详情视图中,我们都使用了Scroll组件来处理可能的内容溢出情况。这样,当内容超出可视区域时,用户可以通过滚动来查看所有内容。

3. 使用条件渲染切换视图

在右侧详情区域,我们使用条件渲染来切换地图视图和详情视图:

if (this.showMap) {
    // 地图视图
    // ...
} else {
    // 详情视图
    // ...
}

这样,用户可以通过点击顶部操作栏中的切换按钮来切换不同的视图。

4. 使用ForEach循环渲染列表

在左侧行程列表和右侧详情视图的活动时间表中,我们都使用了ForEach组件来循环渲染列表数据。这样,我们可以根据数据数组的长度动态生成列表项,而不需要手动编写重复的代码。

交互实现

1. 行程选择

在左侧行程列表中,我们为每个行程项添加了点击事件处理函数:

.onClick(() => this.selectedDay = index)

当用户点击一个行程项时,会更新selectedDay状态,从而触发界面更新,显示对应的行程详情。

2. 视图切换

在右侧详情区域的顶部操作栏中,我们为切换按钮添加了点击事件处理函数:

.onClick(() => this.showMap = !this.showMap)

当用户点击切换按钮时,会切换showMap状态,从而触发界面更新,切换地图视图和详情视图。

3. 状态管理

在本案例中,我们使用了@State装饰器来定义组件的状态变量:

@State selectedDay: number = 0
@State showMap: boolean = false
@State tripDays: TripDay[] = [ /* ... */ ]

这些状态变量的变化会触发界面的自动更新,实现响应式的用户界面。

小结

在本教程中,我们学习了如何使用HarmonyOS NEXT的RowSplit组件构建一个旅行规划应用。通过水平分割布局,我们将界面分为左侧行程列表和右侧详情区域,实现了行程选择、视图切换等交互功能。

收藏00

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