赞
踩
/ 今日科技快讯 /
近日,苹果首席执行官蒂姆·库克接受《时代》杂志专访,谈及他本人对领导力、企业价值和新技术的看法。库克表示,苹果不仅要引领创新,还要努力让世界变得更安全更公平,而这一点很重要。
在谈到令人兴奋的新技术时,库克表示自己对人工智能很感兴趣。他说,“我对人工智能很感兴趣。如今,人工智能已经出现在许多你根本想不到的产品中。从我们识别用户面部、指纹的方式,归纳整理照片的方式,再到Siri的工作方式都是如此。关于人工智能能为人们做什么,如何让人们生活更轻松,实际上我们还处在早期阶段。”
/ 作者简介 /
大家周三好,这周已然过半,新的一周继续加油吧!
本篇文章来自半夏微凉的投稿,文章主要分享了如何在HarmonyOS中实现基于JS卡片的音乐播放器,相信会对对鸿蒙感兴趣的朋友们有所帮助!同时也感谢作者贡献的精彩文章。
半夏微凉的博客地址:
https://developer.huawei.com/consumer/cn/personalcenter/myCommunity/communityBlog?uid=5747c32f82a141bf8af5febde6b6af2f&siteId=1
/ 文章简介 /
这是一款基于JS卡片打造的音乐播放卡片应用,我们可以通过桌面卡片来获取音乐播放的信息,也可以进行播放、暂停、歌曲切换等功能。
2X2卡片展示歌曲封面、播放状态、播放进度、歌曲名称等信息。
4X4卡片增加歌词展示。
通过卡片即可操作音乐播放、暂停、切换等操作。
/ 搭建HarmonyOS环境 /
安装DevEco Studio,详情请参考DevEco Studio下载。
设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,可以根据如下两种情况来配置开发环境:
如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作
如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境
本程序需要在真机运行,需要提前申请证书:
准备密钥和证书请求文件
申请调试证书
/ 音乐卡片开发 /
卡片的功能有三部分:
信息展示
页面跳转
数据交互
因此我们可以实现的功能有:
歌曲名称、歌手名称、歌曲封面等信息展示
卡片跳转至播放器主页,并且数据同步
播放、暂停、歌曲切换
播放进度展示
确定了我们要实现的功能后,再进行Java卡片与JS卡片选型,如图所示,java卡片支持的组件比较少,无法满足我们的功能需求。
JS卡片功能更强大,支持的组件也更多,可以实现我们的开发需求。
包名 | 功能 |
bean | 歌词、柱状图数据实体类 |
customcomponent | 自定义组件柱状图 |
database | 数据库和数据表 |
provider | 列表数据提供者 |
service | 播放器服务类 |
slice | 启动页和主页面 |
utils | 数据库、log、歌词解析、线程切换工具类 |
工具为我们提供了多个模板,我们选择一个音乐播放模板。
创建完成后,config.json文件中会自动生成卡片配置的参数,我们可以在此处设置卡片的名称、规格、类型等参数。
Java Ability中的jsComponentName和js模块下的name相对应。
创建完成后有三个文件,js文件为卡片提供数据,hml文件编写布局属性,css文件编写控件样式。
js为卡片提供数据,配置跳转事件和message事件。
- export default {
- data: {
-
- //歌曲封面
- picName: "/common/music.png",
-
- //进度条的值
- progressValue: 0,
-
- //按钮点击发送message
- last: "last",
- play: "play",
- next: "next",
-
- //歌曲名
- songName: "未播放",
-
- //歌手名
- singer: "未知歌手",
-
- //歌词列表
- lyric: [{
- fontColor: "#9C9C9C",
- fontSize: "14px",
- Lrc: "",
- }],
-
- actions: {
- //跳转事件
- routerEvent: {
- action: "router",
- bundleName: "com.example.wryproject",
- abilityName: "com.example.wryproject.MusicAbility"
- },
-
- //message事件
- lastEvent: {
- action: "message",
- params: {
- message: this.last
- }
- },
- playEvent: {
- action: "message",
- params: {
- message: this.play
- }
- },
- nextEvent: {
- action: "message",
- params: {
- message: this.next
- }
- },
- }
- }

html代码
- <div class="container">
- <div class="title_container">
- <image src="{{ picName }}" @click="routerEvent" class="music-img"></image>
- <div class="songData">
- <text class="songName">{{ songName }}</text>
- <text class="singer">{{ singer }}</text>
- </div>
- </div>
-
- <list class="lyric_list">
- <list-item class="lyric_list_item" for="{{ lyric }}">
- <text class="lyric_text" style="color : {{ $item.fontColor }}; font-size : {{ $item.fontSize }};">{{
- $item.Lrc }}</text>
- </list-item>
- </list>
-
- <div class="button_container">
- <progress class="progress" type="ring" percent="{{ progressValue }}">
- </progress>
- <image src="{{ lastImage }}" @click="lastEvent" class="last-img"></image>
- <image src="{{ playImage }}" @click="playEvent" class="play-img"></image>
- <image src="{{ nextImage }}" @click="nextEvent" class="next-img"></image>
- </div>
- </div>

css代码较长,这里只贴出一部分:
- .container {
- width: 100%;
- height: 100%;
- }
-
- .title_container {
- height: 70%;
- width: 100%;
- flex-direction: row;
- position: absolute;
- top: 5;
- }
-
- .music-img {
- background-color: #0F000000;
- height: 50%;
- width: 40%;
- object-fit: contain;
- border-radius: 10px;
- margin-left: 8%;
- margin-top: 8%;
- position: absolute;
- }
-
- .songData {
- height: 50%;
- width: 50%;
- position: absolute;
- flex-direction: column;
- align-items: center;
- padding-right: 10%;
- right: 3%;
- top: 8%;
- }

初始预览效果:
JS界面已经准备好了,接下来我们实现音乐播放功能,音频播放的实现不是我们文章的重点,所以下面我只大致分享一下实现流程,感兴趣的小伙伴可以到文章末尾获取项目代码。
通过AVStorage,我们可以获取到本地歌曲的地址、名称、时长等信息,但很遗憾,AVStorage目前没有歌手信息,当我以为项目功能又要“裁剪”时,我想到大部分音频文件的地址是包含歌手信息的,我们将音频地址打印出来(如下图),这样我们就可以通过字符串截取,来拿到歌手信息了,然后将音频数据保存至数据库中,此项目使用了对象关系映射数据库 ,它可以让开发者不必再去编写复杂的SQL语句, 以操作对象的形式来操作数据库,相当友好呢。
- DataAbilityHelper helper = DataAbilityHelper.creator(this);
- try {
- ResultSet resultSet = helper.query(AVStorage.Audio.Media.EXTERNAL_DATA_ABILITY_URI, null, null);
- //通过while循环拿到所有音频数据
- while (resultSet != null && resultSet.goToNextRow()) {
- //音乐ID
- int musicId = resultSet.getInt(resultSet.getColumnIndexForName(AVStorage.AVBaseColumns.ID));
- //音频地址
- String musicPath = resultSet.getString(resultSet.getColumnIndexForName(AVStorage.AVBaseColumns.DATA));
- //音频名称
- String musicTitle = resultSet.getString(resultSet.getColumnIndexForName(AVStorage.AVBaseColumns.TITLE));
- //音频时常
- int musicDuration = resultSet.getInt(resultSet.getColumnIndexForName(AVStorage.AVBaseColumns.DURATION));
- //AVStorage自带属性中暂无歌手信息,但musicPath中包含歌手信息,此处将歌手名称截取出来
- int startIndex = musicPath.lastIndexOf("/");
- int endIndex = musicPath.lastIndexOf("-");
- String singer = "未知歌手";
- if (startIndex != -1 && endIndex != -1) {
- if (endIndex < startIndex) {
- endIndex = musicPath.lastIndexOf(".");
- }
- singer = musicPath.substring(startIndex + 1, endIndex - 1);
- }
- //过滤小于10秒的音频
- if (musicDuration > 10000) {
- //将音频数据插入数据库
- }
- }
- resultSet.close();

音频封面的获取比较消耗性能,而且获取项目内音频封面和获取本地音频封面的方法不同,我将它单独放在一个方法里实现。
封面获取主要用到AVMetadataHelper对象,官网示例链接:获取音频的图像数据的开发步骤。
获取音频的图像数据的开发步骤:
https://developer.harmonyos.com/cn/docs/documentation/doc-guides/media-data-mgmt-obtaining-0000001050751061
- Uri uri = Uri.appendEncodedPathToUri(AVStorage.Audio.Media.EXTERNAL_DATA_ABILITY_URI, stringId);
- fileDescriptor = helper.openFile(uri, "r");
-
- //通过文件描述符获取封面
- avMetadataHelper.setSource(fileDescriptor);
- byte[] data = avMetadataHelper.resolveImage();
通过AVStorage.Audio.Media.EXTERNAL_DATA_ABILITY_URI和音频ID,我们拿到了音频的Uri,helper.openFile(uri, "r")拿到文件描述符,调用avMetadataHelper.resolveImage方法拿到字节数组,我们就可以创建PixelMap对象了(见下图):
- /**
- * 通过MP3文件的数据流获取专辑封面
- *
- * @return 封面
- */
- public PixelMap getPixelMapCover() {
- OrmContext ormContext = DatabaseUtils.getOrmContext(getContext());
- OrmPredicates ormPredicates = ormContext.where(MusicData.class);
- List<MusicData> musicDataList = ormContext.query(ormPredicates);
- MusicData musicData = musicDataList.get(currentPosition);
- Blob musicCover = musicData.getMusicCover();
- PixelMap pixelMap = null;
- if (musicCover != null && musicCover.length() != 0) {
- byte[] bytes = musicCover.getBytes(1, Math.toIntExact(musicCover.length()));
- ImageSource imageSource = ImageSource.create(bytes, null);
-
- //普通解码createPixelMap传入的DecodingOptions 设置为null
- pixelMap = imageSource.createPixelmap(null);
- }
- return pixelMap;
- }

歌词是通过网络获取(资源网站地址+歌曲名+歌手名),拿到歌词地址数据。
最终拿到歌词lrc文件,解析为歌词列表,免费的资源毕竟是少数,所以这个网站只有传唱度高的歌曲才有歌词资源。
歌词解析代码较多,见项目LyricAnalysisUtil类。
第一次点击播放就随机设置资源,如果正在播放就暂停,反之就播放,然后更新列表播放状态。
- /**
- * 播放音乐
- */
- public void playMusic() {
- if (currentPosition == -1) {
- randomPlayer(musicDataList.size());
- } else if (player.isNowPlaying()) {
- player.pause();
- } else {
- player.play();
- }
- musicService.notice();
- if (MusicAbilitySlice.getProvider() != null) {
- ThreadUtil.runUI(() -> MusicAbilitySlice.getProvider()
- .updatePlayerState(-1, currentPosition, player.isNowPlaying()));
- }
- }

调整播放列表的pisition,实现播放资源的切换。
- /**
- * 播放上一曲
- */
- public void lastMusic() {
- if (currentPosition == -1) {
- //未设置歌曲资源弹出dialog提示
- new ToastDialog(mContext)
- .setAlignment(LayoutAlignment.CENTER)
- .setText("请选择需要播放的歌曲~")
- .show();
- } else if (currentPosition == 0) {
- //提示用户已经是第一首了
- new ToastDialog(mContext)
- .setAlignment(LayoutAlignment.CENTER)
- .setText("已经是第一首了~")
- .show();
- } else {
- //播放上一首
- currentPosition -= 1;
- setSource();
- }
- }

- /**
- * 根据传入的数值区间,生成随机数,并播放
- *
- * @param end 随机数的最大区间
- */
- public void randomPlayer(int end) {
- Random random = new Random();
- currentPosition = random.nextInt(end);
- setSource();
- }
- /**
- * 重置播放器,设置资源重新播放
- */
- public void setSource() {
- //判断地址是raw文件下资源还是本地资源
- if (!path.contains("Music")) {
- RawFileDescriptor rawFileDescriptor;
- try {
- rawFileDescriptor = getResourceManager().getRawFileEntry(path).openRawFileDescriptor();
- player.setSource(rawFileDescriptor);
- } catch (IOException e) {
- e.printStackTrace();
- }
- } else {
- source = new Source(path);
- player.setSource(source);
- }
- player.prepare();
- player.play();
- }

接下来就是本文的主题了:如何将获取到的数据显示到卡片上?
我们通过IDE创建卡片时,会自动绑定MainAbility,生成卡片的回调方法。
在onCreateForm方法中我们要将卡片ID保存至数据库,后面将会使用ID来进行卡片更新。
- @Override
- protected ProviderFormInfo onCreateForm(Intent intent) {
- if (intent == null) {
- return new ProviderFormInfo();
- }
- //返回主界面后musicRemoteObject就会为空,此时操作卡片就需要重新获取musicRemoteObject对象
- if (musicRemoteObject == null) {
- musicRemoteObject = MusicServiceAbility.get();
- }
- // 获取卡片id
- long cardId = INVALID_CARD_ID;
- if (intent.hasParameter(AbilitySlice.PARAM_FORM_IDENTITY_KEY)) {
- cardId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_CARD_ID);
- }
- ProviderFormInfo providerFormInfo = new ProviderFormInfo();
-
- // 获取卡片规格
- int dimension = DEFAULT_DIMENSION_2X2;
- if (intent.hasParameter(AbilitySlice.PARAM_FORM_DIMENSION_KEY)) {
- dimension = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, DEFAULT_DIMENSION_2X2);
- }
- CardData cardData = new CardData(cardId, dimension);
- DatabaseUtils.insertCardData(cardData, DatabaseUtils.getOrmContext(this));
- musicRemoteObject.updateCardCover();
- musicRemoteObject.lrcLoading("暂无歌词数据");
- providerFormInfo.setJsBindingData(new FormBindingData());
- return providerFormInfo;
- }

