背景
写HarmonyOS穿戴应用研发,仅仅是为了一线研发人员提供少许的帮助。
在有些公司,可能因为业务的需要,所以要求研发人员一定要在华为手表穿戴上研发特定的功能,并且理所应当的认为这个开发成本就一个顺手的事情。
开发应用顺手的原因无非就几点
- 宣传 - HarmonyOS已经看似非常成熟
- 实践UI效果 - 研发人员在手机上也能写出绚丽的页面和功能
- 汇报 - 会写H5前端,就能立即开发HarmonyOS应用
穿戴研发核心注意点
- developer.harmonyos.com 网站中的文档,只能阅读3.0版本
- 智能穿戴 和 轻穿戴 应用均采用JS语言开发
- 智能穿戴 对应产品 - HUAWEI WATCH 3
- 轻穿戴 对应产品 - HUAWEI WATCH GT 2 Pro,HUAWEI WATCH GT 3
- 和你产品功能相关的API,切记真机验证
- JS调用Java的文档一定要阅读,因为穿戴设备上的JS API功能相对来说比较少
- JS语言开发的应用启动时的第一个页面是由 congfig.json 文件中的 module -> js -> pages 中的第一个位置文件决定的,比如如下截图中的红色框文件
重点-JS调用Java
场景
录音docs.qq.com/doc/DUmN4VVhBd3NxdExK前往。
效果图
为了快速演示效果,效果图来自IDE
代码说明
这里暂且认为你已阅读过3.0的开发文档,并且已经知晓所有的目录结构和开发语言
js代码包含三部分:1. hml页面 2. js逻辑处理 3. css页面样式
布局
testrecordaudiopage.js
import router from '@system.router';
import featureAbility from '@ohos.ability.featureAbility';
import serviceBridge from '../../generated/ServiceBridge.js';var vm = nullexport default {data: {title: "",button_record: "录音",show_button_record: true,button_play: "播放",show_button_play: true,record: null},onInit() {this.title = "JS 录音";vm = this},onHide() {if(this.record){this.record.stopPlay()this.record.stopRecord()}},swipeEvent(e) {if (e.direction == "right") {router.back()}},async audioRecorderDemo(type) {this.record = new serviceBridge()if (type === 'recordaudio') {if(this.button_record === '录音'){this.record.startRecord().then(value => {if(value.abilityResult == 3){vm.button_record = '停止录音'vm.show_button_play = false}});} else {this.record.stopRecord().then(value => {if(value.abilityResult == 1){vm.button_record = '录音'vm.show_button_play = true}});}} else if (type === 'playaudio') {if(this.button_play === '播放'){this.record.play().then(value => {if(value.abilityResult == 3){vm.button_play = '停止播放'vm.show_button_record = falsevar playTimeStatus = setInterval(()=>{this.record.isPlaying().then(value => {if(!value.abilityResult){vm.button_play = '播放'vm.show_button_record = trueclearInterval(playTimeStatus)}})}, 1000)}})} else {this.record.stopPlay().then(value => {if(value.abilityResult == 1){vm.button_play = '播放'vm.show_button_record = true}})}}}
}
testrecordaudiopage.hml
<div class="container" onswipe="swipeEvent"><text class="title">{{ title }}</text><div class="audiobutton"><button class="buttons_record" show="{{show_button_record}}" onclick="audioRecorderDemo('recordaudio')">{{button_record}}</button><button class="buttons_play" show="{{show_button_play}}" onclick="audioRecorderDemo('playaudio')">{{button_play}}</button></div></div>
testrecordaudiopage.css
.container {width: 100%;flex-direction: column;background-color: black;
}.title {font-size: 25fp;text-align: center;width: 100%;margin: 20px;
}.audiobutton {width: 100%;display: flex;flex-direction: column;align-items: center;
}.buttons_record {width: 45%;height: 15%;font-size: 20fp;text-color: white;background-color: #1F71FF;
}.buttons_play {width: 45%;height: 15%;font-size: 20fp;margin-top: 10vp;text-color: white;background-color: #1F71FF;
}
Java API实现的功能
js/generated/ServiceBridge.js,注意这个文件是自动生成的.
// This file is automatically generated. Do not modify it!
const ABILITY_TYPE_EXTERNAL = 0;
const ABILITY_TYPE_INTERNAL = 1;
const ACTION_SYNC = 0;
const ACTION_ASYNC = 1;
const BUNDLE_NAME = 'com.harvey.hw.wear';
const ABILITY_NAME = 'com.harvey.hw.wear.ServiceBridgeStub';
......
const OPCODE_startRecord = 11;
const OPCODE_stopRecord = 12;
const OPCODE_stopPlay = 13;
const OPCODE_isPlaying = 14;
const OPCODE_play = 15;
const sendRequest = async (opcode, data) => {var action = {};action.bundleName = BUNDLE_NAME;action.abilityName = ABILITY_NAME;action.messageCode = opcode;action.data = data;action.abilityType = ABILITY_TYPE_INTERNAL;action.syncOption = ACTION_SYNC;return FeatureAbility.callAbility(action);
}
class ServiceBridge {......async startRecord() {if (arguments.length != 0) {throw new Error("Method expected 0 arguments, got " + arguments.length);}let data = {};const result = await sendRequest(OPCODE_startRecord, data);return JSON.parse(result);}async stopRecord() {if (arguments.length != 0) {throw new Error("Method expected 0 arguments, got " + arguments.length);}let data = {};const result = await sendRequest(OPCODE_stopRecord, data);return JSON.parse(result);}async stopPlay() {if (arguments.length != 0) {throw new Error("Method expected 0 arguments, got " + arguments.length);}let data = {};const result = await sendRequest(OPCODE_stopPlay, data);return JSON.parse(result);}async isPlaying() {if (arguments.length != 0) {throw new Error("Method expected 0 arguments, got " + arguments.length);}let data = {};const result = await sendRequest(OPCODE_isPlaying, data);return JSON.parse(result);}async play() {if (arguments.length != 0) {throw new Error("Method expected 0 arguments, got " + arguments.length);}let data = {};const result = await sendRequest(OPCODE_play, data);return JSON.parse(result);}
}
export default ServiceBridge;
既然这个文件是自动生成的,那么继续看工程配置。
首先,在 工程根目录/entry/src/main/java 文件夹下的包名(文章举例使用:com.harvey.hw.wear)中创建一个名为ServiceBridge.java的文件
其次,配置初始化和声明ServiceBridge.js文件的注解。为什么注册路径文件是MainAbility, 因为初始化的代码是在MainAbility.java文件中
package com.harvey.hw.wear;import com.harvey.hw.wear.bluetooth.BLEMain;
import ohos.annotation.f2pautogen.ContextInject;
import ohos.annotation.f2pautogen.InternalAbility;
import ohos.app.AbilityContext;
import ohos.bundle.IBundleManager;
import ohos.dcall.DistributedCallManager;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.media.audio.*;import java.io.*;
import java.util.Arrays;@InternalAbility(registerTo = "com.harvey.hw.wear.MainAbility")
public class ServiceBridge {private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, "ServiceBridge");@ContextInjectAbilityContext abilityContext;......//样例:录音/*** 录音 - 启动* @return*/public int startRecord() {if(isRecording){return 1;}if(abilityContext.verifySelfPermission("ohos.permission.MICROPHONE") == IBundleManager.PERMISSION_DENIED){requestPermissions();return 2;}HiLog.error(LABEL_LOG, "RecordServiceAbility::onStart");init();runRecord();return 3;}/*** 录音 - 停止* @return*/public int stopRecord() {if (isRecording && audioCapturer.stop()) {audioCapturer.release();}isRecording = false;return 1;}private AudioRenderer renderer;private static boolean isPlaying = false;/*** 播放 - 停止* @return*/public int stopPlay() {if(isPlaying && renderer.stop()){renderer.release();}isPlaying = false;return 1;}/*** 获取音频播放状态* @return*/public boolean isPlaying(){return isPlaying;}/*** 播放 - 启动* @return*/public int play() {if(isPlaying){return 1;}isPlaying = true;String Path = "/data/data/"+abilityContext.getBundleName()+"/files/record.pcm";File pcmFilePath = new File(Path);if(!pcmFilePath.isFile() || !pcmFilePath.exists()){isPlaying = false;return 2;}new Thread(new Runnable() {@Overridepublic void run() {AudioStreamInfo audioStreamInfo = new AudioStreamInfo.Builder().sampleRate(SAMPLE_RATE).encodingFormat(ENCODING_FORMAT).channelMask(CHANNEL_OUT_MASK).streamUsage(AudioStreamInfo.StreamUsage.STREAM_USAGE_MEDIA).build();AudioRendererInfo audioRendererInfo = new AudioRendererInfo.Builder().audioStreamInfo(audioStreamInfo).audioStreamOutputFlag(AudioRendererInfo.AudioStreamOutputFlag.AUDIO_STREAM_OUTPUT_FLAG_DIRECT_PCM).sessionID(AudioRendererInfo.SESSION_ID_UNSPECIFIED).bufferSizeInBytes(BUFFER_SIZE).isOffload(false).build();renderer = new AudioRenderer(audioRendererInfo, AudioRenderer.PlayMode.MODE_STREAM);AudioInterrupt audioInterrupt = new AudioInterrupt();AudioManager audioManager = new AudioManager();audioInterrupt.setStreamInfo(audioStreamInfo);audioInterrupt.setInterruptListener(new AudioInterrupt.InterruptListener() {@Overridepublic void onInterrupt(int type, int hint) {if (type == AudioInterrupt.INTERRUPT_TYPE_BEGIN&& hint == AudioInterrupt.INTERRUPT_HINT_PAUSE) {renderer.pause();} else if (type == AudioInterrupt.INTERRUPT_TYPE_BEGIN&& hint == AudioInterrupt.INTERRUPT_HINT_NONE) {} else if (type == AudioInterrupt.INTERRUPT_TYPE_END && (hint == AudioInterrupt.INTERRUPT_HINT_NONE|| hint == AudioInterrupt.INTERRUPT_HINT_RESUME)) {renderer.start();} else {HiLog.error(LABEL_LOG, "unexpected type or hint");}}});audioManager.activateAudioInterrupt(audioInterrupt);AudioDeviceDescriptor[] devices = AudioManager.getDevices(AudioDeviceDescriptor.DeviceFlag.INPUT_DEVICES_FLAG);for(AudioDeviceDescriptor des:devices){if(des.getType() == AudioDeviceDescriptor.DeviceType.SPEAKER){renderer.setOutputDevice(des);break;}}renderer.setVolume(1.0f);renderer.start();BufferedInputStream bis1 = null;try {bis1 = new BufferedInputStream(new FileInputStream(pcmFilePath));int minBufferSize = renderer.getMinBufferSize(SAMPLE_RATE, ENCODING_FORMAT,CHANNEL_OUT_MASK);byte[] buffers = new byte[minBufferSize];while ((bis1.read(buffers)) != -1) {if(isPlaying){renderer.write(buffers, 0, buffers.length);renderer.flush();}}} catch (Exception e) {e.printStackTrace();} finally {if (bis1 != null) {try {bis1.close();} catch (IOException e) {e.printStackTrace();}}}stopPlay();}}).start();return 3;}private AudioCapturer audioCapturer;private static final AudioStreamInfo.EncodingFormat ENCODING_FORMAT = AudioStreamInfo.EncodingFormat.ENCODING_PCM_16BIT;private static final AudioStreamInfo.ChannelMask CHANNEL_IN_MASK = AudioStreamInfo.ChannelMask.CHANNEL_IN_STEREO;private static final AudioStreamInfo.ChannelMask CHANNEL_OUT_MASK = AudioStreamInfo.ChannelMask.CHANNEL_OUT_STEREO;private static final int SAMPLE_RATE = 16000;private static final int BUFFER_SIZE = 1024;private static boolean isRecording = false;private void init() {AudioDeviceDescriptor[] devices = AudioManager.getDevices(AudioDeviceDescriptor.DeviceFlag.INPUT_DEVICES_FLAG);AudioDeviceDescriptor currentAudioType = null;for(AudioDeviceDescriptor des:devices){if(des.getType() == AudioDeviceDescriptor.DeviceType.MIC){currentAudioType = des;break;}}AudioCapturerInfo.AudioInputSource source = AudioCapturerInfo.AudioInputSource.AUDIO_INPUT_SOURCE_MIC;AudioStreamInfo audioStreamInfo = new AudioStreamInfo.Builder().audioStreamFlag(AudioStreamInfo.AudioStreamFlag.AUDIO_STREAM_FLAG_AUDIBILITY_ENFORCED).encodingFormat(ENCODING_FORMAT).channelMask(CHANNEL_IN_MASK).streamUsage(AudioStreamInfo.StreamUsage.STREAM_USAGE_MEDIA).sampleRate(SAMPLE_RATE).build();AudioCapturerInfo audioCapturerInfo = new AudioCapturerInfo.Builder().audioStreamInfo(audioStreamInfo).audioInputSource(source).build();audioCapturer = new AudioCapturer(audioCapturerInfo, currentAudioType);}private void runRecord() {isRecording = true;new Thread(new Runnable() {@Overridepublic void run() {//启动录音audioCapturer.start();File file = new File("/data/data/"+abilityContext.getBundleName()+"/files/record.pcm");if (file.isFile()) {file.delete();}try (FileOutputStream outputStream = new FileOutputStream(file)) {byte[] bytes = new byte[BUFFER_SIZE];while (audioCapturer.read(bytes, 0, bytes.length) != -1) {outputStream.write(bytes);bytes = new byte[BUFFER_SIZE];outputStream.flush();if(!isRecording){outputStream.close();break;}}} catch (IOException exception) {HiLog.error(LABEL_LOG, "record exception," + exception.getMessage());}}}).start();}private void requestPermissions() {String[] permissions = {"ohos.permission.MICROPHONE"};abilityContext.requestPermissionsFromUser(Arrays.stream(permissions).filter(permission -> abilityContext.verifySelfPermission(permission) != IBundleManager.PERMISSION_GRANTED).toArray(String[]::new), 0);}}
第三,如何初始化ServiceBridge.java呢?创建一个中间文件,命名为ServiceBridgeStub.java。
public class MainAbility extends AceAbility {@Overridepublic void onStart(Intent intent) {ServiceBridgeStub.register(this);......}......
}
ServiceBridgeStub.java
package com.harvey.hw.wear;import java.lang.Object;
import java.lang.String;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import ohos.ace.ability.AceInternalAbility;
import ohos.app.AbilityContext;
import ohos.rpc.IRemoteObject;
import ohos.rpc.MessageOption;
import ohos.rpc.MessageParcel;
import ohos.rpc.RemoteException;
import ohos.utils.zson.ZSONObject;public class ServiceBridgeStub extends AceInternalAbility {public static final String BUNDLE_NAME = "com.harvey.hw.wear";public static final String ABILITY_NAME = "com.harvey.hw.wear.ServiceBridgeStub";public static final int ERROR = -1;public static final int SUCCESS = 0;......public static final int OPCODE_startRecord = 11;public static final int OPCODE_stopRecord = 12;public static final int OPCODE_stopPlay = 13;public static final int OPCODE_isPlaying = 14;public static final int OPCODE_play = 15;private static ServiceBridgeStub instance;private ServiceBridge service;private AbilityContext abilityContext;public ServiceBridgeStub() {super(BUNDLE_NAME, ABILITY_NAME);}public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply,MessageOption option) {Map<String, Object> result = new HashMap<String, Object>();switch(code) {......case OPCODE_startRecord: {java.lang.String zsonStr = data.readString();ZSONObject zsonObject = ZSONObject.stringToZSON(zsonStr);result.put("code", SUCCESS);result.put("abilityResult", service.startRecord());break;}case OPCODE_stopRecord: {java.lang.String zsonStr = data.readString();ZSONObject zsonObject = ZSONObject.stringToZSON(zsonStr);result.put("code", SUCCESS);result.put("abilityResult", service.stopRecord());break;}case OPCODE_stopPlay: {java.lang.String zsonStr = data.readString();ZSONObject zsonObject = ZSONObject.stringToZSON(zsonStr);result.put("code", SUCCESS);result.put("abilityResult", service.stopPlay());break;}case OPCODE_isPlaying: {java.lang.String zsonStr = data.readString();ZSONObject zsonObject = ZSONObject.stringToZSON(zsonStr);result.put("code", SUCCESS);result.put("abilityResult", service.isPlaying());break;}case OPCODE_play: {java.lang.String zsonStr = data.readString();ZSONObject zsonObject = ZSONObject.stringToZSON(zsonStr);result.put("code", SUCCESS);result.put("abilityResult", service.play());break;}default: reply.writeString("Opcode is not defined!");return false;}return sendResult(reply, result, option.getFlags() == MessageOption.TF_SYNC);}private boolean sendResult(MessageParcel reply, Map<String, Object> result, boolean isSync) {if (isSync) {reply.writeString(ZSONObject.toZSONString(result));} else {MessageParcel response = MessageParcel.obtain();response.writeString(ZSONObject.toZSONString(result));IRemoteObject remoteReply = reply.readRemoteObject();try {remoteReply.sendRequest(0, response, MessageParcel.obtain(), new MessageOption());response.reclaim();} catch (RemoteException exception) {return false;}}return true;}public static void register(AbilityContext abilityContext) {instance = new ServiceBridgeStub();instance.onRegister(abilityContext);}private void onRegister(AbilityContext abilityContext) {this.abilityContext = abilityContext;this.service = new ServiceBridge();this.setInternalAbilityHandler(this::onRemoteRequest);try {Field field = ServiceBridge.class.getDeclaredField("abilityContext");field.setAccessible(true);field.set(this.service, abilityContext);field.setAccessible(false);} catch (NoSuchFieldException | IllegalAccessException e) {ohos.hiviewdfx.HiLog.error(new ohos.hiviewdfx.HiLogLabel(0, 0, null), "context injection fail.");}}public static void deregister() {instance.onDeregister();}private void onDeregister() {abilityContext = null;this.setInternalAbilityHandler(null);}
}
最后,对自动生成js代码的功能,做编译配置
entry 主模块的 build.gradle 文件中添加如下代码
apply plugin: 'com.huawei.ohos.hap'
apply plugin: 'com.huawei.ohos.decctest'ohos {compileSdkVersion 6defaultConfig {compatibleSdkVersion 6// 在文件头部定义JS模板代码生成路径def jsOutputDir = project.file("src/main/js/default/generated").toString()// 在ohos -> defaultConfig中设置JS模板代码生成路径javaCompileOptions {annotationProcessorOptions {arguments = ["jsOutputDir": jsOutputDir] // JS模板代码生成赋值}}}......compileOptions {f2pautogenEnabled true // 此处为启用js2java-codegen工具的开关}}......
至此,关于穿戴应用研发的重点介绍已完成。
如果你还有兴趣,可以尝试实践一下BLE传输数据
结尾
这是去年研发穿戴应用的一个Demo应用工程结构
备注
看的HarmonyOS文档多了, 对于开发语言和文档中的描述容易产生混淆,这里附上一张简单的HarmonyOS 3.0 到 3.1版本的说明
鸿蒙开发岗位需要掌握那些核心要领?
目前还有很多小伙伴不知道要学习哪些鸿蒙技术?不知道重点掌握哪些?为了避免学习时频繁踩坑,最终浪费大量时间的。
自己学习时必须要有一份实用的鸿蒙(Harmony NEXT)资料非常有必要。 这里我推荐,根据鸿蒙开发官网梳理与华为内部人员的分享总结出的开发文档。内容包含了:【ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、Harmony南向开发、鸿蒙项目实战】等技术知识点。
废话就不多说了,接下来好好看下这份资料。
如果你是一名Android、Java、前端等等开发人员,想要转入鸿蒙方向发展。可以直接领取这份资料辅助你的学习。鸿蒙OpenHarmony知识←前往。下面是鸿蒙开发的学习路线图。
针对鸿蒙成长路线打造的鸿蒙学习文档。鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,帮助大家在技术的道路上更进一步。
其中内容包含:
《鸿蒙开发基础》鸿蒙OpenHarmony知识←前往
- ArkTS语言
- 安装DevEco Studio
- 运用你的第一个ArkTS应用
- ArkUI声明式UI开发
- .……
《鸿蒙开发进阶》鸿蒙OpenHarmony知识←前往
- Stage模型入门
- 网络管理
- 数据管理
- 电话服务
- 分布式应用开发
- 通知与窗口管理
- 多媒体技术
- 安全技能
- 任务管理
- WebGL
- 国际化开发
- 应用测试
- DFX面向未来设计
- 鸿蒙系统移植和裁剪定制
- ……
《鸿蒙开发实战》鸿蒙OpenHarmony知识←前往
- ArkTS实践
- UIAbility应用
- 网络案例
- ……
最后
鸿蒙是完全具备无与伦比的机遇和潜力的;预计到年底将有 5,000 款的应用完成原生鸿蒙开发,这么多的应用需要开发,也就意味着需要有更多的鸿蒙人才。鸿蒙开发工程师也将会迎来爆发式的增长,学习鸿蒙势在必行!