项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
在前两篇教程中,我们学习了如何使用HarmonyOS NEXT的ColumnSplit
组件构建课程学习平台的基本布局,以及如何添加交互功能和状态管理。本篇教程将进一步深入,讲解课程学习平台的高级布局技巧和自适应设计,使应用能够在不同尺寸的设备上提供一致且优质的用户体验。
自适应布局是指应用界面能够根据设备屏幕尺寸和方向自动调整布局,提供最佳的用户体验。在HarmonyOS NEXT中,我们可以使用以下技术实现自适应布局:
技术 | 描述 | 使用场景 |
---|---|---|
媒体查询 | 根据设备屏幕尺寸和方向应用不同的样式 | 在不同尺寸的设备上使用不同的布局 |
百分比布局 | 使用百分比值设置组件尺寸 | 使组件尺寸相对于父容器自动调整 |
弹性布局 | 使用弹性布局使组件自动填充可用空间 | 使组件尺寸相对于可用空间自动调整 |
栅格布局 | 使用栅格系统组织界面元素 | 创建复杂且响应式的布局 |
媒体查询允许我们根据设备屏幕尺寸和方向应用不同的样式。在HarmonyOS NEXT中,我们可以使用@MediaQuery
装饰器实现媒体查询:
@Component
export struct LearningPlatform {
@State currentLesson: number = 0
@State currentChapter: Lesson[] = [...]
// 添加媒体查询状态
@State isWideScreen: boolean = false
@State videoHeight: string = '40%'
@State contentPadding: number = 20
// 媒体查询装饰器
@MediaQuery(MediaQueryCondition.WIDE_SCREEN)
onWideScreen(matches: boolean) {
this.isWideScreen = matches
this.videoHeight = matches ? '50%' : '40%'
this.contentPadding = matches ? 30 : 20
}
build() {
// 根据屏幕宽度选择不同的布局
if (this.isWideScreen) {
this.buildWideScreenLayout()
} else {
this.buildNormalLayout()
}
}
// 宽屏布局
buildWideScreenLayout() {
ColumnSplit() {
// 上部视频区
Column() {
Image($r('app.media.big14'))
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
}
.height(this.videoHeight)
// 下部内容区
Row() {
// 左侧课程列表
Column() {
Text('课程章节')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 15 })
Scroll() {
Column() {
ForEach(this.currentChapter, (item: Lesson, index) => {
this.buildLessonItem(item, index)
})
}
}
.layoutWeight(1)
}
.width('30%')
.padding(15)
.backgroundColor('#f5f5f5')
// 右侧课程详情
Column() {
Text('HarmonyOS应用开发')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Text('第3章: UI组件与布局')
.fontSize(18)
.fontColor('#666666')
.margin({ bottom: 20 })
// 课程详情内容
this.buildLessonDetail()
// 导航按钮
this.buildNavigationButtons()
}
.layoutWeight(1)
.padding(this.contentPadding)
}
.layoutWeight(1)
}
.height(600)
}
// 普通布局
buildNormalLayout() {
ColumnSplit() {
// 上部视频区
Column() {
Image($r('app.media.big14'))
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
}
.height(this.videoHeight)
// 下部内容区
Column() {
Text('HarmonyOS应用开发')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Text('第3章: UI组件与布局')
.fontSize(16)
.fontColor('#666666')
.margin({ bottom: 20 })
Scroll() {
Column() {
ForEach(this.currentChapter, (item: Lesson, index) => {
this.buildLessonItem(item, index)
})
}
}
.layoutWeight(1)
this.buildNavigationButtons()
}
.padding(this.contentPadding)
}
.height(600)
}
@Builder
buildLessonItem(item: Lesson, index: number) {
Row() {
Image(item.completed ? $r('app.media.01') : $r('app.media.02'))
.width(20)
.height(20)
.margin({ right: 15 })
Column() {
Text(item.title)
.fontSize(this.isWideScreen ? 16 : 14)
Text(item.duration)
.fontSize(this.isWideScreen ? 12 : 10)
.fontColor('#999999')
}
.layoutWeight(1)
}
.padding(10)
.backgroundColor(index === this.currentLesson ? '#e3f2fd' : 'transparent')
.borderRadius(this.isWideScreen ? 8 : 4)
.onClick(() => this.currentLesson = index)
}
@Builder
buildLessonDetail() {
Column() {
Text(this.currentChapter[this.currentLesson].title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 15 })
Text('课程时长: ' + this.currentChapter[this.currentLesson].duration)
.fontSize(16)
.fontColor('#666666')
.margin({ bottom: 20 })
Text('课程内容')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Text('本课程将详细讲解HarmonyOS NEXT中的UI组件和布局容器,帮助你掌握构建优美界面的技巧。')
.fontSize(16)
.lineHeight(24)
}
}
@Builder
buildNavigationButtons() {
Row() {
Button('上一节')
.width(120)
.height(40)
.enabled(this.currentLesson > 0)
.opacity(this.currentLesson > 0 ? 1 : 0.5)
.onClick(() => {
if (this.currentLesson > 0) {
this.currentLesson--
}
})
Button('下一节')
.width(120)
.height(40)
.margin({ left: 20 })
.enabled(this.currentLesson < this.currentChapter.length - 1)
.opacity(this.currentLesson < this.currentChapter.length - 1 ? 1 : 0.5)
.onClick(() => {
if (this.currentLesson < this.currentChapter.length - 1) {
this.currentLesson++
}
})
}
.margin({ top: 20 })
.justifyContent(FlexAlign.Center)
}
}
在上面的代码中,我们使用@MediaQuery
装饰器监听屏幕宽度的变化,并根据屏幕宽度选择不同的布局:
ColumnSplit
将界面分为上部视频区和下部内容区,然后在下部内容区使用Row
将其分为左侧课程列表和右侧课程详情。ColumnSplit
将界面分为上部视频区和下部内容区,然后在下部内容区显示课程列表。此外,我们还根据屏幕宽度调整了组件的尺寸、字体大小和边距,使界面在不同尺寸的设备上都能提供良好的用户体验。
栅格布局是一种将界面划分为网格的布局方式,可以帮助我们创建复杂且响应式的布局。在HarmonyOS NEXT中,我们可以使用GridRow
和GridCol
组件实现栅格布局:
@Component
struct GridLayoutExample {
build() {
Column() {
GridRow() {
GridCol({ span: 12 }) {
Text('课程学习平台')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ bottom: 20 })
}
GridCol({ span: 4 }) {
this.buildCourseList()
}
GridCol({ span: 8 }) {
this.buildCourseDetail()
}
}
.gutter(16) // 设置列间距
}
.width('100%')
.padding(20)
}
@Builder
buildCourseList() {
// 课程列表内容
}
@Builder
buildCourseDetail() {
// 课程详情内容
}
}
在这个例子中,我们使用GridRow
和GridCol
组件创建了一个12列的栅格布局:
通过调整span
属性的值,我们可以轻松地调整各部分的宽度比例,实现响应式布局。
@BuilderParam
装饰器允许我们将构建函数作为参数传递给组件,实现高度的布局定制。这在创建可复用的组件时特别有用:
@Component
struct LessonCard {
@Prop lesson: Lesson
@Prop isSelected: boolean
@BuilderParam cardHeader: () => void
@BuilderParam cardFooter: () => void
onSelect: (lesson: Lesson) => void
build() {
Column() {
// 使用传入的构建函数构建卡片头部
this.cardHeader()
// 卡片内容
Row() {
Image(this.lesson.completed ? $r('app.media.01') : $r('app.media.02'))
.width(20)
.height(20)
.margin({ right: 15 })
Column() {
Text(this.lesson.title)
.fontSize(16)
Text(this.lesson.duration)
.fontSize(12)
.fontColor('#999999')
}
.layoutWeight(1)
}
.padding(10)
// 使用传入的构建函数构建卡片底部
this.cardFooter()
}
.backgroundColor(this.isSelected ? '#e3f2fd' : 'transparent')
.borderRadius(8)
.onClick(() => this.onSelect(this.lesson))
}
}
// 使用LessonCard组件
@Component
struct LessonList {
@State currentLesson: number = 0
@State lessons: Lesson[] = [...]
build() {
Column() {
ForEach(this.lessons, (item: Lesson, index) => {
LessonCard({
lesson: item,
isSelected: index === this.currentLesson,
onSelect: (lesson: Lesson) => {
this.currentLesson = index
},
cardHeader: () => {
Text('课程 ' + (index + 1))
.fontSize(14)
.fontWeight(FontWeight.Bold)
.padding(5)
.backgroundColor('#f0f0f0')
.width('100%')
},
cardFooter: () => {
Row() {
Text('点击查看详情')
.fontSize(12)
.fontColor('#666666')
}
.padding(5)
.justifyContent(FlexAlign.Center)
.width('100%')
}
})
.margin({ bottom: 10 })
})
}
}
}
在这个例子中,我们创建了一个LessonCard
组件,它接受两个@BuilderParam
参数:cardHeader
和cardFooter
。这允许使用者自定义卡片的头部和底部,实现高度的布局定制。
@Component
struct CoursePlayer {
@Prop title: string
@Prop duration: string
@Prop videoSource: Resource
@Prop isPlaying: boolean = false
@State progress: number = 0
onPlayStateChange: (isPlaying: boolean) => void
onProgressChange: (progress: number) => void
build() {
Column() {
// 视频播放区域
Stack() {
Image(this.videoSource)
.width('100%')
.height(200)
.objectFit(ImageFit.Cover)
if (!this.isPlaying) {
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image($r('app.media.play'))
.width(24)
.height(24)
}
.width(50)
.height(50)
.backgroundColor('rgba(0, 0, 0, 0.5)')
.onClick(() => this.onPlayStateChange(true))
}
}
.width('100%')
// 播放控制区域
Column() {
Text(this.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ top: 10, bottom: 5 })
Text(this.formatTime(this.progress * parseInt(this.duration)) + ' / ' + this.duration)
.fontSize(12)
.fontColor('#666666')
.margin({ bottom: 10 })
Slider({
value: this.progress * 100,
min: 0,
max: 100,
step: 1,
style: SliderStyle.OutSet
})
.width('100%')
.onChange((value: number) => {
this.progress = value / 100
this.onProgressChange(this.progress)
})
Row() {
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image(this.isPlaying ? $r('app.media.pause') : $r('app.media.play'))
.width(20)
.height(20)
}
.width(40)
.height(40)
.backgroundColor('#e3f2fd')
.onClick(() => this.onPlayStateChange(!this.isPlaying))
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image($r('app.media.forward'))
.width(20)
.height(20)
}
.width(40)
.height(40)
.backgroundColor('#e3f2fd')
.margin({ left: 10 })
.onClick(() => {
this.progress = Math.min(1, this.progress + 0.1)
this.onProgressChange(this.progress)
})
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image($r('app.media.fullscreen'))
.width(20)
.height(20)
}
.width(40)
.height(40)
.backgroundColor('#e3f2fd')
.margin({ left: 10 })
}
.margin({ top: 10 })
.justifyContent(FlexAlign.Start)
}
.padding(10)
}
.width('100%')
.backgroundColor('#ffffff')
.borderRadius(8)
}
private formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = Math.floor(seconds % 60)
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`
}
}
这个CoursePlayer
组件封装了视频播放器的功能,包括播放/暂停按钮、进度条、快进按钮和全屏按钮。通过@Prop
和回调函数,它可以与父组件进行交互,实现播放状态和进度的同步。
@Component
struct CourseNotes {
@Link notes: string
@State isEditing: boolean = false
build() {
Column() {
Row() {
Text('课程笔记')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Button(this.isEditing ? '保存' : '编辑')
.fontSize(14)
.height(30)
.backgroundColor(this.isEditing ? '#4CAF50' : '#2196F3')
.onClick(() => this.isEditing = !this.isEditing)
}
.width('100%')
.margin({ bottom: 10 })
if (this.isEditing) {
TextArea({ text: this.notes, placeholder: '在这里添加笔记...' })
.width('100%')
.height(150)
.onChange((value: string) => {
this.notes = value
})
} else {
Text(this.notes || '暂无笔记')
.fontSize(14)
.width('100%')
.height(150)
.backgroundColor('#f5f5f5')
.padding(10)
.borderRadius(4)
}
}
.width('100%')
.padding(15)
.backgroundColor('#ffffff')
.borderRadius(8)
}
}
这个CourseNotes
组件封装了课程笔记的功能,包括查看和编辑两种模式。通过@Link
装饰器,它可以与父组件共享笔记数据,实现数据的双向绑定。
在HarmonyOS NEXT中,我们可以使用AppStorage
和@StorageProp
实现主题切换功能:
// 在应用启动时初始化主题设置
AppStorage.SetOrCreate('isDarkMode', false)
@Component
struct ThemeSwitcher {
@StorageProp('isDarkMode') isDarkMode: boolean = false
build() {
Row() {
Text(this.isDarkMode ? '深色模式' : '浅色模式')
.fontSize(16)
.fontColor(this.isDarkMode ? '#ffffff' : '#333333')
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
.onChange((isOn: boolean) => {
AppStorage.Set('isDarkMode', isOn)
})
}
.width('100%')
.padding(15)
.backgroundColor(this.isDarkMode ? '#333333' : '#ffffff')
.borderRadius(8)
}
}
// 在主组件中使用主题
@Component
struct LearningPlatform {
@StorageProp('isDarkMode') isDarkMode: boolean = false
build() {
Column() {
// 应用内容
}
.width('100%')
.height('100%')
.backgroundColor(this.isDarkMode ? '#121212' : '#f5f5f5')
// 根据主题设置其他样式
}
}
通过AppStorage
和@StorageProp
,我们可以在应用的不同组件之间共享主题设置,实现全局的主题切换功能。
在HarmonyOS NEXT中,我们可以使用资源文件和$r
函数实现多语言支持:
// 在resources/zh_CN/string.json中定义中文文本
{
"course_title": "HarmonyOS应用开发",
"chapter_title": "第3章: UI组件与布局",
"prev_lesson": "上一节",
"next_lesson": "下一节",
"add_note": "添加笔记"
}
// 在resources/en_US/string.json中定义英文文本
{
"course_title": "HarmonyOS App Development",
"chapter_title": "Chapter 3: UI Components and Layouts",
"prev_lesson": "Previous",
"next_lesson": "Next",
"add_note": "Add Note"
}
// 在组件中使用多语言文本
@Component
struct LearningPlatform {
build() {
Column() {
Text($r('app.string.course_title'))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Text($r('app.string.chapter_title'))
.fontSize(16)
.fontColor('#666666')
.margin({ bottom: 20 })
// 其他内容
Row() {
Button($r('app.string.prev_lesson'))
.width(120)
.height(40)
Button($r('app.string.next_lesson'))
.width(120)
.height(40)
.margin({ left: 20 })
}
.margin({ top: 20 })
.justifyContent(FlexAlign.Center)
}
.padding(20)
}
}
通过使用资源文件和$r
函数,我们可以根据设备的语言设置自动显示相应的文本,实现多语言支持。
在本教程中,我们学习了课程学习平台的高级布局技巧和自适应设计,包括:
@BuilderParam
实现布局定制,创建高度可定制的组件扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有