#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏 原创 精华

软通动力HOS
发布于 2022-6-30 09:22
浏览
11收藏

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

前言

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

项目介绍

本项目主要采用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.钢琴按键按下触发效果

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-鸿蒙开发者社区

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

#夏日挑战赛#带你玩转HarmonyOS多端钢琴演奏-鸿蒙开发者社区

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

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

更多原创内容请关注软通动力OpenHarmony学院

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2022-6-30 09:22:53修改
15
收藏 11
回复
举报
6条回复
按时间正序
/
按时间倒序
软通田可辉
软通田可辉

这个真心厉害👍

回复
2022-6-30 10:10:06
芒果爱学习
芒果爱学习

疫情居家亲子教育利器👍

回复
2022-6-30 10:11:52
粉粉gg
粉粉gg

感谢老师分享👌

回复
2022-6-30 10:14:00
江湖人称鸿老师
江湖人称鸿老师

真棒👍

回复
2022-6-30 10:16:51
娜子开心
娜子开心

感谢老师分享👍

回复
2022-6-30 10:24:33
wx62b17c6f2e9ff
wx62b17c6f2e9ff

牛逼牛逼

回复
2022-7-1 17:28:19
回复
    相关推荐