Flutter室内定位插件situm_flutter的使用

发布于 1周前 作者 yibo5220 来自 Flutter

Flutter室内定位插件situm_flutter的使用

描述

Situm Logo

Situm Wayfinding for Flutter. 通过集成楼层平面图、兴趣点(POIs)、路径和转弯方向,快速实现即插即用的导航体验。借助 Situm 的力量。

License: MIT Pub Version Flutter

快速开始

有关如何使用此插件设置新应用程序的详细教程,请参阅 Situm 文档页面

以下是安装和配置插件的基本步骤。这些步骤已经在本仓库中的示例应用程序中完成,但其他项目需要执行这些步骤。

安装插件

要将 Situm 依赖项添加到您的 Flutter 项目中,您可以使用 flutter pub add 命令。在终端中运行以下命令:

flutter pub add situm_flutter

设置您的 Situm 凭据

创建一个新的 config.dart 文件来保存您的 Situm 凭据。您可以参考 example/config.dart.example 文件作为示例。 如果您还没有设置 Situm 账户,请按照 Wayfinding 指南进行操作。

Android 配置

  1. 如果您已配置 Situm SDK 使用 GPS,则需在 AndroidManifest.xml 文件中添加 ACCESS_FINE_LOCATION 权限:
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    
  2. 在应用的 build.gradle 文件中将 minSdkVersion 设置为 21 或更高版本。

iOS 配置

  1. 移除 iOS 项目 Podfile 中的 “use_frameworks!” 指令。
  2. 运行 pod installpod update 将依赖项引入您的项目。
  3. 在应用的 Info.plist 文件中声明以下权限以成功启动定位:
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>Location is required to find out where you are</string>
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>Location is required to find out where you are</string>
    <key>NSMotionUsageDescription</key>
    <string>We use your phone sensors (giroscope, accelerometer and altimeter) to improve location quality</string>
    
  4. 对于离线支持,您需要在 Info.plist 文件中的 WKAppBoundDomains 入口中添加底层 Web 应用程序的域,如下所示:
    <key>WKAppBoundDomains</key>
    <array>
        <string>map-viewer.situm.com</string>
    </array>
    

示例代码

下面是一个完整的示例 demo,展示了如何使用 situm_flutter 插件:

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:situm_flutter/sdk.dart';
import 'package:situm_flutter/wayfinding.dart';

import './config.dart';

ValueNotifier<String> currentOutputNotifier = ValueNotifier<String>('---');

void main() => runApp(const MyApp());

const _title = "Situm Flutter";

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: MyTabs(),
    );
  }
}

class MyTabs extends StatefulWidget {
  const MyTabs({super.key});

  @override
  State<MyTabs> createState() => _MyTabsState();
}

class _MyTabsState extends State<MyTabs> {
  late SitumSdk situmSdk;
  late FlutterTts flutterTts;
  int _selectedIndex = 0;
  List<Poi> pois = [];
  List<Floor> floors = [];
  Poi? poiDropdownValue;
  Floor? floorDropdownValue;
  bool fitCameraToFloor = false;
  Function? mapViewLoadAction;

  MapViewController? mapViewController;

