查找设备页面(amap_map)

效果图

代码

Dart 复制代码
import 'dart:io';

import 'package:amap_map/amap_map.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:map_launcher/map_launcher.dart' as map_launcher;
import 'package:permission_handler/permission_handler.dart';
import 'package:x_amap_base/x_amap_base.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:mate/bluetooth/headset.dart';
import 'package:mate/caches/user_data.dart';
import 'package:mate/manager/device_manager.dart';
import 'dart:math';
import '../config.dart';
import '../generated/l10n.dart';

class AMapPage extends StatefulWidget {
  const AMapPage({super.key,required this.headset});

  final Headset headset;
  @override
  State<StatefulWidget> createState() => _AMapPage();
}

class _AMapPage extends State<AMapPage> {

  AMapController? mapController; // 地图控制器
  LatLng? latLng; // 当前坐标

  final earphoneMarkers = <Marker>[];

  bool _isMapReady = false; // 地图是否准备就绪
  bool _isLoading = true; // 是否正在加载
  bool _showMyLocation = false;// 控制是否显示定位蓝点
  bool isFinding = false;//是否正在查找

  String address = ""; //存储定位地址
  String time = UserData.instance.getUserTime();//获取时间戳

  // 获取保存的耳机的经纬度
  double earphoneLatitude = UserData.instance.getEarphoneLatitude();
  double earphoneLongitude = UserData.instance.getEarphoneLongitude();


  final player = AudioPlayer(); //音频播放器

  //存储当前定位
  double myLatitude = 0.0;
  double myLongitude = 0.0;
  //存储耳机和我之间的距离
  int meter = 0;


  @override
  void initState() {
    super.initState();

    AMapInitializer.updatePrivacyAgree(AMapPrivacyStatement(hasContains: true,hasShow: true,hasAgree: true)); //初始化隐私协议

    _initializeLocation();

  }

  @override
  void dispose() {
    super.dispose();

    player.stop();
    player.dispose();
  }


  //==================================初始化位置==================================
  Future<void> _initializeLocation() async {

    setState(() => _isLoading = true);//显示加载中

    //获取我的位置
    getMyLocation();

    //读取耳机位置
    if (earphoneLatitude != 0 && earphoneLongitude != 0) {
      latLng = LatLng(earphoneLatitude, earphoneLongitude);
      print('【使用缓存位置】从UserData读取耳机的位置: (${latLng!.latitude}, ${latLng!.longitude})');

      //获取耳机的位置字符串
      _getAddressFromLatLng(earphoneLatitude,earphoneLongitude);
      print("获取耳机的区域位置");
    } else {
      print('【缓存位置】无缓存位置数据 (latitude=$earphoneLatitude, longitude=$earphoneLongitude)');
    }

    setState(() => _isLoading = false); //隐藏加载中
  }

