一、应用概述

1.1 应用简介

指南针(Compass)是一款模拟方向定位的实用工具,能够显示当前方向角度、方位文字、目标方位跟踪和方位保存功能。该应用以HarmonyOS NEXT的ArkTS框架为基础,深入展示了方向计算算法、角度映射、方位保存、旋转动画、传感器调用和视觉反馈等关键技术。

1.2 核心功能

功能模块 功能描述 技术实现 设计考量
方向显示 当前方向角度 角度计算+传感器 0-360°精确显示
方位文字 16方位文字 角度映射表 北/北东北/东北等
目标设定 设置目标方位 偏差计算 到达提醒
方位保存 保存方位记录 数组存储 带位置名称
校准功能 模拟校准 定时器动画 抖动动画效果
可视化 指针旋转动画 旋转动画+渐变 平滑过渡
磁场强度 显示磁场强度 传感器数据 检测干扰
水平仪 水平角度检测 加速度传感器 辅助功能

1.3 应用架构

指南针应用采用分层架构:

  1. UI表现层:指南针表盘、方向指针、角度/方位文字显示、目标方位设定、保存列表。
  2. 业务逻辑层:方向计算引擎、方位映射器、目标跟踪器、传感器管理器。
  3. 数据持久层:使用Preferences API保存方位记录和设置。

二、方向计算算法

2.1 方位映射

class CompassEngine {
  // 16方位映射
  private static readonly DIRECTIONS_16 = [
    { name: '北', abbreviation: 'N', range: [0, 11.25] },
    { name: '北东北', abbreviation: 'NNE', range: [11.25, 33.75] },
    { name: '东北', abbreviation: 'NE', range: [33.75, 56.25] },
    { name: '东东北', abbreviation: 'ENE', range: [56.25, 78.75] },
    { name: '东', abbreviation: 'E', range: [78.75, 101.25] },
    { name: '东东南', abbreviation: 'ESE', range: [101.25, 123.75] },
    { name: '东南', abbreviation: 'SE', range: [123.75, 146.25] },
    { name: '南东南', abbreviation: 'SSE', range: [146.25, 168.75] },
    { name: '南', abbreviation: 'S', range: [168.75, 191.25] },
    { name: '南西南', abbreviation: 'SSW', range: [191.25, 213.75] },
    { name: '西南', abbreviation: 'SW', range: [213.75, 236.25] },
    { name: '西西南', abbreviation: 'WSW', range: [236.25, 258.75] },
    { name: '西', abbreviation: 'W', range: [258.75, 281.25] },
    { name: '西西北', abbreviation: 'WNW', range: [281.25, 303.75] },
    { name: '西北', abbreviation: 'NW', range: [303.75, 326.25] },
    { name: '北西北', abbreviation: 'NNW', range: [326.25, 348.75] },
    { name: '北', abbreviation: 'N', range: [348.75, 360] }
  ];
  
  // 获取方向文字
  static getDirectionText(degrees: number): { name: string; abbreviation: string } {
    const normalized = ((degrees % 360) + 360) % 360;
    for (const dir of this.DIRECTIONS_16) {
      if (normalized >= dir.range[0] && normalized < dir.range[1]) {
        return { name: dir.name, abbreviation: dir.abbreviation };
      }
    }
    return { name: '北', abbreviation: 'N' };
  }
  
  // 简化版:使用公式计算(更高效)
  static getDirectionSimple(degrees: number): string {
    const directions = ['北', '北东北', '东北', '东东北', '东', '东东南', '东南', 
      '南东南', '南', '南西南', '西南', '西西南', '西', '西西北', '西北', '北西北'];
    const index = Math.round(((degrees % 360) + 360) % 360 / 22.5) % 16;
    return directions[index];
  }
  
  // 计算两个方向的偏差角度
  static getBearingDifference(current: number, target: number): number {
    let diff = target - current;
    if (diff > 180) diff -= 360;
    if (diff < -180) diff += 360;
    return diff;
  }
  
  // 判断是否面向目标方向
  static isFacingTarget(current: number, target: number, threshold: number = 5): boolean {
    return Math.abs(this.getBearingDifference(current, target)) <= threshold;
  }
  
  // 角度平滑过渡(防止指针抖动)
  static smoothAngle(current: number, target: number, smoothingFactor: number = 0.3): number {
    let diff = target - current;
    if (diff > 180) diff -= 360;
    if (diff < -180) diff += 360;
    return current + diff * smoothingFactor;
  }
  
  // 磁偏角校正(根据地理位置)
  static applyDeclination(degrees: number, declination: number): number {
    return ((degrees + declination) % 360 + 360) % 360;
  }
}

2.2 目标方位跟踪

class TargetTracker {
  private targetAngle: number | null = null;
  private targetName: string = '';
  private reached: boolean = false;
  private onReached: (() => void) | null = null;
  
  setTarget(angle: number, name: string): void {
    this.targetAngle = ((angle % 360) + 360) % 360;
    this.targetName = name;
    this.reached = false;
  }
  
  clearTarget(): void {
    this.targetAngle = null;
    this.targetName = '';
    this.reached = false;
  }
  
  update(currentAngle: number): TrackingInfo {
    if (this.targetAngle === null) {
      return { hasTarget: false, difference: 0, direction: '', isReached: false };
    }
    
    const diff = CompassEngine.getBearingDifference(currentAngle, this.targetAngle);
    const isReached = Math.abs(diff) <= 5;
    
    if (isReached && !this.reached) {
      this.reached = true;
      this.onReached?.();
    }
    
    let direction = '';
    if (diff > 0) direction = '← 向左转';
    else if (diff < 0) direction = '向右转 →';
    else direction = '✓ 正对目标';
    
    return {
      hasTarget: true,
      targetAngle: this.targetAngle,
      targetName: this.targetName,
      difference: diff,
      direction,
      isReached: isReached
    };
  }
  
