最近一个项目中需要在App端做摇一摇交互,同时要求支持自定义震动模式。起初找了几个现成的插件,要么只支持安卓,要么没有震动时长控制,而且代码混杂着原生语言,改一个参数都得重新编译。后来趁着uniapp新推出UTS(Universal TypeScript)插件的机会,干脆自己写了一个插件——用同一套TypeScript风格的语法,直接调起安卓和iOS底层的振动器与加速度计,运行效率跟原生一样,还能完美融入uniapp的跨端工程。
这篇文章就把打包过程中踩过的坑和最终落地的完整代码整理出来,让你读完就能上手开发自己的原生插件。
UTS插件是什么,能做什么
UTS全称是“统一类型系统”,它是DCloud在uniapp中推出的一个插件开发方案。简单说,就是用类似TypeScript的语法来写原生代码,编译后直接生成安卓的Kotlin/Java或者iOS的Swift代码,没有额外的桥接层,所以性能等同于原生模块。你可以在UTS插件里调用系统的震动服务、传感器、文件系统,还能集成第三方原生SDK。
对我们来说,UTS最大的好处是学习成本低——不需要重新学Kotlin或Swift,只要会TypeScript基本语法,再了解一点平台API函数就能开写。而且同一个插件可以同时编译出安卓和iOS两个版本,省去双份代码的维护成本。
环境准备与插件目录结构
首先确认HBuilderX版本在3.7.0以上。然后在项目根目录下右键新建“UTS插件”,选择模板“原生能力扩展”,取名为sensor-utils。创建完成后会得到如下结构:
uni_modules/sensor-utils/
├── utssdk/
│ ├── app-android/
│ │ └── index.uts // 安卓侧实现
│ ├── app-ios/
│ │ └── index.uts // iOS侧实现
│ └── interface.uts // 公共接口定义(可选)
├── package.json
└── ...
公共的类型声明放在interface.uts里,两边共享。接口里定义震动和加速度计的方法签名,这样我们在uniapp页面里获得的自动补全就会很准确。
第一步:定义公共接口
打开utssdk/interface.uts,写入要暴露给JavaScript的方法:
export class VibrationUtils {
/**
* 触发指定时长的震动(毫秒)
*/
static vibrate(duration: number): void;
/**
* 触发预设的震动模式(数组元素为振动和停歇交替的时长)
*/
static vibratePattern(pattern: number[], repeat: number): void;
}
export type AccelerometerData = {
x: number;
y: number;
z: number;
timestamp: number;
};
export type AccelerometerCallback = (data: AccelerometerData) => void;
export class AccelerometerUtils {
/**
* 开始加速度计监听
*/
static startListening(callback: AccelerometerCallback, interval: string): void;
/**
* 停止监听
*/
static stopListening(): void;
}
这里我们将震动和加速度计分别封装成两个静态类,方便在页面里直接VibrationUtils.vibrate(500),非常直觉。
第二步:安卓侧实现
安卓的震动服务来自Vibrator类,加速度计使用SensorManager。在app-android/index.uts中填充:
import { Context, Vibrator, SensorManager, Sensor, SensorEvent, SystemClock } from 'android';
import { VibrationUtils, AccelerometerUtils, AccelerometerCallback } from '../interface.uts';
let vibrator: Vibrator | null = null;
let sensorManager: SensorManager | null = null;
let accelerometer: Sensor | null = null;
let currentCallback: AccelerometerCallback | null = null;
// 振动实现
VibrationUtils.vibrate = (duration: number) => {
if (vibrator == null) {
vibrator = UTSAndroid.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator;
}
vibrator!.vibrate(duration);
};
VibrationUtils.vibratePattern = (pattern: number[], repeat: number) => {
if (vibrator == null) {
vibrator = UTSAndroid.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator;
}
const longArray: number[] = pattern;
vibrator!.vibrate(longArray, repeat);
};
// 加速度计实现
AccelerometerUtils.startListening = (callback: AccelerometerCallback, interval: string) => {
if (sensorManager == null) {
sensorManager = UTSAndroid.getSystemService(Context.SENSOR_SERVICE) as SensorManager;
accelerometer = sensorManager!.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
if (accelerometer == null) {
console.error('设备不支持加速度计');
return;
}
currentCallback = callback;
const samplingPeriodUs = interval === 'game' ? SensorManager.SENSOR_DELAY_GAME : SensorManager.SENSOR_DELAY_UI;
sensorManager!.registerListener({
onSensorChanged(event: SensorEvent) {
if (currentCallback != null) {
currentCallback({
x: event.values[0],
y: event.values[1],
z: event.values[2],
timestamp: SystemClock.uptimeMillis()
});
}
},
onAccuracyChanged(sensor: Sensor, accuracy: number) {}
}, accelerometer, samplingPeriodUs);
};
AccelerometerUtils.stopListening = () => {
if (sensorManager != null && accelerometer != null) {
sensorManager.unregisterListener(accelerometer);
currentCallback = null;
}
};
这里的UTSAndroid是UTS环境提供的全局对象,可以直接获取系统服务。SystemClock.uptimeMillis()提供相对开机时间,比Date.now()更准确。
第三步:iOS侧实现
iOS端的震动比较简单,通过UIImpactFeedbackGenerator还可以产生轻中重等不同触感,不过我们这里保持与安卓一致的时长震动,使用AudioServicesPlaySystemSound和Timer来模拟持续震动。加速度计则用CMMotionManager。
import { CMMotionManager, NSOperationQueue } from 'ios';
import { VibrationUtils, AccelerometerUtils, AccelerometerCallback } from '../interface.uts';
let motionManager: CMMotionManager | null = null;
// 震动:简易版,复杂模式可用UIFeedbackGenerator
VibrationUtils.vibrate = (duration: number) => {
// iOS没有简单持续震动API,这里用周期性短震模拟
// 仅示意,生产环境建议使用 UIImpactFeedbackGenerator
const timer = new NSTimer.scheduledTimerWithTimeInterval(0.5, true, () => {
AudioServicesPlaySystemSound(1519); // 轻震
});
setTimeout(() => {
timer.invalidate();
}, duration);
};
VibrationUtils.vibratePattern = (pattern: number[], repeat: number) => {
// 按pattern依次播放短震,循环repeat次,实现略
};
// 加速度计
AccelerometerUtils.startListening = (callback: AccelerometerCallback, interval: string) => {
if (motionManager == null) {
motionManager = new CMMotionManager();
}
if (motionManager!.isAccelerometerAvailable) {
const updateInterval = interval === 'game' ? 0.02 : 0.1;
motionManager!.accelerometerUpdateInterval = updateInterval;
motionManager!.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue, (data, error) => {
if (data != null) {
callback({
x: data.acceleration.x,
y: data.acceleration.y,
z: data.acceleration.z,
timestamp: Date.now()
});
}
});
}
};
AccelerometerUtils.stopListening = () => {
motionManager?.stopAccelerometerUpdates();
};
这里模拟震动用了旧式API,如需现代触感可以引入UIKit的UISelectionFeedbackGenerator等,但代码量会稍多,核心思路不变。
第四步:在uniapp页面中使用插件
插件编译后,我们在页面里直接导入即可,跟使用JavaScript模块完全一样:
<template>
<view class="container">
<button @click="doVibrate">重震一下</button>
<button @click="startAccel">开始摇一摇监看</button>
<text>X: {{ accel.x }}, Y: {{ accel.y }}, Z: {{ accel.z }}</text>
</template>
<script setup>
import { VibrationUtils, AccelerometerUtils } from '@/uni_modules/sensor-utils';
import { reactive } from 'vue';
const accel = reactive({ x: 0, y: 0, z: 0 });
let shakeWatching = false;
function doVibrate() {
VibrationUtils.vibrate(600); // 震动600毫秒
}
function startAccel() {
if (shakeWatching) {
AccelerometerUtils.stopListening();
shakeWatching = false;
return;
}
AccelerometerUtils.startListening((data) => {
accel.x = data.x;
accel.y = data.y;
accel.z = data.z;
// 简易摇一摇判断:加速度向量模长超过阈值
const magnitude = Math.sqrt(data.x ** 2 + data.y ** 2 + data.z ** 2);
if (magnitude > 15) {
uni.showToast({ title: '摇一摇触发' });
// 可以配合震动
VibrationUtils.vibrate(100);
}
}, 'game');
shakeWatching = true;
}
</script>
上面代码直接拿到了设备传感器数据,并且实现了摇一摇交互。全程没有离开过TypeScript的语法体系,安卓和iOS只是编译目标。
调试与真机测试
在HBuilderX中选择运行到真实iOS或安卓设备,UTS插件会自动编译为对应平台的代码并集成到安装包里。如果编译报错,通常是指定的原生类型名不对,可以在uts-sdk文档中查内置的声明列表。另外注意震动和加速度计的权限:安卓需要VIBRATE权限,HBuilderX默认已添加;iOS的加速度计权限不需要特别授权。
还有一个常见的坑:iOS模拟器上没有加速度计,真机调试时必须选择实体设备。
几种典型的应用扩展
掌握了基础框架后,可以很容易地扩展这个插件:
- 增加陀螺仪数据,返回
{ pitch, roll, yaw }。 - 封装更高阶的触感反馈,利用iOS的
UIImpactFeedbackGenerator产生轻、中、重三种力度。 - 在安卓端读取步数传感器,做计步功能。
所有新增能力仍然写在interface.uts里,然后分别在两端实现,JavaScript调用层完全不变。
为什么用UTS而不是cordova或者webview桥接
之前我们用过cordova插件和webview内置桥,它们的问题在于每一个调用都要序列化参数到字符串再解析,高频传感器数据(每秒几十次回调)性能很差,经常卡断。UTS编译后直接跑在原生线程里,数据是二进制传递,不存在这种瓶颈,这也是加速度计这类场景最适合UTS的原因。
总结
打包这个震动与加速度计插件的过程中,最让我意外的是代码量之少——安卓和iOS两侧加起来都没超过150行,却实现了一个可以直接跨端复用的原生能力模块。UTS把以前必须分开维护的Kotlin和Swift工程,拉进了同一个TypeScript世界里,又保留了原生性能,这几乎是目前跨端插件开发中最省力的方案。
如果你也在项目中碰到“官方API不够用,又不想离开uniapp生态”的时刻,不妨试试UTS,自己动手封装一个插件,那种“写一次代码两端跑”的畅快感,比搜索三天得到的半成品插件要可靠得多。

