[文章]鸿湖万联:带你玩转HarmonyOS多端钢琴演奏

阅读量0
1
2

项目介绍

本项目主要采用HarmonyOS跨端迁移,Fractio等实现钢琴88个按键分为七个区域流转到不同设备上播放对应音频。传统实体钢琴三个音区,分为九组,如下图所示:

图片

本项目在设备A上初始显示的是中音区小字一组区域的钢琴按键,点击流转按钮即可弹出三音区,七个音域供用户选择,在用户确认好所选音域,在满足流转特性的约束及限制的前提下,即可在设备B上展示所选音域,并且设备A,B可独立操作,互不影响。

图片

进入项目后,展示的钢琴中音区中的小字一组这部分,如下图所示:

图片

白色七个按键和黑色五个按键,对应中音区小字一组相对应的音频,可同时多个按键触发音频播放。

流转按钮

点击流转按钮,会弹出选择音域弹出框,选项总共有三个音区,分别为低音区、中音区、高音区。

图片

选择确定,则会弹出流转设备选择框,点击对应设备名称,则在选择音域时,选择的对应音域流转到设备B,如下图所示:

图片

设备B显示,A设备所选则对应音域,流转按钮变为已流转。

图片

若在设备B上点击已流转按钮,则会弹出退出流转弹出框,如下图所示:

图片

若选择取消,则弹出框消失,界面无变化,触摸及点击弹出框以外的区域,弹出框也会消失。

图片

若选择确定,设备B退出流转。

音域选择按钮

点击音域选择按钮,会选项总共有三个音区,分别为低音区、中音区、高音区,低音区二级选项为大字二、一组;大字组;中音区二级选项为小字组、小字一组、小字二组;高音区二级选项为小字三组,小字四、五组,默认为中音区,小字一组,如下图所示:

图片

选择确定,选择的对应音域,该设备的当前音域界面则会变成所选音域,比如选择小字四,五组音域,同时再次点击音域选择按钮时,默认选择项则变为小字四、五组,与当前选择结果对应,如下图所示:

图片

图片

  1. 钢琴按键按下触发效果
  2. 白色按钮E触发效果,如下图所示:

图片

  1. 黑色按钮d1m触发效果,如下图所示:

图片

  1. 多指按键触发效果,如下图所示:

图片

逻辑实现

一、流转相关功能开发步骤:

1.创建项目中的MainAbility中实现IAbilityContinuation接口,此外,还需要在MainAbility的onStart()中,调用requestPermissionsFromUser()方法申请权限。

public class MainAbility extends FractionAbility implements IAbilityContinuation {

@Override

public void onStart(Intent intent) {

WindowManager.getInstance().getTopWindow().get().setStatusBarColor(ConstantUtils.COLOR_DEFAULT);//设置状态栏颜色

super.onStart(intent);

super.setMainRoute(MainAbilitySlice.class.getName());

requestPermission();

}

//请求权限

private void requestPermission() {

String[] permission = {

"ohos.permission.servicebus.ACCESS_SERVICE",

"ohos.permission.DISTRIBUTED_DATASYNC",

"ohos.permission.GET_DISTRIBUTED_DEVICE_INFO",

"ohos.permission.KEEP_BACKGROUND_RUNNING"};

List applyPermissions = new ArrayList<>();

for (String element : permission) {

if (verifySelfPermission(element) != 0) {

if (canRequestPermission(element)) {

applyPermissions.add(element);


}

}

}

requestPermissionsFromUser(applyPermissions.toArray(new String[0]), 0);

}

@Override

public boolean onStartContinuation() {  return true;}

@Override

public boolean onSaveData(IntentParams intentParams) { return true; }

@Override

public boolean onRestoreData(IntentParams intentParams) { return true; }

@Override

public void onCompleteContinuation(int i) {}

}

2.在对应的config.json中声明跨端迁移访问的权限:ohos.permission.DISTRIBUTED_DATASYNC,在config.json中的配置如下:

{

"name":"ohos.permission.DISTRIBUTED_DATASYNC"

},

3.在MainAbilitySlice中实现钢琴按键的页面,代码逻辑在MainAbilitySlice中实现,代码示例如下:




