如何在Flutter中更改Google Maps标记的图标大小?

74

我在我的Flutter应用中使用google_maps_flutter来使用Google地图,我有自定义标记图标,并使用BitmapDescriptor.fromAsset("images/car.png")加载它。但是,我的地图上的图标尺寸太大了,我想让它变小,但是我找不到任何选项来实现。是否有更改自定义标记图标的选项。

这是我的Flutter代码:

mapController.addMarker(
        MarkerOptions(
          icon: BitmapDescriptor.fromAsset("images/car.png"),

          position: LatLng(
            deviceLocations[i]['latitude'],
            deviceLocations[i]['longitude'],
          ),
        ),
      );

这是我的Android模拟器截屏:

从图片中可以看到,我的自定义图标大小太大了。


5
把您的PNG文件保存为更小的尺寸? - MrUpsidown
1
@MrUpsidown,设备分辨率不同,我该怎么办? - Daniel.V
2
我实际上已经创建了一个PR(https://github.com/flutter/plugins/pull/815),提供了一种使用字节作为替代方案的方法,使其更加动态化(BitmapDescriptor.fromBytes())。然而,它仍未被合并到主分支,因为iOS还没有完成(我可能会在几天内完成)。 目前,恐怕您除了缩小资产之外没有太多选择。 - Miguel Ruivo
1
@moonvader 好的,没问题。如果你能在周一晚上之前提醒我,我会发一篇文章的,因为我现在无法访问我的电脑。 - Miguel Ruivo
1
@MiguelRuivo 星期一 - moonvader
显示剩余6条评论
14个回答

205

简而言之: 只要能够将任何图像编码为原始字节(如Uint8List),您就可以将其用作标记。


目前,您可以使用Uint8List数据来创建带有Google Maps的标记。这意味着只要保持正确的编码格式(在这种特定情况下是png),您就可以使用原始数据绘制任何您想要的地图标记。

我将通过两个示例进行说明,您可以选择:

  1. 选择本地资源并动态更改其大小以适应您想要的大小,并在地图上呈现它(一个Flutter徽标图像);
  2. 在画布中绘制一些内容,并将其呈现为标记,但这可以是任何渲染小部件。

除此之外,您甚至可以将渲染小部件转换为静态图像,因此也可以将其用作标记。


1. 使用资源

首先,创建一个处理资源路径并接收大小的方法(这可以是宽度、高度或两者都可以,但只使用一个将保持比例)。

import 'dart:ui' as ui;

Future<Uint8List> getBytesFromAsset(String path, int width) async {
  ByteData data = await rootBundle.load(path);
  ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(), targetWidth: width);
  ui.FrameInfo fi = await codec.getNextFrame();
  return (await fi.image.toByteData(format: ui.ImageByteFormat.png)).buffer.asUint8List();
}

然后,只需使用正确的描述符将其添加到您的地图中:

final Uint8List markerIcon = await getBytesFromAsset('assets/images/flutter.png', 100);
final Marker marker = Marker(icon: BitmapDescriptor.fromBytes(markerIcon));

这将分别生成50、100和200宽度的以下内容。

asset_example


2. 使用画布

您可以使用画布绘制任何想要的内容,然后将其用作标记。以下代码将生成一个简单的圆角框,并在其中显示“Hello world!”文本。

因此,首先使用画布绘制一些内容:

Future<Uint8List> getBytesFromCanvas(int width, int height) async {
  final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
  final Canvas canvas = Canvas(pictureRecorder);
  final Paint paint = Paint()..color = Colors.blue;
  final Radius radius = Radius.circular(20.0);
  canvas.drawRRect(
      RRect.fromRectAndCorners(
        Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()),
        topLeft: radius,
        topRight: radius,
        bottomLeft: radius,
        bottomRight: radius,
      ),
      paint);
  TextPainter painter = TextPainter(textDirection: TextDirection.ltr);
  painter.text = TextSpan(
    text: 'Hello world',
    style: TextStyle(fontSize: 25.0, color: Colors.white),
  );
  painter.layout();
  painter.paint(canvas, Offset((width * 0.5) - painter.width * 0.5, (height * 0.5) - painter.height * 0.5));
  final img = await pictureRecorder.endRecording().toImage(width, height);
  final data = await img.toByteData(format: ui.ImageByteFormat.png);
  return data.buffer.asUint8List();
}