  // Home Tab UI
  Widget _createHomeTab() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        _buttonsGroup(Icons.my_location, "Positioning", [
          _sdkButton('Start', _requestLocationUpdates),
          _sdkButton('Stop', _removeUpdates),
        ]),
        _buttonsGroup(Icons.cloud_download, "Fetch resources", [
          _sdkButton('Prefetch', _prefetch),
          _sdkButton('Clear cache', _clearCache),
          _sdkButton('Pois', _fetchPois),
          _sdkButton('Categories', _fetchCategories),
          _sdkButton('Buildings', _fetchBuildings),
          _sdkButton('Building Info', _fetchBuildingInfo),
        ]),
        _poiInteraction(),
        _setCamera(),
        _setFloor(),
        Expanded(
            child: ValueListenableBuilder<String>(
          valueListenable: currentOutputNotifier,
          builder: (context, value, child) {
            return SingleChildScrollView(
              padding: const EdgeInsets.all(30),
              child: Text(value),
            );
          },
        ))
      ],
    );
  }

  Card _buttonsGroup(IconData iconData, String title, List<Widget> children) {
    return Card(
      child: Column(children: [
        ExpansionTile(
          shape: const Border(),
          title: _cardTitle(iconData, title),
          children: <Widget>[
            GridView.count(
              physics: const NeverScrollableScrollPhysics(),
              crossAxisCount: 3,
              padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
              shrinkWrap: true,
              childAspectRatio: 2.5,
              children: children,
            ),
          ],
        )
      ]),
    );
  }

  Widget _sdkButton(String buttonText, void Function() onPressed) {
    return TextButton(
        onPressed: () {
          onPressed();
        },
        child: Text(buttonText));
  }

  Widget _sdkCheckbox(
      String labelText, bool value, void Function(bool?) onChanged) {
    return Row(
      children: [
        Text(labelText),
        Checkbox(
          value: value,
          onChanged: onChanged,
        ),
      ],
    );
  }

  Padding _cardTitle(IconData iconData, String title) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
      child: Row(
        children: [
          Icon(iconData, color: Colors.black45),
          const SizedBox(width: 16.0),
          Text(
            title,
            style: const TextStyle(
              fontSize: 18.0,
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    );
  }

  Card _setCamera() {
    return Card(
      child: ExpansionTile(
        shape: const Border(),
        title: _cardTitle(Icons.video_camera_front_rounded, "Set Camera"),
        children: [
          Row(
            children: <Widget>[
              Flexible(
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: DropdownButton<Poi>(
                    isExpanded: true,
                    value: poiDropdownValue,
                    elevation: 16,
                    onChanged: (Poi? value) {
                      setState(() {
                        poiDropdownValue = value!;
                      });
                    },
                    items: pois.map((value) {
                      return DropdownMenuItem<Poi>(
                        value: value,
                        child: Text(value.name),
                      );
                    }).toList(),
                  ),
                ),
              ),
              _sdkButton("Set", (() => _setCameraViewer(poiDropdownValue))),
            ],
          ),
        ],
      ),
    );
  }

  Card _setFloor() {
    return Card(
      child: ExpansionTile(
        shape: const Border(),
        title: _cardTitle(Icons.video_camera_front_rounded, "Set floor"),
        children: [
          Row(
            children: <Widget>[
              Flexible(
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: DropdownButton<Floor>(
                    isExpanded: true,
                    value: floorDropdownValue,
                    elevation: 16,
                    onChanged: (Floor? value) {
                      setState(() {
                        floorDropdownValue = value!;
                      });
                    },
                    items: floors.map((value) {
                      return DropdownMenuItem<Floor>(
                        value: value,
                        child: Text(value.name),
                      );
                    }).toList(),
                  ),
                ),
              ),
              _sdkButton("Set", (() => _selectFloor(floorDropdownValue))),
              _sdkCheckbox(
                "Fit",
                fitCameraToFloor,
                (bool? newValue) {
                  setState(() {
                    fitCameraToFloor = newValue ?? !fitCameraToFloor;
                  });
                },
              )
            ],
          ),
        ],
      ),
    );
  }

  Card _poiInteraction() {
    return Card(
      child: ExpansionTile(
        shape: const Border(),
        title: _cardTitle(Icons.interests, "POI Interaction"),
        children: <Widget>[
          Row(
            children: <Widget>[
              Flexible(
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: DropdownButton<Poi>(
                    isExpanded: true,
                    value: poiDropdownValue,
                    elevation: 16,
                    onChanged: (Poi? value) {
                      setState(() {
                        poiDropdownValue = value!;
                      });
                    },
                    items: pois.map((value) {
                      return DropdownMenuItem<Poi>(
                        value: value,
                        child: Text(value.name),
                      );
                    }).toList(),
                  ),
                ),
              ),
              _sdkButton("Select", (() => _selectPoi(poiDropdownValue))),
              _sdkButton("Navigate", (() => _navigateToPoi(poiDropdownValue))),
            ],
          )
        ],
      ),
    );
  }

  // 显示 Situm MapView
  Widget _createSitumMapTab() {
    return Stack(children: [
      MapView(
        key: const Key("situm_map"),
        configuration: MapViewConfiguration(
          situmApiKey: situmApiKey,
          buildingIdentifier: buildingIdentifier,
          remoteIdentifier: remoteIdentifier,
          viewerDomain: viewerDomain,
        ),
        onLoad: _onLoad,
        onError: _onError,
      ),
    ]);
  }

  void printWarning(String text) {
    debugPrint('\x1B[33m$text\x1B[0m');
  }

  void printError(String text) {
    debugPrint('\x1B[31m$text\x1B[0m');
  }

  void _onLoad(MapViewController controller) {
    mapViewController = controller;

    // 自动启动定位示例
    // situmSdk.requestLocationUpdates(LocationRequest(
    //   buildingIdentifier: buildingIdentifier,
    //   useDeadReckoning: false,
    // ));

    _callMapviewLoadAction();

    // 自动居中用户位置示例
    //controller.followUser();

    controller.onPoiSelected((poiSelectedResult) {
      printWarning("WYF> Poi SELECTED: ${poiSelectedResult.poi.name}");
    });
    controller.onPoiDeselected((poiDeselectedResult) {
      printWarning("WYF> Poi DESELECTED: ${poiDeselectedResult.poi.name}");
    });
    controller.onNavigationRequestInterceptor((navigationRequest) {
      printWarning("WYF> Navigation interceptor: ${navigationRequest.toMap()}");
    });

    // 处理 TTS
    controller.onSpeakAloudText((speakaloudTextResult) async {
      _echo("Situm > SDK > Speak aloud: ${speakaloudTextResult.text}");
      if (speakaloudTextResult.lang != null) {
        flutterTts.setLanguage(speakaloudTextResult.lang!);
      }
      if (speakaloudTextResult.rate != null) {
        flutterTts.setSpeechRate(speakaloudTextResult.rate!);
      }
      if (speakaloudTextResult.volume != null) {
        flutterTts.setVolume(speakaloudTextResult.volume!);
      }
      if (speakaloudTextResult.pitch != null) {
        flutterTts.setPitch(speakaloudTextResult.pitch!);
      }

      await flutterTts.speak(speakaloudTextResult.text);
    });
  }

  void _onError(MapViewError error) {
    _echo("Situm> MapView> Error ${error.code}:\n${error.message}");
  }

  void _setCameraViewer(Poi? poi) {
    Camera c = Camera();
    c.center = poi?.position.coordinate;
    mapViewController?.setCamera(c);
    setState(() {
      _selectedIndex = 1;
    });
  }

  void _selectFloor(Floor? floor) {
    int floorId = int.tryParse(floor?.identifier ?? "") ?? 0;
    if (floorId != 0) {
      SelectCartographyOptions options = SelectCartographyOptions();
      options.fitCamera = fitCameraToFloor;

      mapViewController?.selectFloor(floorId, options: options);

      setState(() {
        _selectedIndex = 1;
      });
    }
  }

  void _selectPoi(Poi? poi) {
    if (poi == null) {
      return;
    }
    setState(() {
      _selectedIndex = 1;
    });
    mapViewLoadAction = () {
      mapViewController?.selectPoi(poi.identifier);
    };
    if (mapViewController != null) {
      _callMapviewLoadAction();
    }
  }

  void _callMapviewLoadAction() {
    mapViewLoadAction?.call();
    mapViewLoadAction = null;
  }

  void _navigateToPoi(Poi? poi) {
    if (poi == null) {
      return;
    }
    setState(() {
      _selectedIndex = 1;
    });
    mapViewLoadAction = () {
      mapViewController?.navigateToPoi(poi.identifier);
    };
    if (mapViewController != null) {
      _callMapviewLoadAction();
    }
  }

  void _downloadPois(String buildingIdentifier) async {
    var poiList = await situmSdk.fetchPoisFromBuilding(buildingIdentifier);
    setState(() {
      pois = poiList;
      poiDropdownValue = pois[0];
    });
  }

  void _downloadFloors(String buildingIdentifier) async {
    var info = await situmSdk.fetchBuildingInfo(buildingIdentifier);
    setState(() {
      floors = info.floors;
      floorDropdownValue = floors[0];
    });
  }

  @override
  void initState() {
    situmSdk = SitumSdk();
    situmSdk.init();
    situmSdk.setApiKey(situmApiKey);
    situmSdk.setConfiguration(ConfigurationOptions(useRemoteConfig: true));

    situmSdk.onLocationUpdate((location) {
      _echo("""SDK> Location changed:
        Time diff: ${location.timestamp - DateTime.now().millisecondsSinceEpoch}
        B=${location.buildingIdentifier},
        F=${location.floorIdentifier},
        C=${location.coordinate.latitude.toStringAsFixed(5)}, ${location.coordinate.longitude.toStringAsFixed(5)}
      """);
    });
    situmSdk.onLocationStatus((status) {
      _echo("Situm> SDK> STATUS: $status");
    });
    situmSdk.onLocationError((Error error) {
      _echo("Situm> SDK> Error ${error.code}:\n${error.message}");
    });
    situmSdk.onEnterGeofences((geofencesResult) {
      _echo("Situm> SDK> Enter geofences: ${geofencesResult.geofences}.");
    });
    situmSdk.onExitGeofences((geofencesResult) {
      _echo("Situm> SDK> Exit geofences: ${geofencesResult.geofences}.");
    });
    _downloadPois(buildingIdentifier);
    _downloadFloors(buildingIdentifier);

    flutterTts = FlutterTts();

    super.initState();
  }

  void _echo(String output) {
    currentOutputNotifier.value = output;
    printWarning(output);
  }

  // SDK 辅助函数

  void _requestLocationUpdates() async {
    var hasPermissions = await _requestPermissions();
    if (!hasPermissions) {
      _echo("You need to accept permissions to start positioning.");
    }
    situmSdk.requestLocationUpdates(LocationRequest(
      buildingIdentifier: buildingIdentifier,
      useDeadReckoning: false,
    ));
  }

  void _removeUpdates() async {
    situmSdk.removeUpdates();
  }

  void _clearCache() async {
    _echo("SDK> RESPONSE: CLEAR CACHE...");
    await situmSdk.clearCache();
    _echo("SDK> RESPONSE: CLEAR CACHE = DONE");
  }

  void _prefetch() async {
    _echo("SDK> PREFETCH...");
    var prefetch = await situmSdk.prefetchPositioningInfo(
      [buildingIdentifier],
      options: PrefetchOptions(preloadImages: true),
    );
    _echo("SDK> RESPONSE: PREFETCH = $prefetch");
  }

  void _fetchPois() async {
    _echo("SDK> POIS...");
    var pois = await situmSdk.fetchPoisFromBuilding(buildingIdentifier);
    _echo("SDK> RESPONSE: POIS = \n\n$pois");
  }

  void _fetchCategories() async {
    _echo("SDK> CATEGORIES...");
    var categories = await situmSdk.fetchPoiCategories();
    _echo("SDK> RESPONSE: CATEGORIES = \n\n$categories");
  }

  void _fetchBuildingInfo() async {
    _echo("SDK> BUILDING INFO...");
    var building = await situmSdk.fetchBuildingInfo(buildingIdentifier);
    _echo("SDK> RESPONSE: BUILDING INFO = \n\n$building)");
  }

  void _fetchBuildings() async {
    _echo("SDK> BUILDINGS...");
    var buildings = await situmSdk.fetchBuildings();
    _echo("SDK> RESPONSE: BUILDINGS = \n\n$buildings");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(_title),
      ),
      body: IndexedStack(
        index: _selectedIndex,
        children: [_createHomeTab(), _createSitumMapTab()],
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.map),
            label: 'Wayfinding',
          ),
        ],
        currentIndex: _selectedIndex,
        selectedItemColor: Colors.amber[800],
        onTap: _onItemTapped,
      ),
    );
  }

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  Future<bool> _requestPermissions() async {
    var permissions = <Permission>[
      Permission.locationWhenInUse,
    ];
    if (Platform.isAndroid) {
      permissions.addAll([
        Permission.bluetoothConnect,
        Permission.bluetoothScan,
      ]);
    }
    Map<Permission, PermissionStatus> statuses = await permissions.request();
    return statuses.values.every((status) => status.isGranted);
  }
}

