本文正在参加星光计划3.0—夏日挑战赛

前言

想弹出悦耳的曲子奈何没有钢琴,代码来实现你的演奏愿望,软通动力程序小哥手把手带你编码造钢琴,用手机弹出你想要的曲子,多个手机同时演奏都不是问题。

项目介绍

本项目主要采用HarmonyOS跨端迁移,Fractio等实现钢琴88个按键分为七个区域流转到不同设备上播放对应音频。传统实体钢琴三个音区,分为九组,如下图所示:
image.png
本项目在设备A上初始显示的是中音区小字一组区域的钢琴按键,点击流转按钮即可弹出三音区,七个音域供用户选择,在用户确认好所选音域,在满足流转特性的约束及限制的前提下,即可在设备B上展示所选音域,并且设备A,B可独立操作,互不影响。
image.png
进入项目后,展示的钢琴中音区中的小字一组这部分,如下图所示:
image.png
白色七个按键和黑色五个按键,对应中音区小字一组相对应的音频,可同时多个按键触发音频播放。

1.流转按钮

点击流转按钮,会弹出选择音域弹出框,选项总共有三个音区,分别为低音区、中音区、高音区。
image.png
选择确定,则会弹出流转设备选择框,点击对应设备名称,则在选择音域时,选择的对应音域流转到设备B,如下图所示:
image.png
设备B显示,A设备所选则对应音域,流转按钮变为已流转。
image.png
若在设备B上点击已流转按钮,则会弹出退出流转弹出框,如下图所示:
image.png
若选择取消,则弹出框消失,界面无变化,触摸及点击弹出框以外的区域,弹出框也会消失。
image.png
若选择确定,设备B退出流转。

2.音域选择按钮

点击音域选择按钮,会选项总共有三个音区,分别为低音区、中音区、高音区,低音区二级选项为大字二、一组;大字组;中音区二级选项为小字组、小字一组、小字二组;高音区二级选项为小字三组,小字四、五组,默认为中音区,小字一组,如下图所示:
image.png
选择确定,选择的对应音域,该设备的当前音域界面则会变成所选音域,比如选择小字四,五组音域,同时再次点击音域选择按钮时,默认选择项则变为小字四、五组,与当前选择结果对应,如下图所示:
image.png
image.png

3.钢琴按键按下触发效果

①.白色按钮E触发效果,如下图所示:

image.png

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

image.png

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

image.png

逻辑实现

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

1.创建项目中的MainAbility中实现IAbilityContinuation接口,此外,还需要在MainAbility的onStart()中,调用requestPermissionsFromUser()方法申请权限。
```public class MainAbility extends FractionAbility implements IAbilityContinuation {br/>@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中实现,代码示例如下:
```public class MainAbilitySlice extends AbilitySlice implements IAbilityContinuation {
br/>@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<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上小字一组音域,使之亦可操作钢琴按键,示例代码如下:
```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
br/>@Override
public boolean onStartContinuation() {
return true;
br/>}
@Override
public boolean onSaveData(IntentParams intentParams) {
intentParams.setParam("data", "remote");
intentParams.setParam(ConstantUtils.RANGE_RESULT, selectRangeResult);
return true;
br/>}
@Override
public boolean onRestoreData(IntentParams intentParams) {
// 远端FA迁移传来的状态数据
data = intentParams.getParam("data").toString();
selectRangeResult = Integer.parseInt(intentParams.getParam(ConstantUtils.RANGE_RESULT).toString())
return true;
br/>}
@Override
public void onCompleteContinuation(int i) {
br/>}
//远程终止
@Override
public void onRemoteTerminated() {
br/>IAbilityContinuation.super.onRemoteTerminated();
}
@Override
protected void onActive() {
br/>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()方法中初始化七个音域在示例代码如下:br/>```@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<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.本项目触发钢琴按键音,是在整个布局页面设置触摸事件,灵活获取设备屏幕大小,对不同按键区域进行划分,使用户在操作时,可以实现对应按键的触摸效果,以及对应钢琴按键音频的播放,示例代码如下:
```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等技术实现手机端钢琴交互流程,通过该项目,我们能够快速理解数据的“多端协同”和“跨端迁移”,便于在其他项目中快速实现无缝切换的需求。
## [更多原创内容请关注软通动力OpenHarmony学院](https://ost.51cto.com/column/30)

[想了解更多关于开源的内容,请访问:](https://ost.51cto.com/#bkwz)

[51CTO 开源基础软件社区](https://ost.51cto.com#bkwz)

https://ost.51cto.com/#bkwz