为了让外卖小哥在地图里开上火箭🚀我用FLutter自定义了地图

简介: 花了五天时间,用Flutter自定义地图是什么体验?外卖小哥都开上火箭了?什么?我被女朋友赶出家门啦?欢迎观看被女友赶出家门之开火箭送外卖篇~

前言:想完成美团一样的自定义地图组件吗?想要软件和美团一样丝滑吗?想要工资和美团一样高吗?现在是白天,你想做白日梦吗?大家好,我是阿T。又过了一周,这次主要是使用高德地图SDK实现了自定义的地图,为啥要做这个呢,因为网上关于Flutter的地图处理文章实在是太少了,更不要说是自定义地图了,然后官网文档又太简略,害,说多了都是泪,于是我付出了少陪女朋友5天的时间,把这篇文章写了出来!!

先上效果图:

效果图.gif
296f9859ab5197057fee20f19a92e9a.jpg

还有其他很多小功能就不展示了,大家可以自己运行看看~

阅读本文的注意点:

1.测试代码需要使用真机,模拟器无法加载地图(可以定位,但是无法加载地图,可能与版本有关)

2.本文使用的插件:

permission_handler: ^8.1.4 #权限管理
amap_flutter_map: 2.0.2 #高德地图

正文:

1.在高德的开发者平台申请key:

第一步:注册开发者账号

高德开发者地址:https://console.amap.com/

申请1.png

第二步:创建新的应用,并申请key

申请key.png

第三步:

申请3.png
关于如何获取SHA1,以及处理高德定位使用中的报错在这篇文章里(下一篇文章,还在努力码字😭,给个赞吧)

第四步获取到key:

key申请成功.png

2.处理使用高德地图的权限

第一步:添加定位所需要的权限

<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!--访问网络-->
<uses-permission android:name="android.permission.INTERNET" />
<!--粗略定位-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--精确定位-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!--申请调用A-GPS模块-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--用于获取运营商信息,用于支持提供运营商信息相关的接口-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--用于访问wifi网络信息,wifi信息会用于进行网络定位-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--用于获取wifi的获取权限,wifi信息会用来进行网络定位-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!--用于读取手机当前的状态-->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!--用于写入缓存数据到扩展存储卡-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

第二步:添加自己刚刚申请的key,添加开发详细定位

<meta-data
    android:name="com.amap.api.v2.apikey"
    android:value="自己的key" />
<service android:name="com.amap.api.location.APSService"/>

第三步:配置一些key.jks

如何生成key.jks在这一篇文章里

生成完后创建一个key.properties

storePassword=你自己的密码
keyPassword=你自己的密码
keyAlias=key
storeFile=存放的位置(D:\\flutter_gaode_keystore\\key.jks)

然后在app文件夹下的build.gradle中使用:

先找到key.properties

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

然后在signingConfigs下引用:

signingConfigs {
    release {
        keyAlias keystoreProperties['keyAlias']
        keyPassword keystoreProperties['keyPassword']
        storeFile file(keystoreProperties['storeFile'])
        storePassword keystoreProperties['storePassword']
    }
}

第四步:导入地图

仍在app文件夹下的build.gradle中

dependencies {
    implementation('com.amap.api:location:5.2.0')
    implementation 'com.amap.api:3dmap:7.6.0'
    implementation 'com.amap.api:search:5.0.0'
}
sourceSets {
    //添加地图SDK引入路径
    main {
        jniLibs.srcDirs = ['libs']
    }
}

并在buildTypes下添加一下两行:

buildTypes {
    release {
        signingConfig signingConfigs.release
        minifyEnabled false //删除无用代码
        shrinkResources false //删除无用资源
    }
}

这样就配置好了~

3.加载高德地图

接下来的部分为代码实现部分,只展示核心代码🐷

微信图片_20211016143251.jpg

第一步:添加申请到的key

class ConstConfig {
  ///配置申请的apikey,在此处配置之后,可以在初始化[AMapWidget]时,通过`apiKey`属性设置
  ///
  ///注意:使用[AMapWidget]的`apiKey`属性设置的key的优先级高于通过Native配置key的优先级,
  ///使用[AMapWidget]的`apiKey`属性配置后Native配置的key将失效,请根据实际情况选择使用
  static const AMapApiKey amapApiKeys = AMapApiKey(
      androidKey: '你自己申请的key',
      iosKey: '没有申请ios的话先填android的key,这样也可以在安卓手机测试');
}

第二步:定义一下地图类型

//地图类型
MapType _mapType;