  //=========================获取我的位置=========================
  Future<void> getMyLocation() async {

    // 检查定位服务
    bool service = await Geolocator.isLocationServiceEnabled();
    if (!service) {
      print("定位服务未开启");
      return;
    }

    // 检查权限
    final permission = await Permission.locationWhenInUse.status;
    if (permission != PermissionStatus.granted) {
      final status = await Permission.locationWhenInUse.request();
      if (status != PermissionStatus.granted) {
        print("没有定位权限");
        return;
      }
    }

    try {
      // 获取当前位置
      Position position = await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high,
      );

      print("获取到我的位置为: ${position.latitude}, ${position.longitude}");

      setState(() {
        myLatitude = position.latitude;
        myLongitude = position.longitude;
      });

      //获取我和耳机之间的距离
      double distance = Geolocator.distanceBetween(myLatitude, myLongitude, earphoneLatitude, earphoneLongitude);

      setState(() {
        meter = distance.round(); // 四舍五入取整
      });

      // 显示我的位置蓝点
      if (!_showMyLocation) {
        setState(() => _showMyLocation = true);
      }

      print(" 我和耳机的距离: ${meter}米");
      print("   我的位置: ($myLatitude, $myLongitude)");
      print("   耳机位置: (${earphoneLatitude}, ${earphoneLongitude})");
    } catch (error) {
      print("上传位置失败: $error");
    }
  }

  //==================================更新距离==================================
  void _updateDistance() {

    if (myLatitude != 0 && myLongitude != 0 && myLatitude != 0 && myLongitude != 0) {
      //获取我和耳机之间的距离
      double distance = Geolocator.distanceBetween(myLatitude, myLongitude, earphoneLatitude, earphoneLongitude);

      setState(() {
        meter = distance.round();
      });

      print("📏 距离更新: ${meter}米");
    }
  }

  //==================================创建耳机标记================================
  Future<void> _createEarphoneMarker() async {
    print('🔴【创建耳机标记】开始');

    if (earphoneLatitude == 0 || earphoneLongitude == 0) {
      print('  ❌ 耳机位置无效');
      return;
    }

    final icon = await BitmapDescriptor.fromAssetImage(const ImageConfiguration(devicePixelRatio: 1), "assets/devices/device_icon_small.png");
    final marker = Marker(position: LatLng(earphoneLatitude, earphoneLongitude), icon: icon);
    earphoneMarkers.add(marker);

    print('  ✅ 耳机标记创建成功,当前标记数量: ${earphoneMarkers.length}');
  }

  //============================== 将经纬度转换为地址==============================
  Future<void> _getAddressFromLatLng(double latitude, double longitude) async {
    try {
      List<Placemark> placemarks = await placemarkFromCoordinates(latitude, longitude);

      if (placemarks.isNotEmpty) {
        Placemark place = placemarks[0];

        // 构建完整地址字符串
        setState(() {
          address = [place.country! + place.street!,].where((s) => s != null && s.isNotEmpty).join(', ');
        });

        print("当前位置: $address");

      }
    } catch (e) {
      print("获取地址失败: $e");
    }
  }

  //=============================移动地图到指定位置===============================
  void _moveToPosition(LatLng position, {double zoom = 15}) {

    if (!_isMapReady || mapController == null) {
      print('  ⚠️【移动取消】地图未就绪');
      return;
    }

    print('🎯 移动地图到: (${position.latitude}, ${position.longitude})');

    mapController!.moveCamera(
      CameraUpdate.newCameraPosition(
        CameraPosition(target: position, zoom: zoom),
      ),
    );
    print('  ✅【移动成功】相机已移动');
  }


  //=========================地图创建回调优化==================================
  void onMapCreate(AMapController controller) {

    mapController = controller;
    _isMapReady = true;
    print(' 地图控制器已保存,_isMapReady=true');

    // 延迟初始化设备,确保地图已就绪
    print('  延迟100ms后初始化设备');
    Future.delayed(const Duration(milliseconds: 100), () {
      if (_isMapReady && mapController != null && earphoneLatitude != 0 && earphoneLongitude != 0) {
        print(' 移动到耳机位置: ($earphoneLatitude, $earphoneLongitude)');
        _moveToPosition(LatLng(earphoneLatitude, earphoneLongitude));
      }
      initDevice();
    });
  }


  //==================================设备初始化==================================
  void initDevice() async {

    //获取设备位置
    latLng = LatLng(earphoneLatitude, earphoneLongitude);

    // 如果没有设备位置,尝试显示当前位置
    if (latLng == null) {
      print('没有设备位置,请求当前位置');
      requestLocation();
    } else if (_isMapReady) {
      print('地图已就绪,移动到设备位置');
      _moveToPosition(latLng!);
    }

    // 添加所有设备标记
    print('开始添加设备标记');
    await _createEarphoneMarker();

    if (mounted) setState(() {});

  }