public class MainAbilitySlice extends AbilitySlice implements IAbilityContinuation {

@Override

public void onStart(Intent intent) {

super.onStart(intent);

super.setUIContent(ResourceTable.Layout_ability_main);

//获取屏幕宽度

windowWidth=WindowUtil.getWindowWidth(getContext());

initView(); //初始化视图

startLocalAudioPlay();

initFraction();//初始化Fraction}

4.给流转按键绑定点击事件,点击流转按钮弹出音域选择框,确定所选音域之后,弹出设备选择框:代码示例如下:




private void showConnectionDialog() {

SelectRangeDialog selectRangeDialog = new SelectRangeDialog(this);

selectRangeDialog.show();

selectRangeDialog.setResultListener((regionValue, groupValue) -> {

selectRegionValue = regionValue;

selectGroupValue = groupValue;

switch (selectRegionValue) {

case ConstantUtils.BASS_AREA:

setSelectResult(0, ConstantUtils.RANGE_ONE, 1, ConstantUtils.RANGE_TWO);

break;

case ConstantUtils.ALTO_SECTION:

if (selectGroupValue == 0) {

selectRangeResult = ConstantUtils.RANGE_THREE;

} else setSelectResult(1, ConstantUtils.RANGE_FOUR, 2, ConstantUtils.RANGE_FIVE);

break;

case ConstantUtils.TREBLE:

setSelectResult(0, ConstantUtils.RANGE_SIX, 1, ConstantUtils.RANGE_SEVEN);

break;

}

getDevices();

});

}

private void getDevices() {

if (devices.size() > 0) {

devices.clear();

}

devices.addAll(deviceInfoList);

showDevicesDialog();

}

5.根据设备列表适配即可将所有符合条件的设备展示在设备弹窗当中,供用户选择,设备列表适配代码如下:


public class DevicesListAdapter extends BaseItemProvider {

private static final int SUBSTRING_START = 0;

private static final int SUBSTRING_END = 4;

private final List deviceInfoList;

private final Context context;

public DevicesListAdapter(List deviceInfoList, Context context) {

this.deviceInfoList = deviceInfoList;

this.context = context;

}

@Override

public int getCount() {

return deviceInfoList == null ? 0 : deviceInfoList.size();

}

@Override

public Object getItem(int position) {

return Optional.of(deviceInfoList.get(position));

}

@Override

public long getItemId(int position) {

return position;

}

@Override

public Component getComponent(int position, Component component, ComponentContainer componentContainer) {

ViewHolder viewHolder = null;

Component mComponent = component;

if (mComponent == null) {

mComponent = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_item_device_list, null, false);

viewHolder = new ViewHolder();

if (mComponent.findComponentById(ResourceTable.Id_device_name) instanceof Text) {

viewHolder.devicesName = (Text) mComponent.findComponentById(ResourceTable.Id_device_name);

}

if (mComponent.findComponentById(ResourceTable.Id_device_id) instanceof Text) {

viewHolder.devicesId = (Text) mComponent.findComponentById(ResourceTable.Id_device_id);

}

mComponent.setTag(viewHolder);

} else {

if (mComponent.getTag() instanceof ViewHolder) {

viewHolder = (ViewHolder) mComponent.getTag();

}

}

if (viewHolder != null) {

viewHolder.devicesName.setText(deviceInfoList.get(position).getDeviceName());

String deviceId = deviceInfoList.get(position).getDeviceId();

deviceId = deviceId.substring(SUBSTRING_START, SUBSTRING_END) + "******"

+ deviceId.substring(deviceId.length() - SUBSTRING_END);

viewHolder.devicesId.setText(deviceId);

}

return mComponent;

}

private static class ViewHolder {

private Text devicesName;

private Text devicesId;

}

}

6.根据所选设备B的Id,即可在设备上展示所选音域,并且根据条件使用Fraction替换设备A上小字一组音域,使之亦可操作钢琴按键,示例代码如下:


private void showDevicesDialog() {

new SelectDeviceDialog(this, devices, deviceInfo -> {

saveDevices.add(deviceInfo.getDeviceId());

//跨端迁移

continueAbility(deviceInfo.getDeviceId());

}).deviceShow();

if (isTag) {

//替换当前布局

try {

ReplaceCurrentLayout();

} catch (Exception e) {

e.printStackTrace();

}

isTag = false;

}

}

7.FA的跨端迁移还涉及到状态数据的传递,需要实现IAbilityContinuation接口,以便实现迁移过程中特定事件的管理能力,代码示例如下:


public class MainAbilitySlice extends AbilitySlice implements IAbilityContinuation {

//开始迁移 AbilitySlice可以不实现默认返回true

@Override

public boolean onStartContinuation() {

return true;

}

@Override

public boolean onSaveData(IntentParams intentParams) {

intentParams.setParam("data", "remote");

intentParams.setParam(ConstantUtils.RANGE_RESULT, selectRangeResult);

return true;

}

@Override

public boolean onRestoreData(IntentParams intentParams) {

// 远端FA迁移传来的状态数据

data = intentParams.getParam("data").toString();

selectRangeResult = Integer.parseInt(intentParams.getParam(ConstantUtils.RANGE_RESULT).toString())

return true;

}

@Override

public void onCompleteContinuation(int i) {

}

//远程终止

@Override

public void onRemoteTerminated() {

IAbilityContinuation.super.onRemoteTerminated();

}

@Override

protected void onActive() {

super.onActive();

}

@Override

protected void onStop() {

super.onStop();

}

}

二、音频播放能力相关功能开发步骤

本项目实现了设备A,B同时具有音频的播放能力,音频播放则是作为一个单独的serviceAbility,使用HarmonyOS IDL实现不同设备之间的通信及数据的传递,代码示例如下:



interface com.isoftstone.simplepiano.IAudioPlaybackCapabilityInterface {

/*

* Example of a service method that uses some parameters

*/

//表示该方法是单向方法,即调用方法后不用等待该方法执行即可返回

[oneway]

void sendCommand([in] int command, [in] int soundId,[in] int selectResults);

}

AudioServiceAbility则在项目启动时,加载钢琴按键音频资源,并保持系统后台运行,防止被系统kill,并且根据用户所选音域,及触摸的不同按键传递给SoundPlayer进行音频播放,代码示例如下:


public class AudioServiceAbility extends Ability {

private static final int NOTIFICATION_ID = 1005;

private static final String TAG = AudioServiceAbility.class.getSimpleName();

private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, TAG);

private OnePianoAudio onePianoAudio;

.....

public static final int PLAY_AUDIO_MSG = 100;

@Override

public void onStart(Intent intent) {

HiLog.error(LABEL_LOG, "PlayerServiceAbility::onStart");

super.onStart(intent);

onePianoAudio = new OnePianoAudio(getContext());

.....

NotificationRequest request = new NotificationRequest(NOTIFICATION_ID).setTapDismissed(true);

NotificationRequest.NotificationNormalContent content = new NotificationRequest.NotificationNormalContent();

content.setTitle("音频服务").setText("服务运行中...");

NotificationRequest.NotificationContent notificationContent = new NotificationRequest.NotificationContent(content);

request.setContent(notificationContent);

keepBackgroundRunning(NOTIFICATION_ID, request);

}

@Override

public void onStop() {

super.onStop();

HiLog.info(LABEL_LOG, "PlayerServiceAbility::onStop");

//取消后台运行

cancelBackgroundRunning();

}

@Override

public IRemoteObject onConnect(Intent intent) {

super.onConnect(intent);

return new AudioRemountObject("AudioRemountObject").asObject();

}

@Override

public void onDisconnect(Intent intent) {

super.onDisconnect(intent);

}

//音频远程对象

private class AudioRemountObject extends AudioPlaybackCapabilityInterfaceStub {

public AudioRemountObject(String descriptor) {

super(descriptor);

}

@Override

public void sendCommand(int command, int soundId, int selectResults) {

LogUtil.debug("AudioServiceAbility", "sendCommand");

if (command == PLAY_AUDIO_MSG) {

switch (selectResults) {

case ConstantUtils.RANGE_ONE:

onePianoAudio.soundOnePlay(soundId);

break;

......

}

}

}

}

}