onDeleteForm方法是卡片删除时的回调,所以我们要同步删除数据库中对应的卡片ID。
- @Override
- protected void onUpdateForm(long formId) {
- HiLog.info(TAG, "onUpdateForm");
- super.onUpdateForm(formId);
- }
-
- @Override
- protected void onDeleteForm(long formId) {
- HiLog.info(TAG, "onDeleteForm: formId=" + formId);
- super.onDeleteForm(formId);
- DatabaseUtils.deleteCardData(formId, DatabaseUtils.getOrmContext(this));
- }
onTriggerFormEvent方法用来接收卡片的message 事件,卡片的播放、暂停、歌曲切换都是通过这个方法来进行交互。
根据卡片传过来的字符串对象判断下一步操作。
- @Override
- protected void onTriggerFormEvent(long formId, String message) {
- super.onTriggerFormEvent(formId, message);
-
- //返回主界面后musicRemoteObject就会为空,此时操作卡片就需要重新获取musicRemoteObject对象
- if (musicRemoteObject == null) {
- musicRemoteObject = MusicServiceAbility.get();
- }
- String musicMessage = (String) ZSONObject.stringToZSON(message).get("message");
- switch (musicMessage) {
- case "last":
- musicRemoteObject.lastMusic();
- break;
- case "play":
- //当用户杀死应用时,player就会为null,此时点击卡片播放,需做判空处理
- if (musicRemoteObject.getPlayer() == null) {
- musicRemoteObject.initPlayer(this);
- }
- musicRemoteObject.playMusic();
- break;
- case "next":
- musicRemoteObject.nextMusic();
- break;
- }
- musicRemoteObject.updateCardCover();
- }