final Map<String, MapType> _radioValueMap = {
  '普通地图': MapType.normal,
  '卫星地图': MapType.satellite,
  '导航地图': MapType.navi,
  '公交地图': MapType.bus,
  '黑夜模式': MapType.night,
};

初始化地图:

@override
void initState() {
  super.initState();
   //默认为普通地图
  _mapType = MapType.normal;
}

创建地图:

//创建地图
final AMapWidget map = AMapWidget(
  apiKey: ConstConfig.amapApiKeys,
  //地图类型属性
  mapType: _mapType ?? MapType.normal,
);

第三步:显示地图

AMapRadioGroup(
  groupLabel: '地图样式',
  groupValue: _mapType,
  radioValueMap: _radioValueMap,
  onChanged: (value) => {
    //改变当前地图样式为选中的样式
    setState(() {
      _mapType = value;
    })
  },
)

第四步:默认地址

//默认显示在北京天安门
static final CameraPosition _kInitialPosition = const CameraPosition(
  target: LatLng(39.909187, 116.397451),
  zoom: 10.0,
);

4.自定义地图

微信图片_202110161432511.jpg

第一步:加载自定义地图的样式

//用于记录是否为自定义地图
bool _mapCreated = false;

//加载自定义地图样式
void _loadCustomData() async {
    if (null == _customStyleOptions) {
      _customStyleOptions = CustomStyleOptions(false);
    }
    ByteData styleByteData = await rootBundle.load('assets/style.data');
    _customStyleOptions.styleData = styleByteData.buffer.asUint8List();
    ByteData styleExtraByteData =
        await rootBundle.load('assets/style_extra.data');
    _customStyleOptions.styleExtraData =
        styleExtraByteData.buffer.asUint8List();
    //如果需要加载完成后直接展示自定义地图,可以通过setState修改CustomStyleOptions的enable为true
    setState(() {
      _customStyleOptions.enabled = true;
    });
}

第二步:定义地图

final AMapWidget map = AMapWidget(
  apiKey: ConstConfig.amapApiKeys,
  onMapCreated: onMapCreated,
  customStyleOptions: _customStyleOptions,
);

void onMapCreated(AMapController controller) {
    if (null != controller) {
      _mapCreated = true;
    }
}

第三步:点击切换为自定义地图

AMapSwitchButton(
  label: Text(
    '自定义地图',
    style: TextStyle(color: Colors.white),
  ),
  defaultValue: _customStyleOptions.enabled,
  onSwitchChanged: (value) => {
    if (_mapCreated)
      {
        setState(() {
          _customStyleOptions.enabled = value;
        })
      }
  },
)

5.让外卖小哥飞起来

第一步:获取自定义的图片

有三种方式:

第一种:

///通过BitmapDescriptor.fromAssetImage的方式获取图片
Future<void> _createMarkerImageFromAsset(BuildContext context) async {
  if (_markerIcon == null) {
    final ImageConfiguration imageConfiguration =
        createLocalImageConfiguration(context);
    BitmapDescriptor.fromAssetImage(imageConfiguration, 'assets/start.png')
        .then(_updateBitmap);
  }
}

第二种:

///通过BitmapDescriptor.fromBytes的方式获取图片
  Future<void> _createMarkerImageFromBytes(BuildContext context) async {
    final Completer<BitmapDescriptor> bitmapIcon =
        Completer<BitmapDescriptor>();
    final ImageConfiguration config = createLocalImageConfiguration(context);

    const AssetImage('assets/end.png')
        .resolve(config)
        .addListener(ImageStreamListener((ImageInfo image, bool sync) async {
      final ByteData bytes =
          await image.image.toByteData(format: ImageByteFormat.png);
      final BitmapDescriptor bitmap =
          BitmapDescriptor.fromBytes(bytes.buffer.asUint8List());
      bitmapIcon.complete(bitmap);
    }));

    bitmapIcon.future.then((value) => _updateBitmap(value));
  }

第三种(最简单的一种):

//最简单的方式
if (null == _markerIcon) {
  _markerIcon = BitmapDescriptor.fromIconPath('assets/location_marker.png');
}

第二步:根据x,y轴计算图片要显示的位置

void _changeAnchor() {
  final Marker marker = _markers[selectedMarkerId];
  if (marker == null) {
    return;
  }
  final Offset currentAnchor = marker.anchor;
  double dx = 0;
  double dy = 0;
  if (currentAnchor.dx < 1) {
    dx = currentAnchor.dx + 0.1;
  } else {
    dx = 0;
  }
  if (currentAnchor.dy < 1) {
    dy = currentAnchor.dy + 0.1;
  } else {
    dy = 0;
  }
  final Offset newAnchor = Offset(dx, dy);
  setState(() {
    _markers[selectedMarkerId] = marker.copyWith(
      anchorParam: newAnchor,
    );
  });
}

这样就可以将外卖小哥展示到地图上了,还有一些其他的小功能

修改图片的位置

void _changePosition() {
  final Marker marker = _markers[selectedMarkerId];
  final LatLng current = marker.position;
  final Offset offset = Offset(
    mapCenter.latitude - current.latitude,
    mapCenter.longitude - current.longitude,
  );
  setState(() {
    _markers[selectedMarkerId] = marker.copyWith(
      positionParam: LatLng(
        mapCenter.latitude + offset.dy,
        mapCenter.longitude + offset.dx,
      ),
    );
  });
}

Future<void> _changeAlpha() async {
  final Marker marker = _markers[selectedMarkerId];
  final double current = marker.alpha;
  setState(() {
    _markers[selectedMarkerId] = marker.copyWith(
      alphaParam: current < 0.1 ? 1.0 : current * 0.75,
    );
  });
}

修改图片的角度:

Future<void> _changeRotation() async {
  final Marker marker = _markers[selectedMarkerId];
  final double current = marker.rotation;
  setState(() {
    _markers[selectedMarkerId] = marker.copyWith(
      rotationParam: current == 330.0 ? 0.0 : current + 30.0,
    );
  });
}

6.添加多个图标在地图上

1.定义marker

static final LatLng mapCenter = const LatLng(39.909187, 116.397451);
 //需要先设置一个空的map赋值给AMapWidget的markers,否则后续无法添加marker
final Map<String, Marker> _initMarkerMap = <String, Marker>{};

2.增加marker

添加一个:

void _addMarker() {
  final _markerPosition =
      LatLng(_currentLatLng.latitude, _currentLatLng.longitude + 2 / 1000);
  final Marker marker = Marker(
    position: _markerPosition,
    //使用默认hue的方式设置Marker的图标
    icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueOrange),
  );
  //调用setState触发AMapWidget的更新,从而完成marker的添加
  setState(() {
    _currentLatLng = _markerPosition;
    //将新的marker添加到map里
    _markers[marker.id] = marker;
  });
}

添加多个:

for(int i=0; i< 10; i++) {
  LatLng position = LatLng(
      mapCenter.latitude + sin(i * pi / 12.0) / 20.0,
      mapCenter.longitude + cos(i * pi / 12.0) / 20.0);
  Marker marker = Marker(position: position);
  _initMarkerMap[marker.id] = marker;
}

7.辅助工具toast

封装了一个toast,通过OverlayEntry添加到屏幕上

显示它的核心代码:

if (_overlayEntry == null) {
  //OverlayEntry负责构建布局
  //通过OverlayEntry将构建的布局插入到整个布局的最上层
  _overlayEntry = OverlayEntry(
      builder: (BuildContext context) => Positioned(
            //top值,可以改变这个值来改变toast在屏幕中的位置
            top: buildToastPosition(context),
            child: Container(
                alignment: Alignment.center,
                width: MediaQuery.of(context).size.width,
                child: Padding(
                  padding: EdgeInsets.symmetric(horizontal: 40.0),
                  child: AnimatedOpacity(
                    opacity: _showing ? 1.0 : 0.0, //目标透明度
                    duration: _showing
                        ? Duration(milliseconds: 100)
                        : Duration(milliseconds: 400),
                    child: _buildToastWidget(),
                  ),
                )),
          ));
  //插入到整个布局的最上层
  overlayState.insert(_overlayEntry);
} else {
  //重新绘制UI,类似setState
  _overlayEntry.markNeedsBuild();
}

绘制核心代码:

//toast绘制
static _buildToastWidget() {
  return Center(
    child: Card(
      color: _bgColor,
      child: Padding(
        padding: EdgeInsets.symmetric(
            horizontal: _pdHorizontal, vertical: _pdVertical),
        child: Text(
          _msg,
          style: TextStyle(
            fontSize: _textSize,
            color: _textColor,
          ),
        ),
      ),
    ),
  );
}

设置toast的位置:

//  设置toast位置
  static buildToastPosition(context) {
    var backResult;
    if (_toastPosition == ToastPostion.top) {
      backResult = MediaQuery.of(context).size.height * 1 / 4;
    } else if (_toastPosition == ToastPostion.center) {
      backResult = MediaQuery.of(context).size.height * 2 / 5;
    } else {
      backResult = MediaQuery.of(context).size.height * 3 / 4;
    }
    return backResult;
  }

这样基本功能就讲解完成啦,还有其他功能源代码里都有写到,比如路况显示,限制地图大小等等

本文结束了,看到这里的兄弟们给点点赞吧,女朋友已经让我睡家门口了😭

(太长时间没理她怎么哄...)评论区告诉小弟😭

相关文章
|
15天前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
79 1
|
17天前
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
114 1
|
17天前
|
前端开发 开发者
深入探索 Flutter 鸿蒙版的画笔使用与高级自定义动画
本文深入探讨了 Flutter 中的绘图功能,重点介绍了 CustomPainter 和 Canvas 的使用方法。通过示例代码,详细讲解了如何绘制自定义图形、设置 Paint 对象的属性以及实现高级自定义动画。内容涵盖基本绘图、动画基础、渐变动画和路径动画,帮助读者掌握 Flutter 绘图与动画的核心技巧。
65 1
|
17天前
|
Dart UED 开发者
Flutter&鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面的重要组成部分。Flutter提供了多种内置按钮组件,但有时这些样式无法满足特定设计需求。因此,封装一个自定义按钮组件变得尤为重要。自定义按钮组件可以确保应用中所有按钮的一致性、可维护性和可扩展性,同时提供更高的灵活性,支持自定义颜色、形状和点击事件。本文介绍了如何创建一个名为CustomButton的自定义按钮组件,并详细说明了其样式、形状、颜色和点击事件的处理方法。
67 1
|
17天前
|
Dart 搜索推荐 API
Flutter & 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理
在现代移动应用开发中,用户体验至关重要。本文探讨了如何在 Flutter 与鸿蒙操作系统(HarmonyOS)中创建自定义对话框,并结合表单验证实现动态反馈与错误处理,提升用户体验。通过自定义对话框和表单验证,开发者可以提供更加丰富和友好的交互体验,同时利用鸿蒙next版本拓展应用的受众范围。
68 1
|
2月前
|
前端开发 搜索推荐
Flutter中自定义气泡框效果的实现
Flutter中自定义气泡框效果的实现
83 3
|
3月前
|
前端开发
Flutter快速实现自定义折线图,支持数据改变过渡动画
Flutter快速实现自定义折线图,支持数据改变过渡动画
93 4
Flutter快速实现自定义折线图,支持数据改变过渡动画
|
3月前
|
开发者 监控 开发工具
如何将JSF应用送上云端?揭秘在Google Cloud Platform上部署JSF应用的神秘步骤
【8月更文挑战第31天】本文详细介绍如何在Google Cloud Platform (GCP) 上部署JavaServer Faces (JSF) 应用。首先,确保已准备好JSF应用并通过Maven构建WAR包。接着,使用Google Cloud SDK登录并配置GCP环境。然后,创建`app.yaml`文件以配置Google App Engine,并使用`gcloud app deploy`命令完成部署。最后,通过`gcloud app browse`访问应用,并利用GCP的监控和日志服务进行管理和故障排查。整个过程简单高效,帮助开发者轻松部署和管理JSF应用。
60 0
|
3月前
|
开发者 容器 Java
Azure云之旅:JSF应用的神秘部署指南,揭开云原生的新篇章!
【8月更文挑战第31天】本文探讨了如何在Azure上部署JavaServer Faces (JSF) 应用,充分发挥其界面构建能力和云平台优势,实现高效安全的Web应用。Azure提供的多种服务如App Service、Kubernetes Service (AKS) 和DevOps简化了部署流程,并支持应用全生命周期管理。文章详细介绍了使用Azure Spring Cloud和App Service部署JSF应用的具体步骤,帮助开发者更好地利用Azure的强大功能。无论是在微服务架构下还是传统环境中,Azure都能为JSF应用提供全面支持,助力开发者拓展技术视野与实践机会。
18 0
|
3月前
|
开发框架 API 开发者
Flutter表单控件深度解析:从基本构建到高级自定义,全方位打造既美观又实用的移动端数据输入体验,让应用交互更上一层楼
【8月更文挑战第31天】在构建美观且功能强大的移动应用时,表单是不可或缺的部分。Flutter 作为热门的跨平台开发框架,提供了丰富的表单控件和 API,使开发者能轻松创建高质量表单。本文通过问题解答形式,深入解读 Flutter 表单控件,并通过具体示例代码展示如何构建优秀的移动应用表单。涵盖创建基本表单、处理表单提交、自定义控件样式、焦点管理和异步验证等内容,适合各水平开发者学习和参考。
81 0