//==================================位置变化回调==================================
  void onLocationChanged(AMapLocation location) async {

    // 从 location 对象中获取经纬度
    if (location.latLng == null) {
      print('❌ 位置数据为空');
      return;
    }

    //获取我的位置
    double lat = location.latLng!.latitude;
    double lng = location.latLng!.longitude;

    print('📍 当前位置: ($lat, $lng)');

    // 更新我的位置
    setState(() {
      myLatitude = lat;
      myLongitude = lng;
    });

    // 更新距离
    _updateDistance();

    //当前我的位置
    final currentPos = LatLng(myLatitude, myLongitude);
    print('当前位置: (${currentPos.latitude}, ${currentPos.longitude})');

    // 如果是首次获取位置,移动到当前位置
    if (latLng == null && _isMapReady) {
      print(' 首次获取位置且地图已就绪,更新相机位置');
      setState(() {
        latLng = currentPos;
        print('latLng 更新为: (${latLng!.latitude}, ${latLng!.longitude})');
      });

      // 延迟一下确保地图已准备好
      print(' 延迟200ms后移动相机');
      Future.delayed(const Duration(milliseconds: 200), () {
        if (_isMapReady && mapController != null) {
          print('执行相机移动');
          _moveToPosition(currentPos);  //锁定我的位置,实时更新
        } else {
          print(' 地图未就绪,无法移动: _isMapReady=$_isMapReady, mapController=${mapController != null}');
        }
      });
    } else {
      print('非首次获取位置或地图未就绪: latLng!=null? ${latLng != null}, _isMapReady=$_isMapReady');
    }
  }


  //==================================播放声音==================================
  void playSound() {
    print('\n🔊🔊🔊【播放声音】用户点击播放声音按钮');

    final command = DeviceManager.instance.current()?.command;
    if (command == null) {
      print('  ❌ 设备未连接,无法播放声音');
      Fluttertoast.showToast(msg: S.current.disconnected);
      return;
    }

    print('  ✅ 设备已连接,发送查找指令');
    command.findHeadset(true);
    _showFindDeviceDialog(command);
  }

  void _showFindDeviceDialog(dynamic command) {
    print('  💬 显示查找设备弹窗');
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (builder) {
        return PopScope(
          canPop: false,
          child: AlertDialog(
            title: Text(S.current.findDevice),
            content: Text(S.current.findDeviceContext),
            actions: [
              TextButton(
                onPressed: () {
                  print('  🛑 用户点击停止查找');
                  command.findHeadset(false);
                  Navigator.pop(context);
                  print('  💬 弹窗关闭');
                },
                child: Text(S.current.stopFind),
              )
            ],
          ),
        );
      },
    );
  }

  //==================================请求当前位置==================================
  void requestLocation() {
    setState(() {
      _showMyLocation = true;
    });
  }


  //==============================停止查找==============================
  void onStop() async{
    await player.stop();
    setState(() {
      isFinding = false;
    });
  }


  //==============================开始查找==============================
  void onStart() async  {
    //开始查找
    await player.setReleaseMode(ReleaseMode.loop);//一直播放
    await player.play(AssetSource('mp3/findphone_alert_long.mp3'));

    setState(() {
      isFinding = true;
    });
  }


  @override
  Widget build(BuildContext context) {

    print('【构建地图】标记数量: ${earphoneMarkers.length}');

    final data = MediaQuery.of(context);
    final height = data.size.height;


    return Scaffold(
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : Stack(
        children: [

          //==========地图==========
          Container(
            height: height,
            width: double.infinity,
            padding: const EdgeInsets.all(5),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(20),
            ),
            child: ClipRRect(
              borderRadius: BorderRadius.circular(15),
              child: AMapWidget(
                onMapCreated: onMapCreate,
                onLocationChanged: onLocationChanged, // 启用位置变化回调
                myLocationStyleOptions: MyLocationStyleOptions(
                  _showMyLocation, //是否显示蓝点
                  circleFillColor: Colors.transparent,
                  circleStrokeColor: Colors.blue,
                  circleStrokeWidth: 0,
                ),
                markers: earphoneMarkers.toSet(), //地图标记
                initialCameraPosition: latLng == null
                    ? const CameraPosition(
                  target: LatLng(39.909187, 116.397451), //北京
                  zoom: 10,
                )
                    : CameraPosition(target: latLng!, zoom: 15),
                compassEnabled: true, //显示指南针
                scaleEnabled: true, //显示比例尺
              ),
            ),
          ),

          // 播放声音卡片
          _buildSoundCard(),

          // 导航卡片
          _buildNavigationCard(),

          // 位置卡片
          _buildLocationCard(),


        ],
      ),
    );
  }

  //导航弹窗
  void showBottomSheetDialog(BuildContext context){
    showModalBottomSheet(
        context: context, 
        isScrollControlled: true,
        backgroundColor: Colors.black,
        shape: const RoundedRectangleBorder(
          borderRadius: BorderRadius.vertical(top: Radius.circular(16))
        ),
        builder: (BuildContext context){
          return Container(
            padding: EdgeInsets.symmetric(
                vertical: 20,
                //horizontal: 10
            ),
            height: 240,
            child: Column(
              children: [
                GestureDetector(
                  onTap: () async {
                    goGaodeMap();
                  },
                  child: Container(
                    width: 325,
                    height: 52,
                    decoration: BoxDecoration(
                        color: Color(0xFFA8A8A8),
                        borderRadius: BorderRadius.circular(10)
                    ),
                    child: Center(
                      child: Text("高德地图",style: TextStyle(color:Colors.white,fontSize: 18),),
                    ),
                  ),
                ),

                SizedBox(height: 5,),

                GestureDetector(
                  onTap: (){
                    //跳转百度地图
                    goBaiduMap();

                  },
                  child: Container(
                    width: 325,
                    height: 52,
                    decoration: BoxDecoration(
                        color:  Color(0xFFA8A8A8),
                        borderRadius: BorderRadius.circular(10)
                    ),
                    child: Center(
                      child: Text("百度地图",style: TextStyle(color:Colors.white,fontSize: 18),),
                    ),
                  ),
                ),
                SizedBox(height: 25,),

                Container(
                  height: 1,
                  width: double.infinity,
                  color: Color(0xFFA8A8A8),
                ),

                SizedBox(height: 25,),

                GestureDetector(
                  onTap: (){
                    Navigator.pop(context);
                  },
                  child: Container(
                    child: Center(
                      child: Text(S.current.cancel,style: TextStyle(color:Colors.white,fontSize: 18),),
                    ),
                  ),
                )
              ],
            ),
          );
        }
    );
  }

  ///跳转高德地图
  Future<void> goGaodeMap() async{
    try {
      // 检查高德地图是否安装
      bool isAmapAvailable = await map_launcher.MapLauncher.isMapAvailable(map_launcher.MapType.amap);

      if (isAmapAvailable) {
        print('高德地图已安装,准备跳转');

        //耳机的位置
        LatLng? headsetLocation = LatLng(earphoneLatitude, earphoneLongitude);

        print("我的位置为:" + myLatitude.toString() + ',' + myLongitude.toString() + ",耳机的位置为" + earphoneLatitude.toString() + ',' + earphoneLongitude.toString());

        if (headsetLocation != null) {
          await map_launcher.MapLauncher.showMarker(
            mapType: map_launcher.MapType.amap,
            coords: map_launcher.Coords(
                headsetLocation.latitude,
                headsetLocation.longitude
            ),
            title: widget.headset.classicName,  // 使用设备名称
            description: "耳机位置",  // 可选描述
          );
          print('跳转成功');
        } else {
          print('无法获取耳机位置');
          Fluttertoast.showToast(msg: "无法获取耳机位置");
        }
      } else {
        print('高德地图未安装');
        Fluttertoast.showToast(msg: "请先安装高德地图");

      }
    } catch (e) {
      print('跳转失败: $e');
      Fluttertoast.showToast(msg: "跳转失败");
    }

    Navigator.pop(context);
  }

  ///跳转百度地图
  Future<void> goBaiduMap() async{
    try {
      // 检查百度地图是否安装
      bool isBaiduAvailable = await map_launcher.MapLauncher.isMapAvailable(map_launcher.MapType.baidu);

      if (isBaiduAvailable) {
        print('高德地图已安装,准备跳转');

        //耳机的位置
        LatLng? headsetLocation = LatLng(earphoneLatitude,earphoneLongitude);

        print("我的位置为:" + myLatitude.toString() + ',' + myLongitude.toString() + ",耳机的位置为" + earphoneLatitude.toString() + ',' + earphoneLongitude.toString());

        if (headsetLocation != null) {
          await map_launcher.MapLauncher.showMarker(
            mapType: map_launcher.MapType.baidu,
            coords: map_launcher.Coords(
                headsetLocation.latitude,
                headsetLocation.longitude
            ),
            title: widget.headset.classicName,  // 使用设备名称
            description: "耳机位置",  // 可选描述
          );
          print('跳转成功');
        } else {
          print('无法获取耳机位置');
          Fluttertoast.showToast(msg: "无法获取耳机位置");
        }
      } else {
        print('高德地图未安装');
        Fluttertoast.showToast(msg: "请先安装百度地图");

      }
    } catch (e) {
      print('跳转失败: $e');
      Fluttertoast.showToast(msg: "跳转失败");
    }

    Navigator.pop(context);
  }


  //====================================播放声音====================================
 Widget _buildSoundCard() {
    return
      Positioned(

          bottom: 170,
          left: 20,
          child: Container(
            padding: EdgeInsets.symmetric(
                horizontal: 20,
                vertical: 10
            ),
            height: 85,
            width: 158,
            decoration: BoxDecoration(
              color: Color(0xFF333333).withOpacity(0.4),
              borderRadius: BorderRadius.circular(10),
            ),
            child:Column(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                //图标
                Container(
                  height: 36,
                  width: 36,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: Color(0xFFF7F7F7),
                  ),
                  child: Center(
                    child: Icon(Icons.play_arrow_outlined,color: Color(0xFFAACD06),size: 29,),
                  ),
                ),

                Row(

                  children: [

                    SizedBox(width: 5,),
                    Text(S.current.playAudio,style: TextStyle(color: Colors.white,fontSize: 13,fontWeight: FontWeight.bold),),

                    Spacer(),

                    //按键
                    GestureDetector(
                      onTap: (){
                        if(isFinding){
                          onStop();
                        }else{
                          onStart();
                        }
                      },
                      child: Container(
                        height: 14,
                        width: 28,
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(10),
                          color:isFinding ? Color(0xFFAACD06): Color(0xFFF7F7F7)   ,
                        ),
                        child: Container(
                          margin: EdgeInsets.only(
                            left: isFinding ? 12 : 0,
                            right: isFinding ? 0 : 12,
                          ),
                          height: 10,
                          width: 10,
                          decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            color: isFinding ? Color(0xFFF7F7F7) : Color(0xFFAACD06)  ,
                          ),
                        ),
                      ),
                    )

                  ],
                )
              ],
            ),
          )
      );
  }

  //===============================导航========================================
  Widget _buildNavigationCard() {
    return
      Positioned(

          bottom: 170,
          right: 20,
          child: GestureDetector(
              onTap: (){
                //跳转弹窗
                showBottomSheetDialog(context);
              },
              child: Container(
                padding: EdgeInsets.symmetric(horizontal: 20,vertical: 10),
                height: 85,
                width: 158,
                decoration: BoxDecoration(
                  color: Color(0xFF333333).withOpacity(0.4),
                  borderRadius: BorderRadius.circular(10),
                ),
                child:Column(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    //图标
                    Container(
                      height: 36,
                      width: 36,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: Color(0xFFF7F7F7),
                      ),
                      child: Center(
                          child: Transform.rotate(
                            angle: 45 * 3.1415926/180,
                            child:Icon(Icons.navigation_outlined,color: Color(0xFFAACD06),size:24,),
                          )
                      ),
                    ),

                    Row(

                      children: [

                        SizedBox(width: 5,),
                        Text(S.current.navigation,style: TextStyle(color: Colors.white,fontSize: 13,fontWeight: FontWeight.bold),),
                        Spacer(),
                        Text("$meter" + S.current.meter,style: TextStyle(fontSize: 12,color: Colors.white,),)

                      ],
                    )
                  ],
                ),
              )
          )
      );

  }


  //==================================具体位置=======================
  Widget _buildLocationCard() {
    return
      Positioned(

          bottom: 20,
          right: 20,
          left: 20,
          child: Container(
            padding: EdgeInsets.symmetric(horizontal: 20,vertical: 10),
            height: 140,
            width: 325,
            decoration: BoxDecoration(
              color: Color(0xFF333333).withOpacity(0.4),
              borderRadius: BorderRadius.circular(10),
            ),
            child:Column(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [

                Text(S.current.position,style: TextStyle(color: Colors.white,fontSize: 18,fontWeight: FontWeight.bold),),

                //具体位置
                Row(
                  children: [
                    //图标
                    Container(
                      height: 40,
                      width: 40,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: Color(0xFFF7F7F7),
                      ),
                      child: Center(
                          child: Image.asset("assets/images/position_icon.png")
                      ),
                    ),

                    SizedBox(width: 15,),

                    Column(
                      children: [

                        //地址
                        Container(
                          width: 210,
                          child: Text(
                            address,
                            style: TextStyle(color: Colors.white, fontSize: 12),
                            softWrap: true,
                            maxLines: 4,
                            overflow: TextOverflow.ellipsis, // 超过4行显示省略号
                          ),
                        ),
                        //时间戳
                        Container(
                          width: 200,
                          child: Text(
                            time,
                            style: TextStyle(color: Colors.white, fontSize: 12),
                          ),
                        )
                      ],
                    )
                  ],
                )

              ],
            ),
          )
      );
  }


}
相关推荐
浩星2 小时前
electron系列7之Electron + Vue 3:构建现代化桌面应用(上)
javascript·vue.js·electron
m0_738120722 小时前
渗透测试基础ctfshow——Web应用安全与防护(四)
前端·安全·web安全·网络安全·flask·弱口令爆破
似水流年QC2 小时前
Chrome Performance 面板前端性能分析从入门到实战
前端·chrome
君穆南2 小时前
docker里面的minio的备份方法
前端
Thomas21432 小时前
--remote-debugging-port=9222 和 chrome://inspect/#remote-debugging 区别
前端·chrome
Luna-player2 小时前
二本生找前端工作
前端
M ? A2 小时前
Vue3 转 React 工具 VuReact v1.6.0 更新:useAttrs 完美兼容,修复模板迁移 / 类型错误
前端·javascript·vue.js·react.js·开源·vureact
迦南的迦 亚索的索2 小时前
PYTHON_DAY21_数据分析
开发语言·python·数据分析
低保和光头哪个先来2 小时前
解决 ios 使用 video 全屏未铺满页面问题
前端·javascript·vue.js·ios·前端框架