ZSONObject设置卡片数据, zsonObject.put("songName", title)方法,第一个参数相当于key,要和JS文件的数据名称相对应,第二个参数为要传入的数据。
- /**
- * 设置卡片数据
- */
- private ZSONObject setCardData() {
- // 设置卡片页面需要的数据
- if (ormContext == null) {
- ormContext = DatabaseUtils.getOrmContext(musicService.getContext());
- }
- ZSONObject zsonObject = new ZSONObject();
- int position = getCurrentPosition();
- //如果player没有设置资源,直接return
- if (position == -1) {
- return null;
- } else {
- //player设置了资源,将卡片按钮点亮
- zsonObject.put("lastImage", "/common/last.png");
- zsonObject.put("nextImage", "/common/next.png");
- }
- //根据播放状态设置卡片播放或暂停
- if (getIsPlay()) {
- zsonObject.put("playImage", "/common/pause.png");
- } else {
- zsonObject.put("playImage", "/common/play.png");
- }
- //设置卡片title
- String title = DatabaseUtils.queryMusicData(position, ormContext).getMusicTitle();
- String singer = DatabaseUtils.queryMusicData(position, ormContext).getSinger();
- zsonObject.put("songName", title);
- zsonObject.put("singer", singer);
- return zsonObject;
- }