然后以相同的方式使用它,但这次提供任何您想要的数据(例如宽度和高度),而不是资产路径。

final Uint8List markerIcon = await getBytesFromCanvas(200, 100);
final Marker marker = Marker(icon: BitmapDescriptor.fromBytes(markerIcon));

这就是你要的。

canvas_example


1
谢谢,这太棒了。在第一个例子中,targetWidth不起作用。 - otto
2
这是一个很好的解决方案,但在Flutter的Google Maps中,标记实现得非常糟糕。看看React版本、组件或替代地图插件,它们处理得更好(Marker只是一个带有纬度和经度的小部件;谁关心子元素是什么)。 - Oliver Dixon
做得好,@MiguelRuivo。真的很有帮助! - Thiyraash David
使用Canvas可以在“Hello World”文本下面添加一个空心圆吗? - Md. Kamrul Amin
@MiguelRuivo 你好,我在画布上绘制标记后遇到了被截断的问题。我在这里发布了一个关于此问题的独立问题:https://stackoverflow.com/questions/71282074/request-for-clarification-using-canvas-and-paint-in-flutter - NinFudj
显示剩余16条评论

10

我已更新上面的函数,现在您可以按照自己的意愿缩放图像。

  Future<Uint8List> getBytesFromCanvas(int width, int height, urlAsset) async {
    final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
    final Canvas canvas = Canvas(pictureRecorder);

    final ByteData datai = await rootBundle.load(urlAsset);
    var imaged = await loadImage(new Uint8List.view(datai.buffer));
    canvas.drawImageRect(
      imaged,
      Rect.fromLTRB(
          0.0, 0.0, imaged.width.toDouble(), imaged.height.toDouble()),
      Rect.fromLTRB(0.0, 0.0, width.toDouble(), height.toDouble()),
      new Paint(),
    );

    final img = await pictureRecorder.endRecording().toImage(width, height);
    final data = await img.toByteData(format: ui.ImageByteFormat.png);
    return data.buffer.asUint8List();
  }

加载图片错误:加载图片方法引用。https://gist.github.com/netsmertia/9c588f23391c781fa1eb791f0dce0768 - Navin Kumar
@NavinKumar 抱歉,我对你的问题一无所知。我的代码仅用于加载“png”文件,可能这就是原因。 - xuetongqin
什么是“loadImage()”方法?它没有被定义。 - Ma250
你应该自己写一个,或者你可以复制这个页面上的一个。 - xuetongqin

10

以下是2020年5月添加自定义Google地图标记的示例。

我的示例应用:

引入:

import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';

在您的主要有状态类中的某个位置实例化标记地图:

Map<MarkerId, Marker> markers = <MarkerId, Marker>{};

将图标资产转换为Uint8List对象的功能(一点也不复杂 /s):

Future<Uint8List> getBytesFromAsset(String path, int width) async {
    ByteData data = await rootBundle.load(path);
    ui.Codec codec =
        await ui.instantiateImageCodec(data.buffer.asUint8List(), targetWidth: width);
    ui.FrameInfo fi = await codec.getNextFrame();
    return (await fi.image.toByteData(format: ui.ImageByteFormat.png)).buffer.asUint8List();
   }

添加标记函数(使用您希望标记的纬度和经度坐标来调用此函数)

  Future<void> _addMarker(tmp_lat, tmp_lng) async {
    var markerIdVal = _locationIndex.toString();
    final MarkerId markerId = MarkerId(markerIdVal);
    final Uint8List markerIcon = await getBytesFromAsset('assets/img/pin2.png', 100);

    // creating a new MARKER
    final Marker marker = Marker(
      icon: BitmapDescriptor.fromBytes(markerIcon),
      markerId: markerId,
      position: LatLng(tmp_lat, tmp_lng),
      infoWindow: InfoWindow(title: markerIdVal, snippet: 'boop'),
    );

    setState(() {
      // adding a new marker to map
      markers[markerId] = marker;
    });
  }

pubspec.yaml文件(可以随意尝试不同的图标)