  setOnReached(callback: () => void): void {
    this.onReached = callback;
  }
  
  getTarget(): { angle: number | null; name: string } {
    return { angle: this.targetAngle, name: this.targetName };
  }
}

interface TrackingInfo {
  hasTarget: boolean;
  targetAngle?: number;
  targetName?: string;
  difference: number;
  direction: string;
  isReached: boolean;
}

三、方位保存管理

3.1 方位记录

interface CompassRecord {
  id: string;
  name: string;
  angle: number;
  direction: string;
  latitude?: number;
  longitude?: number;
  createdAt: number;
  note: string;
}

class CompassRecordManager {
  private records: CompassRecord[] = [];
  private prefs: preferences.Preferences | null = null;
  
  async init(context: Context): Promise<void> {
    this.prefs = await preferences.getPreferences(context, 'compass_prefs');
    await this.load();
  }
  
  addRecord(angle: number, name: string, note: string = ''): CompassRecord {
    const record: CompassRecord = {
      id: Date.now().toString(),
      name,
      angle: ((angle % 360) + 360) % 360,
      direction: CompassEngine.getDirectionSimple(angle),
      createdAt: Date.now(),
      note
    };
    this.records.push(record);
    this.save();
    return record;
  }
  
  deleteRecord(id: string): void {
    this.records = this.records.filter(r => r.id !== id);
    this.save();
  }
  
  getRecords(): CompassRecord[] {
    return [...this.records].sort((a, b) => b.createdAt - a.createdAt);
  }
  
  async load(): Promise<void> {
    if (!this.prefs) return;
    const json = await this.prefs.get('compass_records', '[]');
    try { this.records = JSON.parse(json); } catch { this.records = []; }
  }
  
  async save(): Promise<void> {
    if (!this.prefs) return;
    await this.prefs.put('compass_records', JSON.stringify(this.records));
    await this.prefs.flush();
  }
}

四、UI交互设计

4.1 指南针表盘

@Component
struct CompassDial {
  @Link currentAngle: number;
  @Link targetAngle: number | null;
  @State displayAngle: number = 0;
  @State smoothAngle: number = 0;
  
  private readonly COMPASS_RING_COLORS = ['#E53935', '#FF9800', '#FFC107', '#4CAF50', '#2196F3', '#1565C0', '#9C27B0', '#E91E63'];
  
  aboutToAppear(): void {
    this.smoothAngle = this.currentAngle;
  }
  
  build() {
    Stack() {
      // 外圈刻度
      Circle()
        .width(280)
        .height(280)
        .fill('none')
        .stroke('#333333')
        .strokeWidth(2)
      
      // 刻度线
      ForEach(Array.from({ length: 72 }, (_, i) => i), (i: number) => {
        const angle = i * 5;
        const isMajor = i % 9 === 0;
        const length = isMajor ? 20 : 10;
        const width = isMajor ? 3 : 1;
        
        // 使用旋转定位
        Path()
          .width(isMajor ? 4 : 2)
          .height(length)
          .fill(isMajor ? '#333' : '#999')
          .rotate({ angle })
      })
      
      // 基本方位文字
      ForEach([
        { text: 'N', angle: 0, color: '#E53935' },
        { text: 'E', angle: 90, color: '#333333' },
        { text: 'S', angle: 180, color: '#333333' },
        { text: 'W', angle: 270, color: '#333333' }
      ], (dir: { text: string; angle: number; color: string }) => {
        Text(dir.text)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor(dir.color)
          .position({
            x: 140 - 8 + Math.sin(dir.angle * Math.PI / 180) * 120,
            y: 140 - 12 - Math.cos(dir.angle * Math.PI / 180) * 120
          })
      })
      
      // 指针
      Path()
        .width(4)
        .height(120)
        .fill('#E53935')
        .rotate({ angle: this.smoothAngle })
        .position({ x: 138, y: 20 })
      
      // 中心圆
      Circle()
        .width(20)
        .height(20)
        .fill('#333333')
      
      // 中心点
      Circle()
        .width(8)
        .height(8)
        .fill('#E53935')
    }
    .width(280)
    .height(280)
    .rotate({ angle: -this.smoothAngle })
    .animation({ duration: 200, curve: Curve.EaseOut })
    .padding(20)
  }
}

五、总结

5.1 核心技术要点

  1. 16方位映射算法:将360°角度精确映射到16个方位,支持简称和全称。
  2. 角度平滑过渡:使用平滑因子算法防止指针抖动,提升视觉体验。
  3. 目标方位跟踪:计算当前方向与目标方向的偏差,提供转向指引。
  4. 磁偏角校正:支持根据地理位置进行磁偏角校正。
  5. 方位记录管理:保存和管理已记录的方位数据。
  6. 旋转动画:使用ArkTS动画API实现指针平滑旋转。

5.2 扩展方向

  1. 真实传感器调用:接入设备磁力计和加速度计获取真实方向数据。
  2. 地图集成:与地图应用联动,显示目标方位在地图上的位置。
  3. 导航路线:记录行进路线,支持返航导航。
  4. 磁场检测:检测周围磁场强度,识别电磁干扰。
  5. 水平仪:集成水平角度检测功能。

5.3 核心代码量统计

模块 核心代码行数 接口数 组件数
方向计算引擎 120 6 -
目标跟踪器 70 5 -
方位记录管理器 80 5 -
UI组件 250 3 4
总计 520 19 4

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