不知道有没有同学遇到过这种场景,和好朋友一起出去游玩时,坐在地铁或公交车上,每当听到一首好听的歌曲,总是忍不住和身边的伙伴一起分享,于是和同学每人戴着一只耳机共用一只手机听,这种方式难免会影响听歌的质感。在这种情况下,想到了使用服务流转开发一款“一起听吧”app,这样就可以和身边的朋友一起听好听的音乐了,可以同时间体会到的高低起伏优美动听的音乐,还能进行歌曲切换,是不是还挺不错。
言归正传,接下来就开始介绍“一起听吧”应用开发的功能。 该应用使用服务流转方式实现,流转:在HarmonyOS中泛指多设备分布式操作。流转能力打破设备界限,多设备联动,使用户应用程序可分可合、可流转。流转按照体验可分为跨端迁移(指在A端运行的FA迁移到B端上,完成迁移后, B端FA继续任务,而A端应用退出)和多端协同(指多端上的不同FA/PA同时运行、或者交替运行实现完整的业务)。可以说开发者站在了巨人的肩膀上创造更多可能,在此对HarmonyOS系统工程师们表示最真挚的崇拜。
开发准备
- 万丈高楼平地起, 开始前请参考下载与安装软件、配置开发环境,完成DevEco Studio的安装和开发环境配置。
- 开发环境配置完成后,请参考创建和运行Hello World创建工程
接下来就从创建工程项目开始介绍
创建一个新工程
- 打开DevEco Studio,在欢迎页点击Create Project,创建一个新工程。
- 根据工程创建向导,选择需要的Ability工程模板,然后点击Next。关于工程模板的介绍和支持的设备类型,请参考工程模板和开发语言介绍。
填写工程相关信息,Device Type选择Phone,Language选择Java ,其他保持默认值即可,点击Finish。关于各个参数的详细介绍,请参考创建一个新的工程。
工程创建完成后,DevEco Studio会自动进行工程的同步
使用模拟器运行HelloWorld
DevEco Studio提供远程模拟器和本地模拟器,本示例以远程模拟器为例进行说明。关于本地模拟器的使用请参考1.6.1-使用Local Emulator运行应用。
DevEco Studio提供模拟器供开发者运行和调试HarmonyOS应用。
- 在DevEco Studio菜单栏,点击Tools > Device Manager。
- 在Remote Emulator页签中点击Login,在浏览器中弹出华为开发者联盟帐号登录界面,请输入已实名认证的华为开发者联盟帐号的用户名和密码进行登录(查看远程模拟器登录常见问题)。
- 登录后,请点击界面的允许按钮进行授权。
- 在设备列表中,选择Phone设备P40,并点击▶按钮,运行模拟器(此处选择Super Device)
- 点击DevEco Studio工具栏中的▶按钮运行工程,或使用默认快捷键Shift+F10(Mac为Control+R)运行工程
- DevEco Studio会启动应用的编译构建,完成后应用即可运行在模拟器上
编写第一个页面
在Java UI框架中,提供了两种编写布局的方式:在XML中声明UI布局和在代码中创建布局。这两种方式创建出的布局没有本质差别,为了熟悉两种方式,我们将通过XML的方式编写第一个页面。
- 在Project窗口,点击“entry > src > main > resources > base > layout”,打开“ability_main.xml”文件。
- 第一个页面内有一个HarmonyOS图标和一个按钮,使用DependentLayout布局,通过DependentLayout 和Button组件来实现,其中vp和fp分别表示虚拟像素和字体像素。“ability_main.xml”的示例代码如下:
- <DirectionalLayout
- xmlns:ohos="http://schemas.huawei.com/res/ohos"
- ohos:height="match_parent"
- ohos:width="match_parent"
- ohos:orientation="vertical"
- ohos:background_element="$media:black">
- <DirectionalLayout
- ohos:height="360vp"
- ohos:width="380vp"
- ohos:alignment="center"
- ohos:top_margin="100vp"
- ohos:background_element="$media:harmonyos_logo">
- </DirectionalLayout>
- <DirectionalLayout
- ohos:id="$+id:music_play_control_container"
- ohos:weight="1"
- ohos:height="0vp"
- ohos:width="match_parent"
- ohos:alignment="center"
- ohos:bottom_margin="80vp"
- ohos:start_padding="10vp"
- ohos:end_padding="10vp"
- ohos:top_margin="100vp">
- <Button
- ohos:id="$+id:btn_music"
- ohos:height="100vp"
- ohos:width="300vp"
- ohos:background_element="$graphic:background_button"
- ohos:text_color="$color:white"
- ohos:layout_alignment="horizontal_center"
- ohos:text_alignment="center"
- ohos:left_padding="15vp"
- ohos:right_padding="15vp"
- ohos:text="$string:btn_into_page"
- ohos:text_size="30vp"
- ohos:top_margin="20vp">
- </Button>
- </DirectionalLayout>
- </DirectionalLayout
复制代码
- 在XML文件中添加组件后,需要在Java代码中加载XML布局。在Project窗口,选择“entry > src > main > java > com.luxiaolu.musictogether > slice” ,打开“MainAbilitySlice.java”文件,使用setUIContent方法加载“ability_main.xml”布局,同时在该Ability中检查相关权限,MainAbilitySlice.java源码如下:
- public class MainAbilitySlice extends AbilitySlice {
- private static final String TAG = CommonData.TAG + MainAbilitySlice.class.getSimpleName();
- private static final int PERMISSION_CODE = 10000000;
- @Override
- public void onStart(Intent intent) {
- super.onStart(intent);
- super.setUIContent(ResourceTable.Layout_ability_main);
- grantPermission();
- initView();
- }
- void grantPermission() {
- if (verifySelfPermission(DISTRIBUTED_DATASYNC) != IBundleManager.PERMISSION_GRANTED) {
- if (canRequestPermission(DISTRIBUTED_DATASYNC)) {
- requestPermissionsFromUser(new String[] {DISTRIBUTED_DATASYNC}, PERMISSION_CODE);
- }
- }
- }
- private void initView() {
- findComponentById(ResourceTable.Id_btn_music).setClickedListener(new ButtonClick());
- }
- private void TogetherMusic() {
- LogUtil.info(TAG, "Click ResourceTable Id_togetherMusic");
- Intent musicIntent = new Intent();
- Operation operationMusic = new Intent.OperationBuilder().withBundleName(getBundleName())
- .withAbilityName(CommonData.ABILITY_MAIN)
- .withAction(CommonData.MUSIC_PAGE)
- .build();
- musicIntent.setOperation(operationMusic);
- startAbility(musicIntent);
- }
-
- /**
- * ButtonClick
- */
- private class ButtonClick implements Component.ClickedListener {
- @Override
- public void onClick(Component component) {
- int btnId = component.getId();
- switch (btnId) {
- case ResourceTable.Id_btn_music:
- TogetherMusic();
- break;
- default:
- LogUtil.info(TAG, "Click default");
- break;
- }
- }
- }
- }
复制代码
5.第二个页面实现音乐播放页面,创建顺序和第一个页面方式相同,可以使用复制修改的方式添加,依然使用DependentLayout布局,通过Text和Image,RoundProgressBar,Slider来实现, ability_music.xml 代码如下:
- MusicAbilitySlice.java是业务逻辑主代码,实现多设备连接,音乐播放/暂停,上下歌曲切换和音乐播放时动态图片旋转效果实现功能1)、 查找在线设备,并选择设备连接
- <pre class="public-DraftStyleDefault-pre" data-offset-key="biqkm-0-0"><pre class="Editable-styled" data-block="true" data-editor="2i0lu" data-offset-key="biqkm-0-0"><div data-offset-key="biqkm-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="biqkm-0-0"><span data-text="true">/**
- * 播放音乐时的动画效果(定时循环旋转图片)
- */
- private RoundProgressBar roundProgressBar;
- private PixelMapElement element;
- private int rotateDegrees =0;
- private int progressValue = 0;
- private int maxProgressVale = 100;
- private void initMyImage(){
- roundProgressBar = (RoundProgressBar) findComponentById(ResourceTable.Id_progressCircularImage);
- roundProgressBar.setMaxValue(maxProgressVale);
- Timer timer = new Timer();
- timer.schedule(new TimerTask(){
- @Override
- public void run() {
- if(isPlaying){
- // 背景图片旋转角度
- if(rotateDegrees<360){
- // 每秒旋转角度
- rotateDegrees +=10;
- }else{
- rotateDegrees =0;
- }
- // 进度变化
- if(maxProgressVale>progressValue){
- progressValue +=10;
- }else {
- progressValue = 0;
- }
- element = new PixelMapElement(transIdToPixelMap(rotateDegrees, resoureId,250,250));
- element.setFilterPixelMap(true);
- getUITaskDispatcher().asyncDispatch(new Runnable() {
- @Override
- public void run() {
- // 设置进度
- roundProgressBar.setProgressValue(progressValue);
- // 进度条背景图片
- roundProgressBar.setBackground(element);
- }
- });
- }
- }
- },0,200);
- }
- // 将本地图片resId转换成PixelMap
- private PixelMap transIdToPixelMap(int rotateDegrees, int resId, int width, int height) {
- InputStream source = null;
- ImageSource imageSource = null;
- try {
- source = getContext().getResourceManager().getResource(resId);
- imageSource = ImageSource.create(source, null);
- ImageSource.DecodingOptions decodingOpts = new ImageSource.DecodingOptions();
- decodingOpts.desiredSize = new Size(width, height);
- decodingOpts.rotateDegrees = rotateDegrees;
- return imageSource.createPixelmap(decodingOpts);
- } catch (IOException | NotExistException e) {
- LogUtil.error(TAG, "getPixelMap error");
- } finally {
- try {
- source.close();
- } catch (IOException e) {
- LogUtil.error(TAG, "getPixelMap source close error");
- }
- }
- return PixelMap.create(null);
- }</span></span></div></pre></pre><div class="Editable-unstyled" data-block="true" data-editor="2i0lu" data-offset-key="dnofi-0-0"><div data-offset-key="dnofi-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="dnofi-0-0"><span data-text="true">2)、</span></span><span data-offset-key="dnofi-1-0"><span data-text="true">启动远程FA/PA</span></span><span data-offset-key="dnofi-2-0"><span data-text="true"> 设备A连接设备B侧的PA,利用连接关系调用该PA执行特定任务</span></span></div></div><pre class="public-DraftStyleDefault-pre" data-offset-key="cvbo4-0-0"><pre class="Editable-styled" data-block="true" data-editor="2i0lu" data-offset-key="cvbo4-0-0"><div data-offset-key="cvbo4-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="cvbo4-0-0"><span data-text="true">private void connectRemotePa(String deviceId, int requestType) {
- if (!deviceId.isEmpty()) {
- Intent connectPaIntent = new Intent();
- Operation operation = new Intent.OperationBuilder().withDeviceId(deviceId)
- .withBundleName(getBundleName())
- .withAbilityName(CommonData.MUSIC_SERVICE_NAME)
- .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
- .build();
- connectPaIntent.setOperation(operation);
- conn = new IAbilityConnection() {
- @Override
- public void onAbilityConnectDone(ElementName elementName, IRemoteObject remote, int resultCode) {
- LogUtil.info(TAG, "onAbilityConnectDone......");
- connectAbility(elementName, remote, requestType);
- }
- @Override
- public void onAbilityDisconnectDone(ElementName elementName, int resultCode) {
- disconnectAbility(this);
- LogUtil.info(TAG, "onAbilityDisconnectDone......");
- }
- };
- isConnected = getContext().connectAbility(connectPaIntent, conn);
- }
- }
- private void connectAbility(ElementName elementName, IRemoteObject remote, int requestType) {
- proxy = new MusicRemoteProxy(remote);
- try {
- proxy.senDataToRemote(requestType);
- } catch (RemoteException e) {
- LogUtil.error(TAG, "onAbilityConnectDone RemoteException");
- }
- }</span></span></div></pre></pre><div class="Editable-unstyled" data-block="true" data-editor="2i0lu" data-offset-key="a141v-0-0"><div data-offset-key="a141v-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="a141v-0-0"><span data-text="true">3)、在本地发起连接侧和对端被连接侧分别实现代理</span></span></div></div><pre class="public-DraftStyleDefault-pre" data-offset-key="16s8a-0-0"><pre class="Editable-styled" data-block="true" data-editor="2i0lu" data-offset-key="16s8a-0-0"><div data-offset-key="16s8a-0-0" class="public-DraftStyleDefault-block public-DraftStyleDefault-ltr"><span data-offset-key="16s8a-0-0"><span data-text="true">class MusicRemoteProxy implements IRemoteBroker {
- private static final int ERR_OK = 0;
- private static final int REQUEST_START_ABILITY = 1;
- private static final int REQUEST_SEND_DATA = 2;
- private final IRemoteObject remote;
- MusicRemoteProxy(IRemoteObject remote) {
- this.remote = remote;
- }
- @Override
- public IRemoteObject asObject() {
- return remote;
- }
- private void senDataToRemote(int requestType) throws RemoteException {
- MessageParcel data = MessageParcel.obtain();
- MessageParcel reply = MessageParcel.obtain();
- try {
- isLocal = false;
- data.writeInt(actionFlag);
- data.writeString(localDeviceId);
- data.writeBoolean(isLocal);
- MessageOption option = new MessageOption(MessageOption.TF_SYNC);
- remote.sendRequest(requestType, data, reply, option);
- LogUtil.info(TAG, "send action: "+actionFlag+"; isLocal: "+isLocal);
- int ec = reply.readInt();
- if (ec != ERR_OK) {
- LogUtil.error(TAG, "ec != ERR_OK RemoteException");
- }
- } catch (RemoteException e) {
- LogUtil.error(TAG, "RemoteException");
- } finally {
- data.reclaim();
- reply.reclaim();
- }
- }
- }</span></span></div></pre></pre>
复制代码
4)、 订阅事件(每个应用都可以订阅自己感兴趣的公共事件,订阅成功后且公共事件发布后,系统会把其发送给应用。这些公共事件可能来自系统、其他应用和应用自身)
公共事件相关基础类包含CommonEventData、CommonEventPublishInfo、CommonEventSubscribeInfo、CommonEventSubscriber和CommonEventManager
- /**
- * 订阅 接收事件
- */
- private void subscribe() {
- MatchingSkills matchingSkills = new MatchingSkills();
- matchingSkills.addEvent(CommonData.MUSIC_PALY_EVENT);
- matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON);
- CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
- subscriber = new MyCommonEventSubscriber(subscribeInfo);
- try {
- LogUtil.info(TAG, "subscribeCommonEvent");
- CommonEventManager.subscribeCommonEvent(subscriber);
- } catch (RemoteException e) {
- LogUtil.error(TAG, "subscribeCommonEvent occur exception.");
- }
- }
- /**
- * 订阅公共事件
- */
- class MyCommonEventSubscriber extends CommonEventSubscriber {
- MyCommonEventSubscriber(CommonEventSubscribeInfo info) {
- super(info);
- }
- @Override
- public void onReceiveEvent(CommonEventData commonEventData) {
- reciveTime = System.currentTimeMillis();
- LogUtil.info(TAG, "onReceiveEvent, sendTime - reciveTime is " + (sendTime - reciveTime));
- if (Math.abs(sendTime - reciveTime) <= MAX_RECIVE_TIME) {
- LogUtil.info(TAG, "almost at the same time, do not handle recive msg");
- isShare = false;
- new ToastDialog(getContext()).setText("操作冲突,后续独立").setAlignment(LayoutAlignment.CENTER).show();
- return;
- }
- // 接收远程数据
- Intent intent = commonEventData.getIntent();
- updateDataInfo(intent);
- }
- }
- private void updateDataInfo(Intent intent) {
- if(null != intent){
- actionFlag = intent.getIntParam(CommonData.KEY_MUSIC_ACTION_ID, 0);
- getUITaskDispatcher().delayDispatch(this::actionFun, DELAY_TIME);
- }
- }
- private void actionFun(){
- if(100 == actionFlag){
- playOrPause();
- }else if(200 == actionFlag){
- nextMusic();
- }else if(300 == actionFlag){
- prevMusic();
- }
- }
复制代码
5)、 播放音乐时显示图片动画效果(暂时使用定时器,循环旋转图片)
- /**
- * 播放音乐时的动画效果(定时循环旋转图片)
- */
- private RoundProgressBar roundProgressBar;
- private PixelMapElement element;
- private int rotateDegrees =0;
- private int progressValue = 0;
- private int maxProgressVale = 100;
- private void initMyImage(){
- roundProgressBar = (RoundProgressBar) findComponentById(ResourceTable.Id_progressCircularImage);
- roundProgressBar.setMaxValue(maxProgressVale);
- Timer timer = new Timer();
- timer.schedule(new TimerTask(){
- @Override
- public void run() {
- if(isPlaying){
- // 背景图片旋转角度
- if(rotateDegrees<360){
- // 每秒旋转角度
- rotateDegrees +=10;
- }else{
- rotateDegrees =0;
- }
- // 进度变化
- if(maxProgressVale>progressValue){
- progressValue +=10;
- }else {
- progressValue = 0;
- }
- element = new PixelMapElement(transIdToPixelMap(rotateDegrees, resoureId,250,250));
- element.setFilterPixelMap(true);
- getUITaskDispatcher().asyncDispatch(new Runnable() {
- @Override
- public void run() {
- // 设置进度
- roundProgressBar.setProgressValue(progressValue);
- // 进度条背景图片
- roundProgressBar.setBackground(element);
- }
- });
- }
- }
- },0,200);
- }
- // 将本地图片resId转换成PixelMap
- private PixelMap transIdToPixelMap(int rotateDegrees, int resId, int width, int height) {
- InputStream source = null;
- ImageSource imageSource = null;
- try {
- source = getContext().getResourceManager().getResource(resId);
- imageSource = ImageSource.create(source, null);
- ImageSource.DecodingOptions decodingOpts = new ImageSource.DecodingOptions();
- decodingOpts.desiredSize = new Size(width, height);
- decodingOpts.rotateDegrees = rotateDegrees;
- return imageSource.createPixelmap(decodingOpts);
- } catch (IOException | NotExistException e) {
- LogUtil.error(TAG, "getPixelMap error");
- } finally {
- try {
- source.close();
- } catch (IOException e) {
- LogUtil.error(TAG, "getPixelMap source close error");
- }
- }
- return PixelMap.create(null);
- }
复制代码
- MusicServiceAbility.java 基于Service模板的Ability主要用于后台运行任务(建立远程连接,发送事件数据),但不提供用户交互界面。Service可由其他应用或Ability启动,即使用户切换到其他应用,Service仍将在后台继续运行。了解更多创建、启动、连接Service
- public class MusicServiceAbility extends Ability {
- private static final String TAG = "####service";
- private TogetherMusicRemote remote = new TogetherMusicRemote();
- @Override
- public void onStart(Intent intent) {
- super.onStart(intent);
- LogUtil.info(TAG, "MusicServiceAbility::onStart");
- }
- @Override
- public void onBackground() {
- super.onBackground();
- LogUtil.info(TAG, "MusicServiceAbility::onBackground");
- }
- @Override
- public void onStop() {
- super.onStop();
- LogUtil.info(TAG, "MusicServiceAbility::onStop");
- }
- @Override
- protected IRemoteObject onConnect(Intent intent) {
- super.onConnect(intent);
- return remote.asObject();
- }
- @Override
- public void onDisconnect(Intent intent) {
- LogUtil.info(TAG, "MusicServiceAbility::onDisconnect");
- }
- /**
- * 发送 点击播放、暂停、下一首音乐切换动作指令
- * [url=home.php?mod=space&uid=3142012]@param[/url] playAction 播放动作指令
- */
- private void sendEvent(int playAction) {
- try {
- Intent intent = new Intent();
- Operation operation = new Intent.OperationBuilder().withAction(CommonData.MUSIC_PALY_EVENT).build();
- intent.setOperation(operation);
- intent.setParam(CommonData.KEY_MUSIC_ACTION_ID, playAction);
- CommonEventData eventData = new CommonEventData(intent);
- CommonEventManager.publishCommonEvent(eventData);
- } catch (RemoteException e) {
- LogUtil.error(TAG, "publishCommonEvent occur exception.");
- }
- }
- /**
- * 建立远程连接
- */
- public class TogetherMusicRemote extends RemoteObject implements IRemoteBroker {
- private static final int ERR_OK = 0;
- private static final int REQUEST_START_ABILITY = 1;
- private TogetherMusicRemote() {
- super("TogetherMusicRemote");
- }
- @Override
- public IRemoteObject asObject() {
- return this;
- }
- @Override
- public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
- String remoteDeviceId = data.readString();
- int playAction = data.readInt();
- boolean isLocal = data.readBoolean();
- reply.writeInt(ERR_OK);
- if (code == REQUEST_START_ABILITY) {
- Intent secondIntent = new Intent();
- Operation operation = new Intent.OperationBuilder().withDeviceId("")
- .withBundleName(getBundleName())
- .withAbilityName(CommonData.ABILITY_MAIN)
- .withAction(CommonData.MUSIC_PAGE)
- .build();
- secondIntent.setParam(CommonData.KEY_REMOTE_DEVICEID, remoteDeviceId);
- secondIntent.setParam(CommonData.KEY_MUSIC_ACTION_ID, playAction);
- secondIntent.setParam(CommonData.KEY_IS_LOCAL, isLocal);
- secondIntent.setOperation(operation);
- startAbility(secondIntent);
- } else {
- sendEvent(playAction);
- }
- return true;
- }
- }
- }
复制代码
整体效果演示
希望有更多同学或朋友加入HarmonyOS, 提供更多新鲜的、新奇的idea。
作者简介:我是老王,一个从事鸿蒙开发的中年老吃货。关注我,每天和你聊点关于华为、鸿蒙认证的一些事儿。