flutter:

  uses-material-design: true

  assets:
    - assets/img/pin1.png
    - assets/img/pin2.png

1
谢谢!我一直在努力调整标记的大小。目前,这种方法似乎有效。使用屏幕宽度/3.2来确定大小。 - Karel Debedts
@Alex 你向Flutter Web开发人员提交了错误报告吗?这应该做的。 - Ian Smith
@IanSmith ...目前在Web上尚未实现... 但也许有一天他们会加入它。 - Alex
惊人的,我喜欢它。 - SardorbekR

6

我有同样的问题,我是这样解决的。

Future < Uint8List > getBytesFromCanvas(int width, int height, urlAsset) async 
{
    final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
    final Canvas canvas = Canvas(pictureRecorder);
    final Paint paint = Paint()..color = Colors.transparent;
    final Radius radius = Radius.circular(20.0);
    canvas.drawRRect(
        RRect.fromRectAndCorners(
            Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()),
            topLeft: radius,
            topRight: radius,
            bottomLeft: radius,
            bottomRight: radius,
        ),
        paint);

    final ByteData datai = await rootBundle.load(urlAsset);

    var imaged = await loadImage(new Uint8List.view(datai.buffer));

    canvas.drawImage(imaged, new Offset(0, 0), new Paint());

    final img = await pictureRecorder.endRecording().toImage(width, height);
    final data = await img.toByteData(format: ui.ImageByteFormat.png);
    return data.buffer.asUint8List();
}

Future < ui.Image > loadImage(List < int > img) async {
    final Completer < ui.Image > completer = new Completer();
    ui.decodeImageFromList(img, (ui.Image img) {

        return completer.complete(img);
    });
    return completer.future;
}

你可以像这样使用。

final Uint8List markerIcond = await getBytesFromCanvas(80, 98, urlAsset);

setState(() {

    markersMap[markerId] = Marker(
        markerId: MarkerId("marker_${id}"),
        position: LatLng(double.parse(place.lat), double.parse(place.lng)),

        icon: BitmapDescriptor.fromBytes(markerIcond),
        onTap: () {
            _onMarkerTapped(placeRemote);
        },

    );
});

1
如果你想要扩展,请查看@xuetongqin的答案。 - otto

6
所有给出的答案都很完美,但是我注意到当你将targetWidth设置为一个特定数字时,你可能会在具有不同devicePixelRatio的不同手机上出现问题。这是我实施它的方法。
import 'dart:ui' as ui;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';


  Future<Uint8List> getBytesFromAsset(String path) async {
    double pixelRatio = MediaQuery.of(context).devicePixelRatio;
    ByteData data = await rootBundle.load(path);
    ui.Codec codec = await ui.instantiateImageCodec(
        data.buffer.asUint8List(),
        targetWidth: pixelRatio.round() * 30
    );
    ui.FrameInfo fi = await codec.getNextFrame();
    return (await fi.image.toByteData(format: ui.ImageByteFormat.png)).buffer.asUint8List();
  }

然后像这样使用该方法

final Uint8List markerIcon = await getBytesFromAsset('assets/images/bike.png');

Marker(icon: BitmapDescriptor.fromBytes(markerIcon),)

这使得我的大小具有根据devicePixelRatio变化的动态性。

这对我来说完美地解决了问题。


2
"BitmapDescriptor.fromAsset() 是添加标记的正确方法,但存在一个影响您的代码的已知 bug。正如 Saed 所回答的那样,您需要为不同设备屏幕密度提供不同尺寸的图像。从您提供的图像来看,我猜想您想要的图像基本大小应该是 48 像素左右。因此,您需要制作尺寸为 48、96(2.0x)和 144(3.0x)的副本。

运行时应根据屏幕密度选择正确的图像。请参见 https://flutter.dev/docs/development/ui/assets-and-images#declaring-resolution-aware-image-assets

目前在 Android 或 Fuschia 上还未自动完成此操作。如果您现在要发布并想解决此问题,可以使用以下逻辑检查平台:

"
    MediaQueryData data = MediaQuery.of(context);
    double ratio = data.devicePixelRatio;

    bool isIOS = Theme.of(context).platform == TargetPlatform.iOS;

如果平台不是iOS,则需要在你的代码中实现桶。将逻辑合并到一个方法中:
String imageDir(String prefix, String fileName, double pixelRatio, bool isIOS) {
    String directory = '/';
    if (!isIOS) {
        if (pixelRatio >= 1.5) {
            directory = '/2.0x/';
        }
        else if (pixelRatio >= 2.5) {
            directory = '/3.0x/';
        }
        else if (pixelRatio >= 3.5) {
            directory = '/4.0x/';
        }
    }
    return '$prefix$directory$fileName';
}

您可以使用以下代码在 assets/map_icons/ 目录下创建一个名为 person_icon 的图标标记,使用该方法:
            myLocationMarker = Marker(
            markerId: MarkerId('myLocation'),
            position: showingLocation, flat: true,
            icon: BitmapDescriptor.fromAsset(imageDir('assets/map_icons','person_icon.png', ratio, isIos)));

2
自从 google_map_flutter 0.5.26 版本开始,fromAsset() 已被弃用,应该使用 fromAssetImage() 替代,就像其他答案中提到的一样。更优雅的应用 fromAssetImage() 来适应不同分辨率设备的方法是声明分辨率感知型图像资源。Flutter 使用逻辑像素来渲染屏幕,如果我没记错的话,每英寸大约为 72 像素,而现代移动设备可能包含超过 200 像素每英寸。使图像在具有不同像素密度的不同移动设备上看起来相似的解决方案是准备多个不同大小的相同图像副本,在像素密度较低的设备上使用较小的图像,在像素密度较高的设备上使用较大的图像。

因此,您应该准备以下示例图像:

images/car.png           <-- if this base image is 100x100px
images/2.0x/car.png      <-- 2.0x one should be 200x200px
images/3.0x/car.png      <-- and 3.0x one should be 300x300px

并将您的代码修改如下,其中createLocalImageConfiguration()将根据设备像素比应用正确的缩放。
mapController.addMarker(
        MarkerOptions(
          icon: BitmapDescriptor.fromAssetImage(
                  createLocalImageConfiguration(context),
                  "images/car.png"),
          position: LatLng(
            deviceLocations[i]['latitude'],
            deviceLocations[i]['longitude'],
          ),
        ),
      );

以下是最新版google_map_flutter 1.0.3fromAssetImage()实现。你可以看到BitmapDescriptor的底层实现需要一个scale参数,这是获取正确图片大小的关键。
  static Future<BitmapDescriptor> fromAssetImage(
    ImageConfiguration configuration,
    String assetName, {
    AssetBundle bundle,
    String package,
    bool mipmaps = true,
  }) async {
    if (!mipmaps && configuration.devicePixelRatio != null) {
      return BitmapDescriptor._(<dynamic>[
        'fromAssetImage',
        assetName,
        configuration.devicePixelRatio,
      ]);
    }
    final AssetImage assetImage =
        AssetImage(assetName, package: package, bundle: bundle);
    final AssetBundleImageKey assetBundleImageKey =
        await assetImage.obtainKey(configuration);
    return BitmapDescriptor._(<dynamic>[
      'fromAssetImage',
      assetBundleImageKey.name,
      assetBundleImageKey.scale,
      if (kIsWeb && configuration?.size != null)
        [
          configuration.size.width,
          configuration.size.height,
        ],
    ]);
  }

注意:您可以看到ImageConfiguration的size属性仅适用于Web。

createLocalImageConfiguration(context) 这个是从哪里来的?它为什么不工作呢?:'( - Sunshine
@Sunshine,它在Flutter的小部件库中。在我发布这个答案时,它可以直接使用而无需导入任何内容。自那以后我没有编写过任何Dart/Flutter代码,可能已经发生了变化。请尝试按照此处建议的方式导入import package:flutter/widgets.dart - Lance Chen

1

以下是我在选择不同密度图像时的有效方法:

MediaQueryData mediaQueryData = MediaQuery.of(context);
ImageConfiguration imageConfig = ImageConfiguration(devicePixelRatio: mediaQueryData.devicePixelRatio);
BitmapDescriptor.fromAssetImage(imageConfig, "assets/images/marker.png");

1

我将添加一个解决方案,混合来自任何地方的多个想法和代码来解决此问题,首先是一个管理图像大小的函数:

Future<Uint8List> getBytesFromCanvas(double escala, urlAsset) async {

  final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
  final Canvas canvas = Canvas(pictureRecorder);

  final ByteData datai = await rootBundle.load(urlAsset);
  var imaged = await loadImage(new Uint8List.view(datai.buffer));

  double width = ((imaged.width.toDouble() * escala).toInt()).toDouble();
  double height = ((imaged.height.toDouble() * escala).toInt()).toDouble();

  canvas.drawImageRect(imaged, Rect.fromLTRB(0.0, 0.0, imaged.width.toDouble(), imaged.height.toDouble()),
                              Rect.fromLTRB(0.0, 0.0, width, height),
                              new Paint(),
  );

  final img = await pictureRecorder.endRecording().toImage(width.toInt(), height.toInt());
  final data = await img.toByteData(format: ui.ImageByteFormat.png);
  return data.buffer.asUint8List();

}

Future < ui.Image > loadImage(List < int > img) async {
  final Completer < ui.Image > completer = new Completer();
  ui.decodeImageFromList(img, (ui.Image img) {

    return completer.complete(img);
  });
  return completer.future;
}

然后根据设备 IOS 或 Android 应用此函数。getBytesFromCanvas() 函数需要两个参数,图像实际大小的比例和资源 URL。

var iconTour;

bool isIOS = Theme.of(context).platform == TargetPlatform.iOS;
if (isIOS){

  final markerIcon = await getBytesFromCanvas(0.7, 'images/Icon.png');
  iconTour = BitmapDescriptor.fromBytes(markerIcon);

}
else{

  final markerIcon = await getBytesFromCanvas(1, 'images/Icon.png');
  iconTour = BitmapDescriptor.fromBytes(markerIcon);

}

setState(() {
  final Marker marker = Marker(icon: iconTour);
});

这就是全部。


1
我发现了解决这个问题最简单的方法。
我使用了以下版本来实现Google地图。在较低版本的Google地图中,BitmapDescriptor.fromBytes无法正常工作。
 google_maps_flutter: ^0.5.19

并设置标记点,如


Future setMarkersPoint() async {
  var icon = 'your url';
  Uint8List dataBytes;
  var request = await http.get(icon);
  var bytes = await request.bodyBytes;

  setState(() {
    dataBytes = bytes;
  });

  final Uint8List markerIcoenter code heren =
      await getBytesFromCanvas(150, 150, dataBytes);

  var myLatLong = LatLng(double.parse(-6.9024812),
      double.parse(107.61881));

  _markers.add(Marker(
    markerId: MarkerId(myLatLong.toString()),
    icon: BitmapDescriptor.fromBytes(markerIcon),
    position: myLatLong,
   infoWindow: InfoWindow(
     title: 'Name of location',
    snippet: 'Marker Description',
   ),
  ));

}

如果您想更改图标大小,请使用以下代码。

Future<Uint8List> getBytesFromCanvas(
  int width, int height, Uint8List dataBytes) async {
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
final Canvas canvas = Canvas(pictureRecorder);
final Paint paint = Paint()..color = Colors.transparent;
final Radius radius = Radius.circular(20.0);
canvas.drawRRect(
    RRect.fromRectAndCorners(
      Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()),
      topLeft: radius,
      topRight: radius,
      bottomLeft: radius,
      bottomRight: radius,
    ),
    paint);

var imaged = await loadImage(dataBytes.buffer.asUint8List());
canvas.drawImageRect(
  imaged,
  Rect.fromLTRB(
      0.0, 0.0, imaged.width.toDouble(), imaged.height.toDouble()),
  Rect.fromLTRB(0.0, 0.0, width.toDouble(), height.toDouble()),
  new Paint(),
);

    final img = await pictureRecorder.endRecording().toImage(width, height);
    final data = await img.toByteData(format: ui.ImageByteFormat.png);
    return data.buffer.asUint8List();
 }

    Future<ui.Image> loadImage(List<int> img) async {
    final Completer<ui.Image> completer = new Completer();
    ui.decodeImageFromList(img, (ui.Image img) {
  return completer.complete(img);
});
return completer.future;
}

enter image description here

希望它能对你有用..!!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接