HarmonyOS-Music 项目技术文档
项目概述
HarmonyOS-Music是一款基于HarmonyOS平台开发的音乐应用,采用ArkTS语言和HarmonyOS的声明式UI开发框架,为用户提供音乐播放、搜索、收藏等功能。项目展示了HarmonyOS应用开发的核心技术和最佳实践,包括组件化设计、状态管理和页面导航等。
技术栈
- ArkTS: HarmonyOS的声明式UI开发语言,基于TypeScript扩展
- HarmonyOS UI框架: 提供丰富的UI组件和布局能力
- 状态管理: 使用@State、@Prop、@Link等装饰器管理组件状态
- 组件化开发: 采用自定义组件封装复用逻辑
项目结构
bash
entry/src/main/
├── ets/ # ArkTS源代码
│ ├── common/ # 公共组件和工具
│ │ ├── components/ # 可复用组件
│ │ ├── constants/ # 常量定义
│ │ └── utils/ # 工具类
│ ├── entryability/ # 应用入口
│ └── pages/ # 页面组件
└── resources/ # 资源文件
├── base/ # 基础资源
│ ├── element/ # 文本和颜色资源
│ └── media/ # 图片和媒体资源
└── rawfile/ # 原始文件资源
核心页面分析
1. 应用入口 (Index.ets)
Index.ets是应用的主入口,负责页面导航和状态管理。
关键代码分析:
typescript
@Entry
@Component
struct Index {
@State currentPage: string = 'home';
@State showPlayerPage: boolean = false;
// 音乐播放相关状态
@State songTitle: string = '夜曲';
@State artistName: string = '周杰伦';
@State albumName: string = '十一月的萧邦';
@State isPlaying: boolean = false;
@State currentTime: string = '0:00';
@State totalTime: string = '3:30';
@State progress: number = 0;
build() {
Stack({ alignContent: Alignment.Bottom }) {
// 根据状态显示不同页面
if (this.showPlayerPage) {
PlayerPage({
songTitle: this.songTitle,
artistName: this.artistName,
// 其他属性...
onClose: () => {
this.showPlayerPage = false;
}
})
} else {
// 显示主导航页面
Stack({ alignContent: Alignment.Bottom }) {
// 根据当前选中的页面显示对应内容
if (this.currentPage === 'home') {
HomePage()
} else if (this.currentPage === 'search') {
SearchPage()
} // 其他页面...
// 迷你播放器
MiniPlayer({
songTitle: this.songTitle,
artistName: this.artistName,
// 其他属性和回调...
})
// 底部导航栏
BottomNavBar({ currentPage: $currentPage })
}
}
}
}
}
技术要点:
- @Entry装饰器: 标记应用入口组件
- @State装饰器: 定义组件内部状态,状态变化会触发UI刷新
- @Link装饰器: 实现父子组件间的双向数据绑定
- 条件渲染: 使用if-else语句根据状态显示不同页面
- 组件嵌套: 通过组合不同组件构建复杂UI
2. 首页 (HomePage.ets)
首页是用户进入应用的第一个界面,主要展示推荐内容和热门歌单。
关键代码分析:
typescript
@Component
export struct HomePage {
@State username: string = '小明';
@State currentPage: string = 'home';
build() {
Column() {
// 内容区域 - 使用滚动容器
Scroll() {
Column() {
// 顶部欢迎语和头像
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
// 欢迎语
Column() {
Text(`你好,${this.username}`)
.fontSize(StyleConstants.FONT_SIZE_XLARGE)
.fontWeight(FontWeight.Bold)
Text('享受美妙的音乐时光')
.fontSize(StyleConstants.FONT_SIZE_SMALL)
.fontColor(StyleConstants.TEXT_SECONDARY)
}
// 用户头像
Stack() {
Text(this.username.substring(0, 2))
.fontColor(StyleConstants.TEXT_WHITE)
}
.backgroundColor(StyleConstants.PRIMARY_COLOR)
.borderRadius(StyleConstants.RADIUS_CIRCLE)
}
// 搜索框
Row() {
Image(IconUtils.SEARCH)
Text('搜索歌曲、艺术家或专辑')
}
.backgroundColor(StyleConstants.BACKGROUND_LIGHT_GRAY)
.borderRadius(StyleConstants.RADIUS_XLARGE)
// 为你推荐
Column() {
// 推荐卡片横向滚动
Scroll() {
Row({ space: StyleConstants.MARGIN_MEDIUM }) {
this.RecommendCard('今日热门', '根据你的口味推荐', $r('app.media.icon'), '#5e35b1')
// 更多推荐卡片...
}
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Horizontal)
}
// 热门歌单和最近播放...
}
}
}
}
// 自定义组件构建器
@Builder
RecommendCard(title: string, description: string, image: Resource, bgColor: string) {
// 推荐卡片UI实现
}
@Builder
SongItem(title: string, artist: string, image: Resource) {
// 歌曲项UI实现
}
}
技术要点:
- @Component装饰器: 定义可复用的自定义组件
- @Builder装饰器: 创建可复用的UI片段,类似于函数式组件
- Flex布局: 使用Flex容器实现灵活的布局
- Scroll组件: 实现内容滚动,支持水平和垂直滚动
- 链式调用: ArkTS支持链式调用设置组件属性
3. 搜索页面 (SearchPage.ets)
搜索页面允许用户查找特定的歌曲、艺术家或专辑,并展示热门搜索和搜索历史。
关键代码分析:
typescript
@Component
export struct SearchPage {
@State searchText: string = '';
@State searchHistory: string[] = ['夜曲 周杰伦', '陈奕迅', 'Beyond 海阔天空'];
build() {
Column() {
Scroll() {
Column() {
// 搜索框
Row() {
Row() {
Image(IconUtils.SEARCH)
TextInput({ placeholder: '搜索歌曲、艺术家或专辑', text: this.searchText })
.onChange((value) => {
this.searchText = value;
})
}
Text('取消')
}
// 热门搜索
Column() {
Text('热门搜索')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
this.SearchTag('周杰伦')
this.SearchTag('陈奕迅')
// 更多搜索标签...
}
}
// 搜索历史
Column() {
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Text('搜索历史')
Row() {
Image(IconUtils.TRASH)
Text('清除')
}
.onClick(() => {
this.searchHistory = [];
})
}
// 历史记录列表
Column({ space: StyleConstants.MARGIN_MEDIUM }) {
if (this.searchHistory.length > 0) {
ForEach(this.searchHistory, (item: string, index: number) => {
this.HistoryItem(item, index)
})
} else {
Text('暂无搜索历史')
}
}
}
}
}
}
}
@Builder
SearchTag(text: string) {
// 搜索标签UI实现
}
@Builder
HistoryItem(text: string, index: number) {
// 历史记录项UI实现
}
}
技术要点:
- TextInput组件: 实现文本输入功能,支持事件监听
- ForEach循环: 基于数组数据动态渲染UI元素
- 条件渲染: 根据数组长度显示不同内容
- 事件处理: 通过onClick等事件处理用户交互
- Flex布局: 使用Flex.wrap实现标签的自动换行
4. 播放器页面 (PlayerPage.ets)
播放器页面是用户欣赏音乐的核心界面,提供了完整的音乐播放控制和歌词显示功能。
关键代码分析:
typescript
@Component
export struct PlayerPage {
@State songTitle: string = '夜曲';
@State artistName: string = '周杰伦';
@State albumName: string = '十一月的萧邦';
@State isPlaying: boolean = true;
@State currentTime: string = '1:13';
@State totalTime: string = '3:30';
@State progress: number = 35; // 播放进度百分比
@State albumRotation: number = 0; // 专辑封面旋转角度
@State pageOpacity: number = 0; // 页面透明度,用于入场动画
@State pageScale: number = 0.95; // 页面缩放,用于入场动画
private albumRotateTimer: number = -1; // 专辑旋转定时器ID
// 关闭播放页面的回调函数
onClose?: () => void;
aboutToAppear() {
// 页面入场动画
animateTo({
duration: 300,
curve: Curve.EaseOut,
}, () => {
this.pageOpacity = 1;
this.pageScale = 1;
});
// 创建专辑旋转动画
this.startAlbumRotation();
}
aboutToDisappear() {
// 停止专辑旋转动画
this.stopAlbumRotation();
}
// 开始专辑旋转动画
private startAlbumRotation() {
// 停止之前的动画
this.stopAlbumRotation();
// 只有在播放状态才旋转
if (this.isPlaying) {
// 使用定时器实现旋转动画
this.albumRotateTimer = setInterval(() => {
this.albumRotation = (this.albumRotation + 0.9) % 360;
}, 50);
}
}
// 停止专辑旋转动画
private stopAlbumRotation() {
if (this.albumRotateTimer !== -1) {
clearInterval(this.albumRotateTimer);
this.albumRotateTimer = -1;
}
}
build() {
Column() {
// 顶部控制
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image(IconUtils.ARROW_DOWN)
}
.onClick(() => {
if (this.onClose) {
this.onClose();
}
})
// 其他顶部控件...
}
// 专辑封面
Column() {
Image($r('app.media.icon'))
.rotate({ x: 0, y: 0, z: 1, angle: this.albumRotation }) // 添加旋转效果
.animation({ duration: 500 }) // 平滑过渡
}
// 进度条
Column() {
// 进度条
Row() {
Column()
.width(`${this.progress}%`)
.height(4)
.backgroundColor(StyleConstants.TEXT_WHITE)
.animation({ duration: 300, curve: Curve.EaseOut }) // 添加进度条动画
}
.backgroundColor('rgba(255, 255, 255, 0.2)')
// 时间显示
Flex({ justifyContent: FlexAlign.SpaceBetween }) {
Text(this.currentTime)
Text(this.totalTime)
}
}
// 播放控制
Row() {
// 播放控制按钮...
// 播放/暂停
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image(this.isPlaying ? IconUtils.PAUSE : IconUtils.PLAY)
.animation({ duration: 200, curve: Curve.FastOutSlowIn }) // 添加图标切换动画
}
.onClick(() => {
this.isPlaying = !this.isPlaying;
// 控制专辑旋转动画
if (this.isPlaying) {
this.startAlbumRotation();
} else {
this.stopAlbumRotation();
}
})
}
// 歌词展示
Column() {
Text('你听见 风声了吗')
Text('风声吹动树叶飘动就像我的心')
.fontWeight(FontWeight.Medium)
Text('动着摇着')
}
}
}
}
技术要点:
- 生命周期函数: 使用aboutToAppear和aboutToDisappear管理组件生命周期
- 动画效果: 使用animateTo和animation实现过渡动画
- 定时器: 使用setInterval实现专辑旋转动画
- 事件回调: 通过回调函数实现组件间通信
- 状态管理: 使用@State管理UI状态和动画状态
5. 底部导航栏组件 (BottomNavBar.ets)
底部导航栏是应用的主要导航工具,允许用户在不同页面间切换。
关键代码分析:
typescript
@Component
export struct BottomNavBar {
@Link currentPage: string;
build() {
Flex({ justifyContent: FlexAlign.SpaceAround, alignItems: ItemAlign.Center }) {
// 首页
Column() {
Image(IconUtils.HOME)
.fillColor(this.currentPage === 'home' ? StyleConstants.PRIMARY_COLOR : StyleConstants.TEXT_GRAY)
Text('首页')
.fontColor(this.currentPage === 'home' ? StyleConstants.PRIMARY_COLOR : StyleConstants.TEXT_GRAY)
}
.onClick(() => {
this.currentPage = 'home';
})
// 搜索
Column() {
Image(IconUtils.SEARCH)
.fillColor(this.currentPage === 'search' ? StyleConstants.PRIMARY_COLOR : StyleConstants.TEXT_GRAY)
Text('搜索')
.fontColor(this.currentPage === 'search' ? StyleConstants.PRIMARY_COLOR : StyleConstants.TEXT_GRAY)
}
.onClick(() => {
this.currentPage = 'search';
})
// 其他导航项...
}
.height(StyleConstants.BOTTOM_NAV_HEIGHT)
.width('100%')
.backgroundColor(StyleConstants.BACKGROUND_COLOR)
.borderColor('rgba(0, 0, 0, 0.1)')
.borderWidth({ top: 1 })
}
}
技术要点:
- @Link装饰器: 实现与父组件的双向数据绑定
- 条件样式: 根据当前页面状态动态改变样式
- 事件处理: 通过onClick事件处理导航切换
- Flex布局: 使用Flex实现均匀分布的导航项
6. 迷你播放器组件 (MiniPlayer.ets)
迷你播放器显示在应用底部,提供当前播放歌曲的基本信息和控制。
关键代码分析:
typescript
@Component
export struct MiniPlayer {
// 当前播放歌曲信息
@Prop songTitle: string = '夜曲';
@Prop artistName: string = '周杰伦';
@Prop coverImage: Resource = $r('app.media.icon');
@Prop isPlaying: boolean = false;
// 播放控制回调
onPlayPause?: () => void;
onNext?: () => void;
onTap?: () => void; // 点击迷你播放器进入完整播放页面
build() {
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
// 歌曲信息部分
Row({ space: StyleConstants.MARGIN_MEDIUM }) {
Image(this.coverImage)
.borderRadius(StyleConstants.RADIUS_SMALL)
Column() {
Text(this.songTitle)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.artistName)
.fontSize(StyleConstants.FONT_SIZE_SMALL)
.fontColor(StyleConstants.TEXT_SECONDARY)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
}
// 控制按钮部分
Row({ space: StyleConstants.MARGIN_LARGE }) {
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image(this.isPlaying ? IconUtils.PAUSE : IconUtils.PLAY)
}
.onClick(() => {
if (this.onPlayPause) {
this.onPlayPause();
}
})
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image(IconUtils.NEXT)
}
.onClick(() => {
if (this.onNext) {
this.onNext();
}
})
}
}
.onClick(() => {
if (this.onTap) {
this.onTap();
}
})
}
}
技术要点:
- @Prop装饰器: 用于接收父组件传递的只读属性
- 回调函数: 通过可选的回调函数实现子到父的事件通知
- 文本溢出处理: 使用maxLines和textOverflow处理文本溢出
- 事件冒泡: 组件整体和内部按钮都有点击事件
7. 样式常量 (StyleConstants.ets)
样式常量文件集中管理应用的样式定义,提高代码的可维护性。
关键代码分析:
typescript
export class StyleConstants {
/**
* 颜色常量
*/
// 主题色
static readonly PRIMARY_COLOR: string = '#5e35b1';
static readonly SECONDARY_COLOR: string = '#4527a0';
static readonly ACCENT_COLOR: string = '#03dac6';
// 文本颜色
static readonly TEXT_PRIMARY: string = '#212121';
static readonly TEXT_SECONDARY: string = '#757575';
static readonly TEXT_WHITE: string = '#FFFFFF';
static readonly TEXT_GRAY: string = '#9e9e9e';
// 背景颜色
static readonly BACKGROUND_COLOR: string = '#FFFFFF';
static readonly BACKGROUND_GRAY: string = '#f5f5f7';
static readonly BACKGROUND_LIGHT_GRAY: string = '#f5f5f5';
/**
* 尺寸常量
*/
// 字体大小
static readonly FONT_SIZE_SMALL: number = 12;
static readonly FONT_SIZE_MEDIUM: number = 14;
static readonly FONT_SIZE_LARGE: number = 16;
static readonly FONT_SIZE_XLARGE: number = 18;
// 间距
static readonly MARGIN_SMALL: number = 4;
static readonly MARGIN_MEDIUM: number = 8;
static readonly MARGIN_LARGE: number = 16;
static readonly MARGIN_XLARGE: number = 24;
// 圆角
static readonly RADIUS_SMALL: number = 8;
static readonly RADIUS_MEDIUM: number = 8;
static readonly RADIUS_LARGE: number = 12;
static readonly RADIUS_XLARGE: number = 16;
static readonly RADIUS_CIRCLE: number = 100;
// 组件高度
static readonly STATUS_BAR_HEIGHT: number = 44;
static readonly BOTTOM_NAV_HEIGHT: number = 56;
static readonly MINI_PLAYER_HEIGHT: number = 64;
}
技术要点:
- 静态常量: 使用static readonly定义不可变的常量
- 分类组织: 按照颜色、尺寸等类别组织常量
- 命名规范: 使用全大写和下划线分隔的命名方式
8. 图标工具类 (IconUtils.ets)
图标工具类集中管理应用中使用的所有图标资源。
关键代码分析:
typescript
export class IconUtils {
// 导航图标
static readonly HOME: Resource = $r('app.media.ic_home');
static readonly SEARCH: Resource = $r('app.media.ic_search');
static readonly LIBRARY: Resource = $r('app.media.ic_library');
static readonly USER: Resource = $r('app.media.ic_user');
static readonly SETTINGS: Resource = $r('app.media.ic_settings');
// 播放控制图标
static readonly PLAY: Resource = $r('app.media.ic_play');
static readonly PAUSE: Resource = $r('app.media.ic_pause');
static readonly NEXT: Resource = $r('app.media.ic_next');
static readonly PREV: Resource = $r('app.media.ic_prev');
static readonly REPEAT: Resource = $r('app.media.ic_repeat');
static readonly SHUFFLE: Resource = $r('app.media.ic_shuffle');
// 其他图标...
}
技术要点:
- 资源引用: 使用$r函数引用应用资源
- 静态常量: 使用static readonly定义不可变的图标引用
- 分类组织: 按照功能分类组织图标资源
ArkTS语言特性
1. 装饰器
ArkTS使用装饰器来扩展组件和属性的功能:
- @Entry: 标记应用入口组件
- @Component: 定义自定义组件
- @State: 定义组件内部状态,状态变化会触发UI刷新
- @Prop: 定义从父组件接收的只读属性
- @Link: 实现与父组件的双向数据绑定
- @Builder: 定义UI构建函数,类似于函数式组件
2. 声明式UI
ArkTS使用声明式语法描述UI结构,类似于React和SwiftUI:
typescript
build() {
Column() {
Text('Hello World')
.fontSize(20)
.fontColor('#FF0000')
Button('Click Me')
.width('80%')
.height(40)
.onClick(() => {
console.info('Button clicked');
})
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
3. 状态管理
ArkTS提供了多种状态管理机制:
- @State: 组件内部状态
- @Prop: 父组件传递的只读属性
- @Link: 父子组件间的双向绑定
- @Provide/@Consume: 跨组件层级的状态共享
4. 生命周期
ArkTS组件有以下生命周期函数:
- aboutToAppear(): 组件即将出现时调用
- aboutToDisappear(): 组件即将消失时调用
- onPageShow(): 页面显示时调用
- onPageHide(): 页面隐藏时调用
- onBackPress(): 用户点击返回按钮时调用