关键功能说明

  • 初始化和配置:通过 SitumSdk 初始化并配置 API 密钥和其他选项。
  • 请求权限:确保应用具有必要的定位权限。
  • 地图加载:加载 MapView 并处理地图加载事件。
  • POI 和楼层选择:提供选择 POI 和楼层的功能,并相应地调整地图视图。
  • 定位更新:启动和停止定位更新,并处理位置变化事件。
  • 缓存管理:预取和清除缓存数据。
  • TTS 支持:通过 FlutterTts 提供文本转语音支持。

通过以上步骤和代码示例,您可以快速上手并集成 Situm 的室内定位功能到您的 Flutter 应用中。更多详细信息请参考 Situm 文档。如果有任何问题或需要帮助,请联系 support@situm.com


更多关于Flutter室内定位插件situm_flutter的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter室内定位插件situm_flutter的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter项目中集成和使用situm_flutter插件来进行室内定位的示例代码。situm_flutter插件允许你访问Situm室内定位服务,获取位置信息。

前提条件

  1. Flutter环境:确保你已经安装并配置好了Flutter开发环境。
  2. Situm账户:你需要在Situm平台上注册并获取必要的API密钥和场所ID。

步骤一:添加依赖

首先,在你的pubspec.yaml文件中添加situm_flutter依赖:

dependencies:
  flutter:
    sdk: flutter
  situm_flutter: ^最新版本号  # 替换为最新版本号

然后运行flutter pub get来安装依赖。

步骤二:配置Situm服务

在你的Flutter应用的入口文件(通常是main.dart)中,配置Situm服务。你需要提供你的API密钥和场所ID。

import 'package:flutter/material.dart';
import 'package:situm_flutter/situm_flutter.dart';

void main() {
  // 初始化Situm SDK
  SitumFlutter.init(
    apiKey: '你的API密钥',
    venueId: '你的场所ID',
  );

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Situm Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

步骤三:获取位置信息

在你的主页面(MyHomePage)中,使用Situm SDK来获取位置信息。

import 'package:flutter/material.dart';
import 'package:situm_flutter/situm_flutter.dart';

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _locationStatus = '获取位置中...';
  SitumPosition? _currentPosition;

  @override
  void initState() {
    super.initState();
    
    // 开始位置监听
    SitumFlutter.startLocationUpdates().listen((SitumPosition position) {
      setState(() {
        _currentPosition = position;
        _locationStatus = '位置更新: ${position.name ?? '未知位置'}';
      });
    }, onError: (error) {
      setState(() {
        _locationStatus = '获取位置失败: $error';
      });
    });
  }

  @override
  void dispose() {
    // 停止位置监听
    SitumFlutter.stopLocationUpdates();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Situm Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              _locationStatus,
              style: TextStyle(fontSize: 20),
            ),
            if (_currentPosition != null)
              Text(
                '坐标: Latitude: ${_currentPosition!.latitude}, Longitude: ${_currentPosition!.longitude}',
                style: TextStyle(fontSize: 16),
              ),
          ],
        ),
      ),
    );
  }
}

说明

  1. 初始化SDK:在main函数中调用SitumFlutter.init方法,传入你的API密钥和场所ID。
  2. 位置监听:在MyHomePage的状态管理中,使用SitumFlutter.startLocationUpdates()方法来开始监听位置更新。每次位置更新时,会调用监听器的回调,你可以在这里更新UI。
  3. 停止位置监听:在dispose方法中调用SitumFlutter.stopLocationUpdates()方法来停止位置监听,以避免内存泄漏。

注意事项

  • 确保你已经正确配置了Situm平台的API密钥和场所ID。
  • 实际应用中,你可能需要处理更多的错误情况和边界情况,比如网络错误、定位失败等。
  • 根据你的应用需求,你可能还需要处理位置权限请求,确保应用有权限访问设备的位置信息。

这个示例代码提供了一个基本的框架,你可以根据需要进行扩展和修改。

回到顶部