1.在MainAbilitySlice中OnStart()启动本地音频服务,避免音频代理接口Proxy为空,代码示例如下:


@Override

public void onStart(Intent intent) {

.....

startLocalAudioPlay()//启动本地音频服务

.....

}

//音频接口代理

AudioPlaybackCapabilityInterfaceProxy PlayerAudioInterfaceProxy = null;

//能力连接

private final IAbilityConnection AudioAbilityConnection = new IAbilityConnection() {

@Override

public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int i) {

PlayerAudioInterfaceProxy = new AudioPlaybackCapabilityInterfaceProxy(iRemoteObject);

}

@Override

public void onAbilityDisconnectDone(ElementName elementName, int i) {

PlayerAudioInterfaceProxy = null;

}

};

private void startLocalAudioPlay() {

Intent localIntent = new Intent();

Operation localOperation = new Intent.OperationBuilder()

.withBundleName(getBundleName())

.withAbilityName(ConstantUtils.AUDIO_ABILITY_MAIN)

.withFlags(Intent.FLAG_START_FOREGROUND_ABILITY)

.build();

localIntent.setOperation(localOperation);

startAbility(localIntent);

//本地音频播放能力连接

connectAbility(localIntent, AudioAbilityConnection);

}

三、音域选择能力相关功能开发步骤

1.点击音域选择按钮,即可弹出音域选择弹出框,同流转按钮时,音域选择弹出框一样,用户在选择好对应音域,当前设备即可切换为所选音域,并可进行相应音频播放,在MainAbilitySlice的OnStart()方法中初始化七个音域在示例代码如下:




