第36集:uni-app 传感器使用
核心知识点
1. 传感器 API
uni-app 提供了完整的传感器 API,用于访问设备的各种传感器。主要包括以下几类:
- 加速度传感器:
uni.onAccelerometerChange()、uni.startAccelerometer()、uni.stopAccelerometer() - 陀螺仪传感器:
uni.onGyroscopeChange()、uni.startGyroscope()、uni.stopGyroscope() - 指南针传感器:
uni.onCompassChange()、uni.startCompass()、uni.stopCompass() - 光线传感器:部分设备支持,通过原生插件实现
- 距离传感器:部分设备支持,通过原生插件实现
2. 加速度传感器
加速度传感器用于检测设备的加速度变化,主要参数包括:
- x:X 轴方向的加速度分量
- y:Y 轴方向的加速度分量
- z:Z 轴方向的加速度分量
加速度传感器的应用场景包括:
- 计步功能:通过检测设备的震动来计数步数
- 摇晃功能:通过检测设备的摇晃来触发特定操作
- 游戏控制:通过设备的倾斜来控制游戏角色
3. 陀螺仪传感器
陀螺仪传感器用于检测设备的旋转角速度,主要参数包括:
- x:X 轴方向的旋转角速度
- y:Y 轴方向的旋转角速度
- z:Z 轴方向的旋转角速度
陀螺仪传感器的应用场景包括:
- 水平仪:检测设备是否水平
- 3D 游戏:实现更精确的游戏控制
- AR 应用:跟踪设备的位置和方向
4. 指南针传感器
指南针传感器用于检测设备的方向,主要参数包括:
- direction:设备指向的方向,以角度为单位,0 表示正北,90 表示正东,180 表示正南,270 表示正西
指南针传感器的应用场景包括:
- 导航应用:显示设备的朝向
- 地图应用:根据设备方向旋转地图
- 增强现实:根据设备方向显示相关信息
实用案例分析
案例1:实现计步功能
功能需求
实现一个计步功能,包括以下功能:
- 实时计步
- 显示步数、距离、卡路里消耗
- 计步历史记录
- 目标设置
代码实现
<template>
<view class="container">
<view class="header">
<text class="title">计步器</text>
</view>
<view class="main-content">
<view class="step-counter">
<text class="step-count">{{ stepCount }}</text>
<text class="step-label">步数</text>
</view>
<view class="stats-container">
<view class="stat-item">
<text class="stat-value">{{ distance.toFixed(2) }}</text>
<text class="stat-label">公里</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ calories.toFixed(1) }}</text>
<text class="stat-label">卡路里</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ activeTime }}</text>
<text class="stat-label">分钟</text>
</view>
</view>
<view class="controls">
<button @click="startStepCount" type="primary" :disabled="isCounting">开始计步</button>
<button @click="stopStepCount" type="default" :disabled="!isCounting">停止计步</button>
<button @click="resetStepCount" type="warn">重置</button>
</view>
<view class="goal-setting">
<text class="section-title">目标设置</text>
<view class="goal-slider">
<text>每日目标: {{ stepGoal }} 步</text>
<slider v-model="stepGoal" min="1000" max="20000" step="1000" show-value></slider>
<button @click="saveGoal" type="primary" size="mini">保存目标</button>
</view>
<view class="goal-progress">
<view class="progress-bar">
<view class="progress-fill" :style="{ width: progressPercentage + '%' }"></view>
</view>
<text class="progress-text">{{ progressPercentage }}% 完成</text>
</view>
</view>
</view>
<view class="history-section">
<text class="section-title">计步历史</text>
<view class="history-list">
<view v-for="(record, index) in stepHistory" :key="index" class="history-item">
<text class="history-date">{{ record.date }}</text>
<text class="history-steps">{{ record.steps }} 步</text>
</view>
<view v-if="stepHistory.length === 0" class="empty-history">
<text>暂无历史记录</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
isCounting: false,
stepCount: 0,
stepGoal: 10000,
distance: 0,
calories: 0,
activeTime: 0,
progressPercentage: 0,
stepHistory: [],
lastAcceleration: {
x: 0,
y: 0,
z: 0
},
lastStepTime: 0,
stepThreshold: 15, // 步数检测阈值
stepCooldown: 300, // 步数检测冷却时间(毫秒)
timer: null
};
},
onLoad() {
// 加载历史数据
this.loadHistory();
// 加载目标设置
this.loadGoal();
},
onUnload() {
// 停止计步
this.stopStepCount();
},
methods: {
// 开始计步
startStepCount() {
if (this.isCounting) return;
uni.showToast({ title: '开始计步', icon: 'success' });
this.isCounting = true;
// 记录开始时间
this.lastStepTime = Date.now();
// 启动加速度传感器
uni.startAccelerometer({
interval: 'normal',
success: (res) => {
console.log('加速度传感器启动成功');
// 监听加速度变化
uni.onAccelerometerChange((res) => {
this.detectStep(res);
});
},
fail: (err) => {
console.error('加速度传感器启动失败:', err);
uni.showToast({ title: '传感器启动失败', icon: 'none' });
this.isCounting = false;
}
});
// 启动计时器,更新活动时间
this.timer = setInterval(() => {
if (this.isCounting) {
this.activeTime++;
}
}, 60000); // 每分钟更新一次
},
// 停止计步
stopStepCount() {
if (!this.isCounting) return;
uni.showToast({ title: '停止计步', icon: 'success' });
this.isCounting = false;
// 停止加速度传感器
uni.stopAccelerometer();
// 清除计时器
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
// 保存今日数据
this.saveTodayData();
},
// 重置计步
resetStepCount() {
uni.showModal({
title: '重置计步',
content: '确定要重置今日步数吗?',
success: (res) => {
if (res.confirm) {
this.stepCount = 0;
this.distance = 0;
this.calories = 0;
this.activeTime = 0;
this.progressPercentage = 0;
uni.showToast({ title: '已重置', icon: 'success' });
}
}
});
},
// 检测步数
detectStep(acceleration) {
const now = Date.now();
// 计算与上次加速度的差值
const deltaX = Math.abs(acceleration.x - this.lastAcceleration.x);
const deltaY = Math.abs(acceleration.y - this.lastAcceleration.y);
const deltaZ = Math.abs(acceleration.z - this.lastAcceleration.z);
// 计算合加速度变化
const delta = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ);
// 更新上次加速度
this.lastAcceleration = acceleration;
// 检测是否达到步数阈值,并且在冷却时间之后
if (delta > this.stepThreshold && now - this.lastStepTime > this.stepCooldown) {
this.stepCount++;
this.lastStepTime = now;
// 更新距离和卡路里
this.updateStats();
}
},
// 更新统计数据
updateStats() {
// 假设每步距离为0.7米
this.distance = this.stepCount * 0.7 / 1000;
// 假设每步消耗0.04卡路里
this.calories = this.stepCount * 0.04;
// 更新进度百分比
this.progressPercentage = Math.min(Math.round((this.stepCount / this.stepGoal) * 100), 100);
},
// 保存目标
saveGoal() {
uni.setStorageSync('stepGoal', this.stepGoal);
uni.showToast({ title: '目标已保存', icon: 'success' });
},
// 加载目标
loadGoal() {
const savedGoal = uni.getStorageSync('stepGoal');
if (savedGoal) {
this.stepGoal = savedGoal;
}
},
// 保存今日数据
saveTodayData() {
const today = new Date().toLocaleDateString();
const todayData = {
date: today,
steps: this.stepCount,
distance: this.distance,
calories: this.calories,
activeTime: this.activeTime
};
// 加载历史数据
let history = uni.getStorageSync('stepHistory') || [];
// 检查是否已有今日数据
const todayIndex = history.findIndex(item => item.date === today);
if (todayIndex >= 0) {
// 更新今日数据
history[todayIndex] = todayData;
} else {
// 添加今日数据
history.push(todayData);
}
// 保存历史数据
uni.setStorageSync('stepHistory', history);
// 更新历史记录
this.loadHistory();
},
// 加载历史数据
loadHistory() {
const history = uni.getStorageSync('stepHistory') || [];
// 按日期倒序排序
history.sort((a, b) => new Date(b.date) - new Date(a.date));
// 只显示最近7天的数据
this.stepHistory = history.slice(0, 7);
}
}
};
</script>
<style scoped>
.container {
padding: 20rpx;
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
margin-bottom: 30rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
text-align: center;
display: block;
}
.main-content {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.step-counter {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40rpx;
}
.step-count {
font-size: 80rpx;
font-weight: bold;
color: #007aff;
margin-bottom: 10rpx;
}
.step-label {
font-size: 32rpx;
color: #666;
}
.stats-container {
display: flex;
justify-content: space-around;
margin-bottom: 40rpx;
padding: 20rpx 0;
border-top: 1rpx solid #eaeaea;
border-bottom: 1rpx solid #eaeaea;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 5rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.controls {
display: flex;
justify-content: space-between;
margin-bottom: 40rpx;
}
.controls button {
flex: 1;
margin: 0 10rpx;
}
.goal-setting {
margin-top: 30rpx;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: block;
}
.goal-slider {
margin-bottom: 20rpx;
}
.goal-slider text {
display: block;
margin-bottom: 15rpx;
font-size: 26rpx;
}
.goal-progress {
margin-top: 20rpx;
}
.progress-bar {
height: 20rpx;
background-color: #eaeaea;
border-radius: 10rpx;
overflow: hidden;
margin-bottom: 10rpx;
}
.progress-fill {
height: 100%;
background-color: #07c160;
border-radius: 10rpx;
transition: width 0.3s ease;
}
.progress-text {
font-size: 24rpx;
color: #666;
text-align: right;
}
.history-section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.history-list {
margin-top: 20rpx;
}
.history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #eaeaea;
}
.history-date {
font-size: 26rpx;
color: #333;
}
.history-steps {
font-size: 26rpx;
font-weight: bold;
color: #007aff;
}
.empty-history {
text-align: center;
padding: 50rpx 0;
color: #999;
font-size: 26rpx;
}
</style>案例2:实现水平仪功能
功能需求
实现一个水平仪功能,包括以下功能:
- 实时显示设备倾斜角度
- 显示水平状态
- 校准功能
- 角度数值显示
代码实现
<template>
<view class="container">
<view class="header">
<text class="title">水平仪</text>
<button @click="calibrate" type="primary" size="mini">校准</button>
</view>
<view class="level-container">
<view class="level-meter" :style="levelStyle">
<view class="bubble"></view>
</view>
<view class="angle-info">
<view class="angle-item">
<text class="angle-label">X轴角度:</text>
<text class="angle-value">{{ angleX.toFixed(1) }}°</text>
</view>
<view class="angle-item">
<text class="angle-label">Y轴角度:</text>
<text class="angle-value">{{ angleY.toFixed(1) }}°</text>
</view>
<view class="angle-item">
<text class="angle-label">Z轴角度:</text>
<text class="angle-value">{{ angleZ.toFixed(1) }}°</text>
</view>
</view>
<view class="level-status" :class="{ level: isLevel }">
<text>{{ isLevel ? '水平' : '未水平' }}</text>
</view>
</view>
<view class="instructions">
<text class="instruction-title">使用说明</text>
<text class="instruction-text">1. 将手机放置在需要测量的平面上</text>
<text class="instruction-text">2. 观察气泡是否位于中心位置</text>
<text class="instruction-text">3. 如需要校准,请点击校准按钮</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
angleX: 0,
angleY: 0,
angleZ: 0,
isLevel: false,
calibration: {
x: 0,
y: 0,
z: 0
},
levelThreshold: 0.5 // 水平阈值(度)
};
},
computed: {
// 水平仪样式
levelStyle() {
// 计算气泡位置,范围限制在 -40% 到 40%
const bubbleX = Math.max(-40, Math.min(40, this.angleY * 5));
const bubbleY = Math.max(-40, Math.min(40, this.angleX * 5));
return {
transform: `translate(${bubbleX}%, ${bubbleY}%)`
};
}
},
onLoad() {
// 启动加速度传感器
this.startSensors();
},
onUnload() {
// 停止传感器
this.stopSensors();
},
methods: {
// 启动传感器
startSensors() {
uni.startAccelerometer({
interval: 'normal',
success: (res) => {
console.log('加速度传感器启动成功');
// 监听加速度变化
uni.onAccelerometerChange((res) => {
this.calculateAngle(res);
});
},
fail: (err) => {
console.error('加速度传感器启动失败:', err);
uni.showToast({ title: '传感器启动失败', icon: 'none' });
}
});
},
// 停止传感器
stopSensors() {
uni.stopAccelerometer();
},
// 计算角度
calculateAngle(acceleration) {
// 应用校准值
const calibratedX = acceleration.x - this.calibration.x;
const calibratedY = acceleration.y - this.calibration.y;
const calibratedZ = acceleration.z - this.calibration.z;
// 计算与重力加速度的夹角
// 注意:这里的计算假设设备处于静止状态
const gravity = Math.sqrt(calibratedX * calibratedX + calibratedY * calibratedY + calibratedZ * calibratedZ);
// 计算各轴角度
this.angleX = Math.atan2(calibratedX, calibratedZ) * 180 / Math.PI;
this.angleY = Math.atan2(calibratedY, calibratedZ) * 180 / Math.PI;
this.angleZ = Math.atan2(calibratedY, calibratedX) * 180 / Math.PI;
// 检查是否水平
this.isLevel = Math.abs(this.angleX) < this.levelThreshold && Math.abs(this.angleY) < this.levelThreshold;
},
// 校准水平仪
calibrate() {
uni.showModal({
title: '校准水平仪',
content: '请将手机放置在水平面上,然后点击确定开始校准',
success: (res) => {
if (res.confirm) {
// 记录当前加速度值作为校准值
uni.onAccelerometerChange((res) => {
this.calibration = {
x: res.x,
y: res.y,
z: res.z
};
uni.showToast({ title: '校准成功', icon: 'success' });
// 移除监听器
uni.offAccelerometerChange();
// 重新启动监听器
this.startSensors();
});
}
}
});
}
}
};
</script>
<style scoped>
.container {
padding: 20rpx;
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
}
.level-container {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.level-meter {
width: 100%;
height: 300rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
position: relative;
overflow: hidden;
margin-bottom: 30rpx;
border: 2rpx solid #eaeaea;
}
.level-meter::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 2rpx;
background-color: #007aff;
transform: translateY(-50%);
}
.level-meter::after {
content: '';
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2rpx;
background-color: #007aff;
transform: translateX(-50%);
}
.bubble {
position: absolute;
width: 60rpx;
height: 60rpx;
background-color: rgba(0, 122, 255, 0.6);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 10rpx rgba(0, 122, 255, 0.5);
}
.angle-info {
margin-bottom: 30rpx;
}
.angle-item {
display: flex;
justify-content: space-between;
margin-bottom: 15rpx;
padding: 10rpx 0;
border-bottom: 1rpx solid #eaeaea;
}
.angle-label {
font-size: 26rpx;
color: #666;
}
.angle-value {
font-size: 26rpx;
font-weight: bold;
color: #007aff;
}
.level-status {
text-align: center;
padding: 20rpx;
border-radius: 8rpx;
background-color: #f5f5f5;
font-size: 28rpx;
font-weight: bold;
color: #ff3b30;
}
.level-status.level {
background-color: #e6f7ee;
color: #07c160;
}
.instructions {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.instruction-title {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 20rpx;
display: block;
}
.instruction-text {
font-size: 24rpx;
color: #666;
margin-bottom: 10rpx;
display: block;
line-height: 1.5;
}
</style>学习目标
通过本集的学习,你应该能够:
- 掌握 uni-app 传感器 API 的使用方法
- 理解加速度传感器、陀螺仪、指南针的工作原理
- 实现基于传感器的计步功能
- 实现基于传感器的水平仪功能
- 开发基于传感器的其他应用
小结
本集详细介绍了 uni-app 中的传感器功能,包括加速度传感器、陀螺仪、指南针等核心知识点,并通过两个实际案例展示了如何实现计步功能和水平仪功能。
传感器是移动应用开发中的重要组成部分,掌握这些技能可以帮助你开发出更加丰富和实用的应用。在实际开发中,你还需要注意传感器的精度、耗电等问题,以确保应用的稳定性和用户体验。
通过本集的学习,你已经具备了在 uni-app 中使用传感器功能的基本能力,可以开始开发需要传感器支持的应用了。