第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:实现计步功能

功能需求

实现一个计步功能,包括以下功能:

  1. 实时计步
  2. 显示步数、距离、卡路里消耗
  3. 计步历史记录
  4. 目标设置

代码实现

<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:实现水平仪功能

功能需求

实现一个水平仪功能,包括以下功能:

  1. 实时显示设备倾斜角度
  2. 显示水平状态
  3. 校准功能
  4. 角度数值显示

代码实现

<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>

学习目标

通过本集的学习,你应该能够:

  1. 掌握 uni-app 传感器 API 的使用方法
  2. 理解加速度传感器、陀螺仪、指南针的工作原理
  3. 实现基于传感器的计步功能
  4. 实现基于传感器的水平仪功能
  5. 开发基于传感器的其他应用

小结

本集详细介绍了 uni-app 中的传感器功能,包括加速度传感器、陀螺仪、指南针等核心知识点,并通过两个实际案例展示了如何实现计步功能和水平仪功能。

传感器是移动应用开发中的重要组成部分,掌握这些技能可以帮助你开发出更加丰富和实用的应用。在实际开发中,你还需要注意传感器的精度、耗电等问题,以确保应用的稳定性和用户体验。

通过本集的学习,你已经具备了在 uni-app 中使用传感器功能的基本能力,可以开始开发需要传感器支持的应用了。

« 上一篇 uni-app NFC 功能 下一篇 » uni-app 后台运行