@Override

public void onStart(Intent intent) {

.....

//初始化Fraction

initFraction();

.....

}

2.根据用户选择的结果,替换设备上的音域,代码示例如下:


private BaseFraction showFraction;

private void setRangeLayout(int selectRangeResult) {

switch (selectRangeResult) {

case ConstantUtils.RANGE_ONE:

showFraction = oneFraction;

rangeSelection();

rangeDisplay.setText("大字二、一组");

break;

.....

default:

break;

}

}

private void rangeSelection() {

FractionManager fractionManager = ((FractionAbility) getAbility()).getFractionManager();

FractionScheduler fractionScheduler = fractionManager.startFractionScheduler();

Optional fractionByTag = fractionManager.getFractionByTag(showFraction.fractionName());

if (mCurrentFraction != null) {

fractionScheduler.hide(mCurrentFraction);

}

if (fractionByTag != null && fractionByTag.isPresent()) {

fractionScheduler.show(fractionByTag.get());

} else {

fractionScheduler.add(ResourceTable.Id_range_key, showFraction, showFraction.fractionName());

fractionScheduler.show(showFraction);

}

fractionScheduler.submit();

mCurrentFraction = showFraction;

//fractionScheduler.replace(ResourceTable.Id_range_key,showFraction);

//fractionScheduler.submit();

}

参考

1.HarmonyOS流转特性(跨端迁移)可参考:https://developer.harmonyos.com/cn/docs/documentation/doc-guides/hop-cross-device-migration-guidelines-0000001146058939

2.HarmonyOS IDL接口使用规范可参考:

https://developer.harmonyos.com/cn/docs/documentation/doc-references/idl-overview-0000001050762835

3.项目地址,以供参考:https://gitee.com/swan-link/simple-piano

总结分析

1.流转前,需满足流转约束条件,各设备需要处于同一WiFi,且为同一华为账号登录;

2.流转之后,设备B上的音域选择功能等同与设备A音域选择功能,设备A与设备B音频播放互不冲突;

3.目前Nova 9手机运行本项目时,底层存在问题,暂时无法解决,其他手机无问题;

4.HarmonyOS SoundPlayer原生短音播放所存在的弊端,SoundPlayer播放短音播放时,需提前加载好所有的音频资源,即createSound(Context context, int resourceId)方法是根据应用程序上下文合音频资源ID加载音频数据生成短音资源,该方法是异步的,而本项目钢琴按键资源较多,有88个按键资源,完成所有短音资源生成需要耗时较长,项目在该处,解决办法如下:

项目中所有按键音频资源,划分为七个音域,同时把所有资源分为七个SoundPlayer进行短音资源生成,可有效减少耗时。

5.本项目触发钢琴按键音,是在整个布局页面设置触摸事件,灵活获取设备屏幕大小,对不同按键区域进行划分,使用户在操作时,可以实现对应按键的触摸效果,以及对应钢琴按键音频的播放,示例代码如下:



private final Component.TouchEventListener touchEventListener = (component, touchEvent) -> {

int pointerIndex = touchEvent.getIndex();

int pointerId = touchEvent.getPointerId(pointerIndex);

float x = touchEvent.getPointerPosition(pointerIndex).getX();

float y = touchEvent.getPointerPosition(pointerIndex).getY();

switch (touchEvent.getAction()) {

case TouchEvent.PRIMARY_POINT_DOWN:

case TouchEvent.OTHER_POINT_DOWN:

onFingerPress(pointerId, x, y);

break;

case TouchEvent.OTHER_POINT_UP:

case TouchEvent.PRIMARY_POINT_UP:

onFingerLift(pointerId, x, y);

break;

case TouchEvent.POINT_MOVE:

//获取一次事件中触控或轨迹追踪的指针数量

int pointCount = touchEvent.getPointerCount();

for (int i = 0; i < pointCount; i++) {

//getPointerPosition(i)获取一次事件中触控或轨迹追踪的某个指针相对于偏移位置的坐标

onFingerSlide(touchEvent.getPointerId(i), touchEvent.getPointerPosition(i).getX(), touchEvent.getPointerPosition(i).getY());

}

break;

case TouchEvent.CANCEL:

onAllFingersLift();

break;

}

return true;

};

以上为采用HarmonyOS跨端迁移,Fractio等技术实现手机端钢琴交互流程,通过该项目,我们能够快速理解数据的“多端协同”和“跨端迁移”,便于在其他项目中快速实现无缝切换的需求。

回帖

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
链接复制成功,分享给好友