React Native鸿蒙跨平台音乐播放器涉及实时进度更新、播放控制、列表交互、状态管理等核心技术点
本文详细探讨了一个基于React Native的音乐播放器应用实现方案。该方案采用清晰的单组件架构,通过useState管理播放状态、进度、音量等核心数据,实现了播放控制、进度管理、播放模式切换等完整功能。应用使用TypeScript定义数据类型,确保代码安全性和可维护性。界面采用垂直滚动布局,符合用户习惯。针对跨平台适配,采用React Native核心组件、统一样式定义等策略。文章还提出了性能
在移动应用开发中,音乐播放器是一种常见的应用类型,需要考虑音频播放、状态管理、用户交互等多个方面。本文将深入分析一个功能完备的 React Native 音乐播放器应用实现,探讨其架构设计、状态管理、用户交互以及跨端兼容性策略。
架构设计
该实现采用了清晰的单组件架构,主要包含以下部分:
- 主应用组件 (
MusicPlayerApp) - 负责整体布局和状态管理 - 播放控制 - 提供播放/暂停、上一首、下一首等控制功能
- 进度管理 - 显示和控制音乐播放进度
- 播放模式 - 提供重复播放、随机播放等模式控制
- 歌曲信息 - 显示当前播放歌曲的标题、艺术家、专辑等信息
- 专辑封面 - 显示专辑封面
这种架构设计使得代码结构清晰,易于维护。主应用组件负责管理全局状态和业务逻辑,而各个UI部分则负责具体的展示,实现了关注点分离。
状态管理
MusicPlayerApp 组件使用 useState 钩子管理多个关键状态:
const [currentTrack, setCurrentTrack] = useState<MusicTrack>({...});
const [isPlaying, setIsPlaying] = useState<boolean>(true);
const [currentTime, setCurrentTime] = useState<number>(0);
const [volume, setVolume] = useState<number>(80);
const [progress, setProgress] = useState<number>(30);
const [repeatMode, setRepeatMode] = useState<'off' | 'one' | 'all'>('all');
const [shuffle, setShuffle] = useState<boolean>(false);
这种状态管理方式简洁高效,通过状态更新触发组件重新渲染,实现了音乐播放器的各种功能。使用 TypeScript 联合类型确保了 repeatMode 的取值只能是预定义的几个选项之一,提高了代码的类型安全性。
播放控制
应用实现了完整的播放控制功能:
- 播放/暂停 - 切换音乐的播放状态
- 上一首 - 播放上一首歌曲,循环播放
- 下一首 - 播放下一首歌曲,循环播放
- 进度控制 - 显示和控制音乐播放进度
这种实现方式为用户提供了直观的音乐播放控制体验,满足了音乐播放器的基本功能需求。
播放模式
应用实现了多种播放模式:
- 重复模式 - 支持关闭重复、单曲循环、全部循环三种模式
- 随机播放 - 支持随机播放模式
这种实现方式为用户提供了灵活的播放模式选择,增强了音乐播放器的实用性。
进度管理
应用实现了音乐播放进度的管理:
// 模拟播放进度
useEffect(() => {
let interval: NodeJS.Timeout;
if (isPlaying) {
interval = setInterval(() => {
setProgress(prev => {
if (prev >= 100) {
handleNext(); // 播放下一首
return 0;
}
return prev + 0.5; // 模拟进度增加
});
}, 1000);
}
return () => clearInterval(interval);
}, [isPlaying]);
这种实现方式使用 useEffect 钩子和 setInterval 函数模拟音乐播放进度,当进度达到 100% 时自动播放下一首歌曲。在组件卸载时,通过返回清理函数清除定时器,避免了内存泄漏。
收藏功能
应用实现了音乐收藏功能:
// 切换收藏状态
const toggleFavorite = () => {
setCurrentTrack(prev => ({
...prev,
isFavorite: !prev.isFavorite
}));
};
这种实现方式允许用户通过点击按钮切换歌曲的收藏状态,增强了音乐播放器的个性化功能。
类型定义
该实现使用 TypeScript 定义了核心数据类型:
// 音乐类型
type MusicTrack = {
id: string;
title: string;
artist: string;
album: string;
duration: string;
cover: string;
isFavorite: boolean;
};
这个类型定义包含了音乐的完整信息,包括 ID、标题、艺术家、专辑、时长、封面和收藏状态。这种类型定义使得数据结构更加清晰,提高了代码的可读性和可维护性,同时也提供了类型安全保障。
数据组织
应用数据按照功能模块进行组织:
- currentTrack - 当前播放的音乐
- playlist - 播放列表,包含多首音乐
- isPlaying - 播放状态
- progress - 播放进度
- repeatMode - 重复模式
- shuffle - 随机播放状态
这种数据组织方式使得数据管理更加清晰,易于扩展和维护。
布局结构
应用界面采用了清晰的垂直滚动布局:
- 顶部 - 显示当前播放歌曲的标题
- 中部 - 显示专辑封面、歌曲信息、播放进度条
- 底部 - 显示播放控制按钮、重复模式、随机播放按钮
这种布局结构符合用户的使用习惯,用户可以通过滚动查看完整的播放器界面。
针对上述问题,该实现采用了以下适配策略:
- 使用 React Native 核心组件 - 优先使用 React Native 内置的组件,如 SafeAreaView、ScrollView、TouchableOpacity 等
- 统一的样式定义 - 使用 StyleSheet.create 定义样式,确保样式在不同平台上的一致性
- Base64 图标 - 使用 Base64 编码的图标,确保图标在不同平台上的一致性
- 平台无关的图标 - 使用 Unicode 表情符号作为专辑封面的占位符,避免使用平台特定的图标库
- 简化的交互逻辑 - 使用简单直接的交互逻辑,减少平台差异带来的问题
- 使用标准的定时器 API - 使用 JavaScript 内置的 setInterval 函数,它在不同平台上有对应实现
当前实现使用 setInterval 模拟播放进度,可以考虑使用更高效的方式:
// 优化前
useEffect(() => {
let interval: NodeJS.Timeout;
if (isPlaying) {
interval = setInterval(() => {
setProgress(prev => {
if (prev >= 100) {
handleNext();
return 0;
}
return prev + 0.5;
});
}, 1000);
}
return () => clearInterval(interval);
}, [isPlaying]);
// 优化后
useEffect(() => {
let animationFrameId: number;
let lastUpdateTime = Date.now();
const updateProgress = () => {
if (isPlaying) {
const now = Date.now();
const deltaTime = (now - lastUpdateTime) / 1000; // 转换为秒
lastUpdateTime = now;
setProgress(prev => {
if (prev >= 100) {
handleNext();
return 0;
}
return prev + (deltaTime * 0.5); // 基于时间增量更新进度
});
animationFrameId = requestAnimationFrame(updateProgress);
}
};
if (isPlaying) {
animationFrameId = requestAnimationFrame(updateProgress);
}
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
}, [isPlaying]);
2. 状态管理优化
当前实现使用多个 useState 钩子管理状态,可以考虑使用 useReducer 或状态管理库来管理复杂状态:
// 优化前
const [currentTrack, setCurrentTrack] = useState<MusicTrack>({...});
const [isPlaying, setIsPlaying] = useState<boolean>(true);
const [progress, setProgress] = useState<number>(30);
const [repeatMode, setRepeatMode] = useState<'off' | 'one' | 'all'>('all');
const [shuffle, setShuffle] = useState<boolean>(false);
// 优化后
type PlayerState = {
currentTrack: MusicTrack;
isPlaying: boolean;
progress: number;
repeatMode: 'off' | 'one' | 'all';
shuffle: boolean;
};
type PlayerAction =
| { type: 'SET_CURRENT_TRACK'; payload: MusicTrack }
| { type: 'TOGGLE_PLAY' }
| { type: 'SET_PROGRESS'; payload: number }
| { type: 'TOGGLE_REPEAT' }
| { type: 'TOGGLE_SHUFFLE' }
| { type: 'PLAY_PREVIOUS' }
| { type: 'PLAY_NEXT' }
| { type: 'TOGGLE_FAVORITE' };
const initialState: PlayerState = {
currentTrack: {...},
isPlaying: true,
progress: 30,
repeatMode: 'all',
shuffle: false,
};
const playerReducer = (state: PlayerState, action: PlayerAction): PlayerState => {
switch (action.type) {
case 'SET_CURRENT_TRACK':
return { ...state, currentTrack: action.payload, progress: 0 };
case 'TOGGLE_PLAY':
return { ...state, isPlaying: !state.isPlaying };
case 'SET_PROGRESS':
return { ...state, progress: action.payload };
case 'TOGGLE_REPEAT':
const modes: Array<'off' | 'one' | 'all'> = ['off', 'one', 'all'];
const currentIndex = modes.indexOf(state.repeatMode);
const nextIndex = (currentIndex + 1) % modes.length;
return { ...state, repeatMode: modes[nextIndex] };
case 'TOGGLE_SHUFFLE':
return { ...state, shuffle: !state.shuffle };
case 'PLAY_PREVIOUS':
// 实现上一首逻辑
return state;
case 'PLAY_NEXT':
// 实现下一首逻辑
return state;
case 'TOGGLE_FAVORITE':
return {
...state,
currentTrack: {
...state.currentTrack,
isFavorite: !state.currentTrack.isFavorite
}
};
default:
return state;
}
};
const [state, dispatch] = useReducer(playerReducer, initialState);
3. 音频播放集成
当前实现只是模拟播放,可以考虑集成真实的音频播放功能:
import { Audio } from 'expo-av';
const MusicPlayerApp = () => {
const [sound, setSound] = useState<Audio.Sound | null>(null);
// 加载音频
const loadSound = async (track: MusicTrack) => {
try {
// 卸载之前的音频
if (sound) {
await sound.unloadAsync();
}
// 加载新音频
const { sound: newSound } = await Audio.Sound.createAsync(
{ uri: track.audioUri },
{ shouldPlay: isPlaying }
);
setSound(newSound);
} catch (error) {
console.error('加载音频失败:', error);
}
};
// 播放/暂停
const togglePlay = async () => {
if (sound) {
if (isPlaying) {
await sound.pauseAsync();
} else {
await sound.playAsync();
}
setIsPlaying(!isPlaying);
}
};
// 组件卸载时清理
useEffect(() => {
return () => {
if (sound) {
sound.unloadAsync();
}
};
}, [sound]);
// 当切换歌曲时加载音频
useEffect(() => {
loadSound(currentTrack);
}, [currentTrack]);
// 其他代码...
};
4. 导航系统
可以集成 React Navigation 实现多页面导航:
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: '音乐库' }}
/>
<Stack.Screen
name="Player"
component={MusicPlayerApp}
options={{ title: '播放器' }}
/>
<Stack.Screen
name="Playlist"
component={PlaylistScreen}
options={{ title: '播放列表' }}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
本文深入分析了一个功能完备的 React Native 音乐播放器应用实现,从架构设计、状态管理、用户交互到跨端兼容性都进行了详细探讨。该实现不仅功能完整,而且代码结构清晰,具有良好的可扩展性和可维护性。
音乐播放器是移动端应用的典型场景,涉及实时进度更新、播放控制、列表交互、状态管理等核心技术点。这份 React Native 音乐播放器代码,不仅完整实现了专业播放器的核心功能,更具备清晰的工程化设计思路,是跨端开发的优质范例。本文将从 React Native 原生实现细节、核心技术设计亮点、鸿蒙(HarmonyOS)跨端适配关键方案 三个维度,拆解这款播放器的技术架构,并提供可落地的跨端迁移方案。
1. 数据模型设计
应用基于 TypeScript 构建,首先定义了 MusicTrack 核心类型接口,明确音乐曲目的完整数据结构,涵盖 ID、标题、歌手、专辑、时长、封面、收藏状态等核心字段,为后续状态管理和 UI 渲染提供强类型约束。这种强类型设计,不仅在编码阶段规避类型错误,更让数据流转逻辑清晰可追溯,是大型应用工程化的基础。
type MusicTrack = {
id: string;
title: string;
artist: string;
album: string;
duration: string;
cover: string;
isFavorite: boolean;
};
播放列表、播放状态、进度、音量等核心状态,均通过 useState 实现精细化管理,状态设计遵循单一职责原则:
currentTrack:管理当前播放曲目,是播放器的核心状态;isPlaying:控制播放/暂停状态,关联进度更新逻辑;currentTime/progress:分别管理播放时间和进度百分比,满足不同展示需求;volume:独立管理音量控制,与播放逻辑解耦;repeatMode/shuffle:管理播放模式,支持单曲循环、全部循环、随机播放等场景;playlist:播放列表数据,通过useState初始化后只读,保证数据稳定性。
2. 实时播放进度核心逻辑
播放器的核心难点在于实时进度更新与播放状态联动,应用通过 useEffect 结合定时器完美实现这一逻辑:
useEffect(() => {
let interval: NodeJS.Timeout;
if (isPlaying) {
interval = setInterval(() => {
setProgress(prev => {
if (prev >= 100) {
handleNext(); // 播放下一首
return 0;
}
return prev + 0.5; // 模拟进度增加
});
}, 1000);
}
return () => clearInterval(interval);
}, [isPlaying]);
技术亮点:
- 状态联动:仅在
isPlaying为true时启动定时器,暂停时自动清理,避免无效计算; - 自动切歌:进度达到 100% 时自动调用
handleNext切换下一首,并重置进度,符合真实播放器逻辑; - 内存安全:通过
useEffect的清理函数销毁定时器,避免组件卸载后内存泄漏; - 不可变更新:使用函数式更新
setProgress(prev => {}),保证进度值的原子性更新; - 性能优化:1 秒更新一次进度,平衡实时性与性能消耗,符合移动端交互体验。
3. 播放控制
播放控制函数(togglePlay、handlePrevious、handleNext 等)设计遵循纯函数思想,仅负责状态更新,不包含 UI 渲染逻辑,保证逻辑与视图解耦:
- 上/下一首切换:通过
findIndex定位当前曲目索引,结合取模运算(currentIndex ± 1) % playlist.length实现列表循环,无需额外判断边界条件,代码简洁且鲁棒; - 播放模式切换:将重复模式存储为数组
['off', 'one', 'all'],通过索引切换实现模式循环,相比多个if/else更易扩展; - 收藏状态切换:使用展开运算符
...prev实现不可变更新,避免直接修改原对象导致的状态异常; - 进度跳转:通过触摸事件的
nativeEvent.locationX获取点击位置,计算百分比后更新进度,实现精准的进度条拖拽/点击跳转。
4. 交互体验
播放器的 UI 设计兼顾视觉层级与交互体验,核心组件设计亮点如下:
(1)进度条交互
进度条采用 View 嵌套实现,通过绝对定位的 progressThumb 实现滑块效果,支持点击跳转:
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${progress}%` }]} />
<TouchableOpacity
style={[styles.progressThumb, { left: `${progress}%` }]}
onPress={(e) => {
const x = e.nativeEvent.locationX;
const percentage = (x / 300) * 100;
seekTo(Math.min(100, Math.max(0, percentage)));
}}
/>
</View>
设计亮点:
- 视觉分层:进度条底色(
#cbd5e1)+ 已播放部分(#3b82f6)+ 滑块(圆形高亮),层次清晰; - 交互精准:通过
locationX计算点击位置百分比,限制取值范围在 0-100 之间,避免越界; - 响应式样式:进度填充和滑块位置均通过
progress状态动态计算,实时响应状态变化。
(2)播放控制
核心播放按钮(播放/暂停)采用大尺寸圆形设计,突出核心操作;次要控制按钮(上一首、下一首、随机、循环)采用统一尺寸,视觉层级分明:
<View style={styles.controlsContainer}>
{/* 随机播放按钮 */}
<TouchableOpacity style={styles.controlButton} onPress={toggleShuffle}>
<Text style={[styles.controlIcon, shuffle && styles.activeControl]}>🔀</Text>
</TouchableOpacity>
{/* 播放按钮(核心) */}
<TouchableOpacity style={styles.playButton} onPress={togglePlay}>
<Text style={styles.playIcon}>{isPlaying ? '⏸️' : '▶️'}</Text>
</TouchableOpacity>
{/* 其他按钮... */}
</View>
设计亮点:
- 视觉权重:播放按钮尺寸(60x60)远大于其他按钮,且采用蓝色背景突出,符合用户操作习惯;
- 状态可视化:激活状态(随机/循环)通过
activeControl样式改变颜色,反馈明确; - Emoji 图标:无需额外图片资源,通过 Emoji 实现图标展示,减少包体积且跨平台兼容。
(3)播放列表
播放列表采用卡片式设计,当前播放曲目通过背景色和文字颜色高亮,点击曲目可切换播放:
<TouchableOpacity
key={track.id}
style={[styles.playlistItem, currentTrack.id === track.id && styles.currentTrack]}
onPress={() => {
setCurrentTrack(track);
setProgress(0);
}}
>
{/* 曲目信息... */}
</TouchableOpacity>
设计亮点:
- 选中态高亮:当前播放曲目使用浅蓝背景 + 蓝色文字,视觉辨识度高;
- 布局合理:封面 + 信息 + 时长的经典布局,符合音乐列表交互习惯;
- 点击反馈:点击任意曲目可直接切换播放,并重置进度,交互流畅。
5. 样式系统
样式系统通过 StyleSheet.create 统一管理,核心设计亮点:
- 响应式尺寸:专辑封面宽度使用
width * 0.7,适配不同屏幕尺寸; - 色彩规范:主色调(
#3b82f6)统一用于进度条、播放按钮、激活状态,视觉一致性强; - 阴影效果:卡片组件通过
elevation(安卓)和shadow(iOS)实现跨平台阴影,提升层次感; - 圆角设计:所有卡片、按钮、进度条均采用圆角,符合现代移动端 UI 审美;
- 间距规范:统一的内边距/外边距(16px、24px),保证界面呼吸感。
将 React Native 音乐播放器迁移至鸿蒙平台,核心是基于 ArkTS + ArkUI 实现状态管理、实时进度、交互逻辑、UI 组件的对等还原,以下是关键适配技术点:
1. 状态管理
RN 的 useState/useEffect 在鸿蒙中对应 @State/@Watch/@Builder,核心状态迁移方案:
// 鸿蒙端状态定义
@Entry
@Component
struct MusicPlayerApp {
// 核心状态(与RN一一对应)
@State currentTrack: MusicTrack = { /* 初始值 */ };
@State isPlaying: boolean = true;
@State progress: number = 30;
@State volume: number = 80;
@State repeatMode: 'off' | 'one' | 'all' = 'all';
@State shuffle: boolean = false;
// 播放列表(只读)
private playlist: MusicTrack[] = [ /* 列表数据 */ ];
// 定时器管理
private intervalId: number = 0;
// 播放进度更新(对应RN的useEffect)
aboutToAppear() {
this.startProgressTimer();
}
aboutToDisappear() {
clearInterval(this.intervalId);
}
// 启动进度定时器
private startProgressTimer() {
if (this.isPlaying) {
this.intervalId = setInterval(() => {
if (this.progress >= 100) {
this.handleNext();
this.progress = 0;
} else {
this.progress += 0.5;
}
}, 1000);
} else {
clearInterval(this.intervalId);
}
}
// 播放/暂停切换(触发定时器重启)
@Watch('startProgressTimer')
private togglePlay() {
this.isPlaying = !this.isPlaying;
}
// 其他方法(handlePrevious/handleNext等)完全复用RN逻辑...
}
适配亮点:
- 生命周期对齐:通过
aboutToAppear/aboutToDisappear替代useEffect,管理定时器的创建与销毁; - 状态监听:使用
@Watch装饰器监听isPlaying变化,自动重启/停止定时器,实现与 RN 一致的状态联动; - 数据结构复用:
MusicTrack类型定义完全复用,仅调整语法为 ArkTS 兼容格式; - 逻辑零修改:上/下一首、播放模式切换、收藏状态更新等核心逻辑,无需修改即可迁移。
2. 核心组件
ArkUI 组件与 RN 组件高度对齐,核心组件映射关系及适配方案:
| RN 组件/特性 | 鸿蒙 ArkUI 对应实现 | 适配关键说明 |
|---|---|---|
SafeAreaView |
Column({ space: 0 }).safeArea(true) |
安全区域适配,避免状态栏遮挡 |
ScrollView |
Scroll() |
滚动容器完全等效 |
TouchableOpacity |
Button().stateEffect(true) |
点击反馈通过 stateEffect 实现 |
View(布局) |
Column/Row |
根据布局方向选择,Flex 布局属性完全兼容 |
Text |
Text |
文本样式、颜色、大小完全复用 |
| 进度条滑块(绝对定位) | Stack + Position |
通过 Stack 嵌套 + position 实现滑块绝对定位 |
| 播放列表循环渲染 | ForEach |
替代 RN 的 map,支持虚拟化列表渲染 |
| 样式动态绑定 | 链式样式 + 条件判断 | width: ${this.progress}% → .width(this.progress + '%') |
进度条组件
// RN 进度条
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${progress}%` }]} />
<TouchableOpacity
style={[styles.progressThumb, { left: `${progress}%` }]}
onPress={(e) => {/* 跳转逻辑 */}}
/>
</View>
// 鸿蒙进度条适配
Stack({ alignContent: Alignment.Center }) {
// 进度条底色
Row()
.width('100%')
.height(4)
.backgroundColor('#cbd5e1')
.borderRadius(2);
// 已播放进度
Row()
.width(this.progress + '%')
.height(4)
.backgroundColor('#3b82f6')
.borderRadius(2);
// 进度滑块
Button()
.width(16)
.height(16)
.borderRadius(8)
.backgroundColor('#3b82f6')
.position({ x: this.progress + '%', y: -6 })
.onClick((e) => {
// 点击位置计算(等效RN的nativeEvent.locationX)
const x = e.localPos.x;
const percentage = (x / 300) * 100;
this.progress = Math.min(100, Math.max(0, percentage));
});
}
.marginHorizontal(8)
.flex(1);
3. 交互体验
RN 的交互逻辑在鸿蒙中可完全复用,仅需调整事件绑定方式:
- RN
onPress→ 鸿蒙onClick; - RN
Alert.alert→ 鸿蒙AlertDialog.show; - RN 触摸事件
nativeEvent→ 鸿蒙localPos; - RN 样式条件绑定 → 鸿蒙链式样式条件判断。
播放控制按钮
// RN 播放按钮
<TouchableOpacity style={styles.playButton} onPress={togglePlay}>
<Text style={styles.playIcon}>{isPlaying ? '⏸️' : '▶️'}</Text>
</TouchableOpacity>
// 鸿蒙播放按钮适配
Button()
.width(60)
.height(60)
.borderRadius(30)
.backgroundColor('#3b82f6')
.justifyContent(FlexAlign.Center)
.onClick(() => this.togglePlay()) {
Text(this.isPlaying ? '⏸️' : '▶️')
.fontSize(28)
.fontColor('#ffffff');
}
.marginHorizontal(20);
4. 样式系统
RN 的 StyleSheet.create 样式在鸿蒙中通过链式样式 + @Styles 装饰器实现复用:
// 鸿蒙通用样式封装
@Styles
progressBarStyle() {
.height(4)
.borderRadius(2)
.backgroundColor('#cbd5e1');
}
@Styles
activeControlStyle() {
.fontColor('#3b82f6');
}
// 组件使用
Row()
.progressBarStyle() // 复用通用样式
.width('100%');
Text('🔀')
.fontSize(24)
.applyIf(this.shuffle, this.activeControlStyle()); // 条件样式
适配优势:
- 样式复用:通过
@Styles封装通用样式,减少代码冗余; - 条件样式:
applyIf替代 RN 的样式数组,语法更简洁; - 单位适配:百分比、像素单位完全兼容,无需转换;
- 色彩一致:保持与 RN 相同的色值,保证视觉体验一致。
这款 React Native 音乐播放器的跨端适配实践,验证了 ArkTS 与 React 技术体系的高度兼容性。对于音乐类应用而言,核心的播放逻辑、进度管理、列表交互均可实现 90% 以上的代码复用,仅需适配 UI 组件和原生 API 调用,是跨端开发的高效路径。
真实演示案例代码:
// app.tsx
import React, { useState, useEffect } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, Image } from 'react-native';
// Base64 图标库
const ICONS_BASE64 = {
home: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
music: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
pause: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
skipPrev: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
skipNext: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
volume: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
shuffle: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};
const { width, height } = Dimensions.get('window');
// 音乐类型
type MusicTrack = {
id: string;
title: string;
artist: string;
album: string;
duration: string;
cover: string;
isFavorite: boolean;
};
const MusicPlayerApp: React.FC = () => {
const [currentTrack, setCurrentTrack] = useState<MusicTrack>({
id: '1',
title: '稻香',
artist: '周杰伦',
album: '魔杰座',
duration: '3:45',
cover: '',
isFavorite: true,
});
const [isPlaying, setIsPlaying] = useState<boolean>(true);
const [currentTime, setCurrentTime] = useState<number>(0);
const [volume, setVolume] = useState<number>(80);
const [progress, setProgress] = useState<number>(30); // 模拟进度百分比
const [playlist] = useState<MusicTrack[]>([
{ id: '1', title: '稻香', artist: '周杰伦', album: '魔杰座', duration: '3:45', cover: '', isFavorite: true },
{ id: '2', title: '告白气球', artist: '周杰伦', album: '周杰伦的床边故事', duration: '3:34', cover: '', isFavorite: false },
{ id: '3', title: '江南', artist: '林俊杰', album: '第二天', duration: '4:02', cover: '', isFavorite: true },
{ id: '4', title: '曹操', artist: '林俊杰', album: '曹操', duration: '4:28', cover: '', isFavorite: false },
{ id: '5', title: '淘汰', artist: '陈奕迅', album: '认了吧', duration: '3:58', cover: '', isFavorite: true },
{ id: '6', title: '十年', artist: '陈奕迅', album: '黑白灰', duration: '3:25', cover: '', isFavorite: false },
{ id: '7', title: 'Love Story', artist: 'Taylor Swift', album: 'Fearless', duration: '3:55', cover: '', isFavorite: true },
{ id: '8', title: 'Perfect', artist: 'Ed Sheeran', album: 'Divide', duration: '4:23', cover: '', isFavorite: true },
]);
const [repeatMode, setRepeatMode] = useState<'off' | 'one' | 'all'>('all');
const [shuffle, setShuffle] = useState<boolean>(false);
// 模拟播放进度
useEffect(() => {
let interval: NodeJS.Timeout;
if (isPlaying) {
interval = setInterval(() => {
setProgress(prev => {
if (prev >= 100) {
handleNext(); // 播放下一首
return 0;
}
return prev + 0.5; // 模拟进度增加
});
}, 1000);
}
return () => clearInterval(interval);
}, [isPlaying]);
// 播放/暂停
const togglePlay = () => {
setIsPlaying(!isPlaying);
};
// 上一首
const handlePrevious = () => {
const currentIndex = playlist.findIndex(track => track.id === currentTrack.id);
const previousIndex = (currentIndex - 1 + playlist.length) % playlist.length;
setCurrentTrack(playlist[previousIndex]);
setProgress(0);
};
// 下一首
const handleNext = () => {
const currentIndex = playlist.findIndex(track => track.id === currentTrack.id);
const nextIndex = (currentIndex + 1) % playlist.length;
setCurrentTrack(playlist[nextIndex]);
setProgress(0);
};
// 切换收藏状态
const toggleFavorite = () => {
setCurrentTrack(prev => ({
...prev,
isFavorite: !prev.isFavorite
}));
};
// 切换重复模式
const toggleRepeat = () => {
const modes: Array<'off' | 'one' | 'all'> = ['off', 'one', 'all'];
const currentIndex = modes.indexOf(repeatMode);
const nextIndex = (currentIndex + 1) % modes.length;
setRepeatMode(modes[nextIndex]);
};
// 切换随机播放
const toggleShuffle = () => {
setShuffle(!shuffle);
};
// 跳转到指定时间
const seekTo = (position: number) => {
setProgress(position);
};
return (
<SafeAreaView style={styles.container}>
{/* 顶部信息栏 */}
<View style={styles.topBar}>
<Text style={styles.topBarText}>现在播放: {currentTrack.title}</Text>
</View>
<ScrollView style={styles.content}>
{/* 专辑封面 */}
<View style={styles.coverContainer}>
<View style={styles.coverArt}>
<Text style={styles.coverText}>🎵</Text>
</div>
<Text style={styles.albumTitle}>{currentTrack.album}</Text>
</div>
{/* 歌曲信息 */}
<View style={styles.songInfo}>
<Text style={styles.songTitle}>{currentTrack.title}</Text>
<Text style={styles.songArtist}>{currentTrack.artist}</Text>
</div>
{/* 播放进度条 */}
<View style={styles.progressContainer}>
<Text style={styles.timeText}>{formatTime(progress * 2.2)}</Text>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{ width: `${progress}%` }
]}
/>
<TouchableOpacity
style={[
styles.progressThumb,
{ left: `${progress}%` }
]}
onPress={(e) => {
const x = e.nativeEvent.locationX;
const percentage = (x / 300) * 100;
seekTo(Math.min(100, Math.max(0, percentage)));
}}
/>
</div>
<Text style={styles.timeText}>{currentTrack.duration}</Text>
</div>
{/* 控制按钮 */}
<View style={styles.controlsContainer}>
<TouchableOpacity
style={styles.controlButton}
onPress={toggleShuffle}
>
<Text style={[
styles.controlIcon,
shuffle && styles.activeControl
]}>🔀</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={handlePrevious}
>
<Text style={styles.controlIcon}>⏮️</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.playButton}
onPress={togglePlay}
>
<Text style={styles.playIcon}>{isPlaying ? '⏸️' : '▶️'}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={handleNext}
>
<Text style={styles.controlIcon}>⏭️</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={toggleRepeat}
>
<Text style={[
styles.controlIcon,
repeatMode !== 'off' && styles.activeControl
]}>
{repeatMode === 'one' ? '🔂' : '🔁'}
</Text>
</TouchableOpacity>
</div>
{/* 音量控制 */}
<View style={styles.volumeContainer}>
<Text style={styles.volumeIcon}>🔈</Text>
<View style={styles.volumeSlider}>
<View
style={[
styles.volumeFill,
{ width: `${volume}%` }
]}
/>
</View>
<Text style={styles.volumeIcon}>🔊</Text>
</div>
{/* 操作按钮 */}
<View style={styles.actionButtons}>
<TouchableOpacity
style={styles.actionButton}
onPress={toggleFavorite}
>
<Text style={styles.actionIcon}>{currentTrack.isFavorite ? '❤️' : '🤍'}</Text>
<Text style={styles.actionText}>收藏</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => Alert.alert('分享', `分享歌曲: ${currentTrack.title}`)}
>
<Text style={styles.actionIcon}>📤</Text>
<Text style={styles.actionText}>分享</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => Alert.alert('下载', `下载歌曲: ${currentTrack.title}`)}
>
<Text style={styles.actionIcon}>📥</Text>
<Text style={styles.actionText}>下载</Text>
</TouchableOpacity>
</div>
{/* 播放列表 */}
<View style={styles.playlistContainer}>
<Text style={styles.playlistTitle}>播放列表 ({playlist.length})</Text>
{playlist.map((track, index) => (
<TouchableOpacity
key={track.id}
style={[
styles.playlistItem,
currentTrack.id === track.id && styles.currentTrack
]}
onPress={() => {
setCurrentTrack(track);
setProgress(0);
}}
>
<View style={styles.playlistCover}>
<Text style={styles.playlistCoverText}>🎵</Text>
</div>
<View style={styles.playlistInfo}>
<Text
style={[
styles.playlistTitleText,
currentTrack.id === track.id && styles.currentTrackText
]}
>
{track.title}
</Text>
<Text style={styles.playlistArtist}>{track.artist}</Text>
</div>
<Text style={styles.playlistDuration}>{track.duration}</Text>
</TouchableOpacity>
))}
</div>
{/* 使用说明 */}
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>使用说明</Text>
<Text style={styles.infoText}>• 点击播放/暂停按钮控制播放</Text>
<Text style={styles.infoText}>• 使用上下一首按钮切换歌曲</Text>
<Text style={styles.infoText}>• 点击进度条可以快进/快退</Text>
<Text style={styles.infoText}>• 点击心形图标收藏歌曲</Text>
</View>
</ScrollView>
{/* 底部导航 */}
<View style={styles.bottomNav}>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('首页')}
>
<Text style={styles.navIcon}>🏠</Text>
<Text style={styles.navText}>首页</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('搜索')}
>
<Text style={styles.navIcon}>🔍</Text>
<Text style={styles.navText}>搜索</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('播放列表')}
>
<Text style={styles.navIcon}>🎵</Text>
<Text style={styles.navText}>播放列表</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('我的')}
>
<Text style={styles.navIcon}>👤</Text>
<Text style={styles.navText}>我的</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
// 格式化时间(秒转 MM:SS)
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
topBar: {
backgroundColor: '#3b82f6',
padding: 10,
alignItems: 'center',
},
topBarText: {
color: '#ffffff',
fontSize: 14,
fontWeight: '500',
},
content: {
flex: 1,
padding: 16,
},
coverContainer: {
alignItems: 'center',
marginBottom: 24,
},
coverArt: {
width: width * 0.7,
height: width * 0.7,
backgroundColor: '#e2e8f0',
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
coverText: {
fontSize: 80,
},
albumTitle: {
fontSize: 16,
color: '#64748b',
fontWeight: '500',
},
songInfo: {
alignItems: 'center',
marginBottom: 24,
},
songTitle: {
fontSize: 24,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 8,
},
songArtist: {
fontSize: 18,
color: '#64748b',
},
progressContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 24,
},
timeText: {
fontSize: 12,
color: '#64748b',
minWidth: 30,
},
progressBar: {
flex: 1,
height: 4,
backgroundColor: '#cbd5e1',
borderRadius: 2,
marginHorizontal: 8,
position: 'relative',
},
progressFill: {
height: '100%',
backgroundColor: '#3b82f6',
borderRadius: 2,
},
progressThumb: {
position: 'absolute',
top: -6,
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: '#3b82f6',
},
controlsContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
},
controlButton: {
paddingHorizontal: 16,
},
controlIcon: {
fontSize: 24,
},
activeControl: {
color: '#3b82f6',
},
playButton: {
backgroundColor: '#3b82f6',
width: 60,
height: 60,
borderRadius: 30,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 20,
},
playIcon: {
fontSize: 28,
color: '#ffffff',
},
volumeContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 24,
},
volumeIcon: {
fontSize: 18,
marginHorizontal: 8,
},
volumeSlider: {
flex: 1,
height: 4,
backgroundColor: '#cbd5e1',
borderRadius: 2,
},
volumeFill: {
height: '100%',
backgroundColor: '#3b82f6',
borderRadius: 2,
},
actionButtons: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 24,
},
actionButton: {
alignItems: 'center',
},
actionIcon: {
fontSize: 24,
marginBottom: 4,
},
actionText: {
fontSize: 12,
color: '#64748b',
},
playlistContainer: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 16,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
playlistTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 12,
},
playlistItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
},
currentTrack: {
backgroundColor: '#eff6ff',
},
playlistCover: {
marginRight: 12,
},
playlistCoverText: {
fontSize: 24,
},
playlistInfo: {
flex: 1,
},
playlistTitleText: {
fontSize: 16,
color: '#1e293b',
fontWeight: '500',
},
currentTrackText: {
color: '#3b82f6',
},
playlistArtist: {
fontSize: 14,
color: '#64748b',
},
playlistDuration: {
fontSize: 14,
color: '#64748b',
},
infoCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginTop: 16,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
infoTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 12,
},
infoText: {
fontSize: 14,
color: '#64748b',
lineHeight: 22,
marginBottom: 8,
},
bottomNav: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#ffffff',
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
paddingVertical: 12,
},
navItem: {
alignItems: 'center',
flex: 1,
},
navIcon: {
fontSize: 20,
color: '#94a3b8',
marginBottom: 4,
},
navText: {
fontSize: 12,
color: '#94a3b8',
},
});
export default MusicPlayerApp;

打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

本文详细探讨了一个基于React Native的音乐播放器应用实现方案。该方案采用清晰的单组件架构,通过useState管理播放状态、进度、音量等核心数据,实现了播放控制、进度管理、播放模式切换等完整功能。应用使用TypeScript定义数据类型,确保代码安全性和可维护性。界面采用垂直滚动布局,符合用户习惯。针对跨平台适配,采用React Native核心组件、统一样式定义等策略。文章还提出了性能优化建议,如使用requestAnimationFrame替代setInterval,以及使用useReducer管理复杂状态。该实现展示了如何在React Native中构建功能完备、跨平台兼容的音乐播放器应用。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐


所有评论(0)