在歌曲播放、切换时设置卡片封面。
此处有一个知识点 --> JS卡片如何设置本地图片或网络图片?官方文档给出了示例:通过内存图片方式使用image组件。他的核心就是如下两个方法:
zsonObject.put("图片名称", 图片地址)
formBindingData.addImageData(图片名称, 图片字节数据)
重点就是我们要拿到图片的数据流,并添加到 "memory://" 这个内存地址。
代码
- /**
- * 更新所有卡片封面
- */
- public void updateCardCover() {
- ThreadUtil.runWork(() -> {
- ZSONObject zsonObject = setCardData();
- List<CardData> cardDataList = DatabaseUtils.queryAllCardData(ormContext);
- FormBindingData formBindingData = new FormBindingData(zsonObject);
- if (getCardCover() != null) {
- int musicId = getMusicList().get(currentPosition).getMusicId();
- String picName = musicId + ".png";
- String picPath = "memory://" + picName;
- assert zsonObject != null;
- zsonObject.put("picName", picPath);
- formBindingData.addImageData(picName, getCardCover());
- }
- //遍历卡片列表更新所有卡片
- for (CardData cardData : cardDataList) {
- try {
- updateForm(cardData.getCardId(), formBindingData);
- } catch (FormException e) {
- DatabaseUtils.deleteCardData(cardData.getCardId(), ormContext);
- }
- }
- });
- }

