1

带你玩转HarmonyOS多端钢琴演奏-51CTO.COM

 1 year ago
source link: https://os.51cto.com/article/712962.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

带你玩转HarmonyOS多端钢琴演奏-51CTO.COM

带你玩转HarmonyOS多端钢琴演奏
作者:软通动力HOS 2022-07-01 17:06:49
本项目主要采用HarmonyOS跨端迁移,Fractio等实现钢琴88个按键分为七个区域流转到不同设备上播放对应音频。
63eaab4471834fea3c597507141cfccf4f7273.png

​想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com​

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

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

1、流转按钮

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

2、音域选择按钮

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区
#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

(1)白色按钮E触发效果,如下图所示:

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

(2)黑色按钮d1m触发效果,如下图所示:

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

(3)多指按键触发效果,如下图所示:

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-开源基础软件社区

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

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等技术实现手机端钢琴交互流程,通过该项目,我们能够快速理解数据的“多端协同”和“跨端迁移”,便于在其他项目中快速实现无缝切换的需求。

​想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com​​。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK