前言
想弹出悦耳的曲子奈何没有钢琴,代码来实现你的演奏愿望,软通动力程序小哥手把手带你编码造钢琴,用手机弹出你想要的曲子,多个手机同时演奏都不是问题。
项目介绍
本项目主要采用HarmonyOS跨端迁移,Fractio等实现钢琴88个按键分为七个区域流转到不同设备上播放对应音频。传统实体钢琴三个音区,分为九组,如下图所示:
本项目在设备A上初始显示的是中音区小字一组区域的钢琴按键,点击流转按钮即可弹出三音区,七个音域供用户选择,在用户确认好所选音域,在满足流转特性的约束及限制的前提下,即可在设备B上展示所选音域,并且设备A,B可独立操作,互不影响。
进入项目后,展示的钢琴中音区中的小字一组这部分,如下图所示:
白色七个按键和黑色五个按键,对应中音区小字一组相对应的音频,可同时多个按键触发音频播放。
1、流转按钮
点击流转按钮,会弹出选择音域弹出框,选项总共有三个音区,分别为低音区、中音区、高音区。
选择确定,则会弹出流转设备选择框,点击对应设备名称,则在选择音域时,选择的对应音域流转到设备B,如下图所示:
设备B显示,A设备所选则对应音域,流转按钮变为已流转。
若在设备B上点击已流转按钮,则会弹出退出流转弹出框,如下图所示:
若选择取消,则弹出框消失,界面无变化,触摸及点击弹出框以外的区域,弹出框也会消失。
若选择确定,设备B退出流转。
2、音域选择按钮
点击音域选择按钮,会选项总共有三个音区,分别为低音区、中音区、高音区,低音区二级选项为大字二、一组;大字组;中音区二级选项为小字组、小字一组、小字二组;高音区二级选项为小字三组,小字四、五组,默认为中音区,小字一组,如下图所示:
选择确定,选择的对应音域,该设备的当前音域界面则会变成所选音域,比如选择小字四,五组音域,同时再次点击音域选择按钮时,默认选择项则变为小字四、五组,与当前选择结果对应,如下图所示:
3、钢琴按键按下触发效果
(1)白色按钮E触发效果,如下图所示:
(2)黑色按钮d1m触发效果,如下图所示:
(3)多指按键触发效果,如下图所示:
逻辑实现
一、流转相关功能开发步骤:
1.创建项目中的MainAbility中实现IAbilityContinuation接口,此外,还需要在MainAbility的onStart()中,调用requestPermissionsFromUser()方法申请权限。
@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<String> 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中实现,代码示例如下:
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
//获取屏幕宽度
windowWidth = WindowUtil.getWindowWidth(getContext());
initView(); //初始化视图
startLocalAudioPlay();
initFraction();//初始化Fraction
}
4.给流转按键绑定点击事件,点击流转按钮弹出音域选择框,确定所选音域之后,弹出设备选择框:代码示例如下:
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.根据设备列表适配即可将所有符合条件的设备展示在设备弹窗当中,供用户选择,设备列表适配代码如下:
private static final int SUBSTRING_START = 0;
private static final int SUBSTRING_END = 4;
private final List<DeviceInfo> deviceInfoList;
private final Context context;
public DevicesListAdapter(List<DeviceInfo> 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上小字一组音域,使之亦可操作钢琴按键,示例代码如下:
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接口,以便实现迁移过程中特定事件的管理能力,代码示例如下:
//开始迁移 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实现不同设备之间的通信及数据的传递,代码示例如下:
/*
* Example of a service method that uses some parameters
*/
//表示该方法是单向方法,即调用方法后不用等待该方法执行即可返回
[oneway]
void sendCommand([in] int command, [in] int soundId,[in] int selectResults);
}
AudioServiceAbility则在项目启动时,加载钢琴按键音频资源,并保持系统后台运行,防止被系统kill,并且根据用户所选音域,及触摸的不同按键传递给SoundPlayer进行音频播放,代码示例如下:
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为空,代码示例如下:
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()方法中初始化七个音域在示例代码如下:
public void onStart(Intent intent) {
.....
//初始化Fraction
initFraction();
.....
}
2.根据用户选择的结果,替换设备上的音域,代码示例如下:
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<Fraction> 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.本项目触发钢琴按键音,是在整个布局页面设置触摸事件,灵活获取设备屏幕大小,对不同按键区域进行划分,使用户在操作时,可以实现对应按键的触摸效果,以及对应钢琴按键音频的播放,示例代码如下:
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等技术实现手机端钢琴交互流程,通过该项目,我们能够快速理解数据的“多端协同”和“跨端迁移”,便于在其他项目中快速实现无缝切换的需求。