音乐开始播放后就开启一个Timer计时器来更新播放进度和歌词。
- /**
- * 播放进度的更新
- */
- private void playListener() {
- EventRunner mainEventRunner = EventRunner.getMainEventRunner();
- eventHandler = new EventHandler(mainEventRunner) {
- @Override
- protected void processEvent(InnerEvent event) {
- super.processEvent(event);
- //更新播放进度和歌词
- }
- }
- };
- if (timer != null) {
- timer.cancel();
- timer = null;
- }
- timer = new Timer();
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- InnerEvent innerEvent = InnerEvent.get();
- innerEvent.param = getPlayer().getCurrentTime();
- eventHandler.sendEvent(innerEvent);
- }
- }, 0, 200);
- }

/ 开发总结 /
此项目的开发,使我们掌握了:
JS卡片的布局编写:JS编写布局相对于JAVA更加高效。例如 JS 组件list,只需三五行代码即可实现列表的基本展示,大大提升了我们的开发效率,JS+JAVA的混合开发模式是一个很不错的开发方案。
卡片的跳转和交互:通过卡片快速直达我们需要的页面,减少层级交互,应用的使用场景更加丰富,操作也更加便捷高效。
卡片数据的实时更新:卡片成为应用信息展示的直接载体,真正做到“信息直达”,只需解锁手机即可获取到我们需要的信息。
Player的简单使用:音视频播放作为手机的常用功能之一,有相当多的用武之地,掌握Player的使用是各位开发者的必经之路。
本地音频资源的获取:数据是一个应用的基础,学会合理使用这些数据、资源也是各位开发者需要掌握的基础。
开发过程中遇到的问题:
直接传入音频地址封面获取失败
问题原因:传入的地址中有中文,资源解析失败。
- AVMetadataHelper avMetadataHelper= new AVMetadataHelper();
- avMetadataHelper.setSource("/path/冬天的秘密.mp4");
解决方案:通过传入文件描述符获取封面。
- Uri uri = Uri.appendEncodedPathToUri(AVStorage.Audio.Media.EXTERNAL_DATA_ABILITY_URI, stringId);
- fileDescriptor = helper.openFile(uri, "r");
- //通过文件描述符获取封面
- avMetadataHelper.setSource(fileDescriptor);
- byte[] data = avMetadataHelper.resolveImage();
网络获取歌词失败
问题原因:需要添加网络明文请求的设置。
解决方案:在config.json中添加网络明文请求设置为true
卡片封面更新无效
问题原因:只有卡片出现在桌面时,调用updateForm方法才可以更新封面。
解决方案:考虑到定时器中更新封面代价较大,我们可以分为两种方案:
① 计时器定时更新文字、进度条等占用性能较小的组件。
② 返回桌面时应用失去焦点,在onInactive方法中更新卡片的图片数据即可减少性能损耗。
卡片歌词列表如何实现
问题原因:卡片中的组件是无法调用其方法的,所以无法控制列表滚动。
解决方案:定时刷新列表数据,我们只给列表设置5条数据,然后定时更新不同的歌词传入列表,达到列表伪滚动效果。
音乐在后台播放,几秒钟后音乐自动暂停,进入应用后音乐又开始自动播放。
问题原因:鸿蒙的应用启动管理默认为系统自动管理,有时会将应用后台冻结。
解决方案:在手机自带的手机管家-->应用启动管理-->找到自己的应用关闭自动管理-->打开允许后台活动,应用就不会被冻结了。
以上就是本人遇到的一些问题,希望可以给各位开发者提供参考。由于篇幅有限,文章有很多细节并不能面面俱到,感兴趣的小伙伴可以通过文章末尾链接下载项目,文中写的不好的地方欢迎大家提出建议,大家也可以在评论区多多交流,共同进步~
代码地址:
https://gitee.com/WRY666/ohos_-music-card
推荐阅读:
Android 12 SplashScreen API快速入门
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。