Flutter室内定位插件situm_flutter的使用
Flutter室内定位插件situm_flutter的使用
描述
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 配置
- 如果您已配置 Situm SDK 使用 GPS,则需在
AndroidManifest.xml
文件中添加ACCESS_FINE_LOCATION
权限:<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
- 在应用的
build.gradle
文件中将minSdkVersion
设置为 21 或更高版本。
iOS 配置
- 移除 iOS 项目
Podfile
中的 “use_frameworks!” 指令。 - 运行
pod install
或pod update
将依赖项引入您的项目。 - 在应用的
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>
- 对于离线支持,您需要在
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
更多关于Flutter室内定位插件situm_flutter的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是一个关于如何在Flutter项目中集成和使用situm_flutter
插件来进行室内定位的示例代码。situm_flutter
插件允许你访问Situm室内定位服务,获取位置信息。
前提条件
- Flutter环境:确保你已经安装并配置好了Flutter开发环境。
- 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),
),
],
),
),
);
}
}
说明
- 初始化SDK:在
main
函数中调用SitumFlutter.init
方法,传入你的API密钥和场所ID。 - 位置监听:在
MyHomePage
的状态管理中,使用SitumFlutter.startLocationUpdates()
方法来开始监听位置更新。每次位置更新时,会调用监听器的回调,你可以在这里更新UI。 - 停止位置监听:在
dispose
方法中调用SitumFlutter.stopLocationUpdates()
方法来停止位置监听,以避免内存泄漏。
注意事项
- 确保你已经正确配置了Situm平台的API密钥和场所ID。
- 实际应用中,你可能需要处理更多的错误情况和边界情况,比如网络错误、定位失败等。
- 根据你的应用需求,你可能还需要处理位置权限请求,确保应用有权限访问设备的位置信息。
这个示例代码提供了一个基本的框架,你可以根据需要进行扩展和修改。