Привет! Меня зовут Анна Ахлёстова, я Flutter-разработчик в Friflex. Мы разрабатываем мобильные приложения для бизнеса и специализируемся на Flutter. В статье я расскажу о том, как мы решаем основные задачи бизнеса с использованием возможностей Яндекс Карт на Flutter.
В связи с некоторыми трудностями в оплате сервисов Google Map API в РФ мы во Friflex в реализации новых проектов российских заказчиков стали использовать Яндекс Карты.
Для подключения Яндекс Карт к приложению Flutter существует специальный плагин yandex_mapkit. Подробно о его преимуществах, недостатках и процессе интеграции в проект можете ознакомиться в статье моего коллеги.
К сожалению, подробная официальная документация об использовании ключевых возможностей плагина отсутствует. Именно поэтому я хочу рассказать о пяти базовых функциях, которые мы применяем для решения основных задач наших заказчиков, а также поделиться опытом их внедрения.
Для начала выделим основной список задач, которые чаще всего стоят перед разработчиками при внедрении карт в коммерческий проект:
отобразить список объектов (магазины, пункты выдачи и т.д.) точками на карте по заданным координатам;
дать возможность пользователю просмотреть дополнительную информацию об объекте при нажатии на него;
определить текущее местоположение пользователя и отобразить его точкой на карте;
выделить доступные для взаимодействия зоны на карте (зоны доставки и т.д.);
построить дорожные маршруты для перемещения от точки А до точки Б.
В этой статье я расскажу о том, как реализовать первые две задачи – отображение списка маркеров на карте и предоставление пользователю возможности взаимодействовать с ними. А также узнаем, с какими проблемами в реализации можно столкнуться и как легко их решить.
Чтобы подробно разобраться, как решить каждую из этих задач стандартными средствами библиотеки Yandex Mapkit, реализуем простой проект.
После полноценного подключения плагина yandex_mapkit в проект реализуем простое приложение:
Файл main.dart
import 'package:flutter/material.dart';
import 'package:yandex_mapkit_demo/presentation/map_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Yandex Mapkit Demo',
home: MapScreen(),
);
}
}
Файл map_screen.dart
import 'package:flutter/material.dart';
import 'package:yandex_mapkit/yandex_mapkit.dart';
import 'package:yandex_mapkit_demo/data/map_point.dart';
import 'package:yandex_mapkit_demo/presentation/clusterized_icon_painter.dart';
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
late final YandexMapController _mapController;
@override
void dispose() {
_mapController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Yandex Mapkit Demo')),
body: YandexMap(
onMapCreated: (controller) async {
_mapController = controller;
await _mapController.moveCamera(
CameraUpdate.newCameraPosition(
const CameraPosition(
target: Point(
latitude: 50,
longitude: 20,
),
zoom: 3,
),
),
);
},
),
);
}
}
На этом этапе мы получаем следующий результат:
Отображение объектов точками на карте
Как показывает опыт, это одна из самых востребованных у бизнеса фичей, связанных с картами. Разберем, как можно ее реализовать с использованием Yandex Mapkit.
Создадим модель точки на карте MapPoint, с помощью которой мы сможем хранить не только данные о координатах точки, как экземпляры класса Point, но и название населенного пункта:
Файл map_point.dart
import 'package:equatable/equatable.dart';
/// Модель точки на карте
class MapPoint extends Equatable {
const MapPoint({
required this.name,
required this.latitude,
required this.longitude,
});
/// Название населенного пункта
final String name;
/// Широта
final double latitude;
/// Долгота
final double longitude;
@override
List<Object?> get props => [name, latitude, longitude];
}
Теперь сгенерируем список объектов MapPoint, которые нам необходимо отобразить на карте. Для этого создадим приватный метод _getMapPoints()
в файле map_screen.dart:
/// Метод для генерации точек на карте
List<MapPoint> _getMapPoints() {
return const [
MapPoint(name: 'Москва', latitude: 55.755864, longitude: 37.617698),
MapPoint(name: 'Лондон', latitude: 51.507351, longitude: -0.127696),
MapPoint(name: 'Рим', latitude: 41.887064, longitude: 12.504809),
MapPoint(name: 'Париж', latitude: 48.856663, longitude: 2.351556),
MapPoint(name: 'Стокгольм', latitude: 59.347360, longitude: 18.341573),
];
}
Список объектов подготовлен, теперь можно переходить к их отображению на карте.
Основной класс объекта на карте — MapObject
. Это абстрактный класс для всех объектов, которых могут быть добавлены на карту (маркеры, зоны, линии и т.д.).
Основной класс маркера (точки на карте) — PlacemarkMapObject
. Экземпляр этого класса позволяет на карте отобразить точку по заданным координатам.
Обязательные параметры объекта:
mapObjectId
— уникальный идентификатор, который позволяет отличать текущий объект на карте от других;point
— координаты точки, для которой создается маркер.
Опционально можно задать:
opacity
— прозрачность маркера на карте (opacity);icon
— иконку для маркера;onTap
— функцию по тапу на маркер;isDraggable
— возможность пользователя перемещать маркер по карте;onDrag
,onDragStart
,onDragEnd
— функции, выполняемые во время перемещения и т.д.
К счастью, YandexMapkit дает возможность разработчикам максимально кастомизировать внешний вид маркеров при их построении на карте. Таким образом у нас есть возможность выбрать один из двух объектов иконки:
PlacemarkIcon.single
— можно использовать, когда иконка простая, состоит из одного изображения;PlacemarkIcon.composite
— можно использовать, когда иконка маркера сложная и требует добавления нескольких изображений.
Также стоит учесть, что изображение для иконки маркера может быть передано в двух форматах: либо изображением формата .png
из assets приложения, либо в байтовом формате. Это также может быть полезно, когда иконка сложная и требует отрисовки с помощью Canvas. Для добавления такого графического объекта его достаточно конвертировать в формат Uint8List
.
В рамках нашего проекта реализуем метод _getPlacemarkObjects()
, который по списку точек из метода _getMapPoints()
будет генерировать маркеры с простыми иконками, состоящими из одного изображения из assets:
Файл map_screen.dart
/// Метод для генерации объектов маркеров для отображения на карте
List<PlacemarkMapObject> _getPlacemarkObjects(BuildContext context) {
return _getMapPoints()
.map(
(point) => PlacemarkMapObject(
mapId: MapObjectId('MapObject $point'),
point: Point(latitude: point.latitude, longitude: point.longitude),
opacity: 1,
icon: PlacemarkIcon.single(
PlacemarkIconStyle(
image: BitmapDescriptor.fromAssetImage(
'assets/icons/map_point.png',
),
scale: 2,
),
),
),
)
.toList();
}
Для отображения любых объектов карты, наследуемых от MapObject, необходимо в поле mapObjects виджета YandexMap передать список этих объектов.
В нашем случае это будет реализовано так:
Файл map_screen.dart
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Yandex Mapkit Demo')),
body: YandexMap(
onMapCreated: (controller) async {
_mapController = controller;
await _mapController.moveCamera(
CameraUpdate.newCameraPosition(
const CameraPosition(
target: Point(
latitude: 50,
longitude: 20,
),
zoom: 3,
),
),
);
},
mapObjects: _getPlacemarkObjects(context),
),
);
}
Также стоит упомянуть о том, что очень часто перед разработчиком стоит дополнительная задача — реализовать вывод дополнительной информации о точке на карте при нажатии на маркер.
Для реализации этой функции достаточно передать необходимые операции в обратный вызов onTap()
в объекте PlacemarkMapObject
.
В нашем примере добавим вызов модального окна и отобразим в нем название населенного пункта и координаты точки. Для этого создадим виджет содержимого модального окна _ModalBodyView
:
/// Содержимое модального окна с информацией о точке на карте
class _ModalBodyView extends StatelessWidget {
const _ModalBodyView({required this.point});
final MapPoint point;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 40),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(point.name, style: const TextStyle(fontSize: 20)),
const SizedBox(height: 20),
Text(
'${point.latitude}, ${point.longitude}',
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
]),
);
}
}
Также внесем изменения в метод _getPlacemarkObjects(
) — передадим вызов модального окна в onTap:
onTap: (_, __) => showModalBottomSheet(
context: context,
builder: (context) => _ModalBodyView(
point: point,
),
),
В итоге получаем следующий результат:
Файл map_screen.dart
import 'package:flutter/material.dart';
import 'package:yandex_mapkit/yandex_mapkit.dart';
import 'package:yandex_mapkit_demo/data/map_point.dart';
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
late final YandexMapController _mapController;
@override
void dispose() {
_mapController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Yandex Mapkit Demo')),
body: YandexMap(
onMapCreated: (controller) async {
_mapController = controller;
// приближаем вид карты ближе к Европе
await _mapController.moveCamera(
CameraUpdate.newCameraPosition(
const CameraPosition(
target: Point(
latitude: 50,
longitude: 20,
),
zoom: 3,
),
),
);
},
mapObjects: _getPlacemarkObjects(context),
),
);
}
}
/// Метод для генерации точек на карте
List<MapPoint> _getMapPoints() {
return const [
MapPoint(name: 'Москва', latitude: 55.755864, longitude: 37.617698),
MapPoint(name: 'Лондон', latitude: 51.507351, longitude: -0.127696),
MapPoint(name: 'Рим', latitude: 41.887064, longitude: 12.504809),
MapPoint(name: 'Париж', latitude: 48.856663, longitude: 2.351556),
MapPoint(name: 'Стокгольм', latitude: 59.347360, longitude: 18.341573),
];
}
/// Метод для генерации объектов маркеров для отображения на карте
List<PlacemarkMapObject> _getPlacemarkObjects(BuildContext context) {
return _getMapPoints()
.map(
(point) => PlacemarkMapObject(
mapId: MapObjectId('MapObject $point'),
point: Point(latitude: point.latitude, longitude: point.longitude),
opacity: 1,
icon: PlacemarkIcon.single(
PlacemarkIconStyle(
image: BitmapDescriptor.fromAssetImage(
'assets/icons/map_point.png',
),
scale: 2,
),
),
onTap: (_, __) => showModalBottomSheet(
context: context,
builder: (context) => _ModalBodyView(
point: point,
),
),
),
)
.toList();
}
/// Содержимое модального окна с информацией о точке на карте
class _ModalBodyView extends StatelessWidget {
const _ModalBodyView({required this.point});
final MapPoint point;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 40),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(point.name, style: const TextStyle(fontSize: 20)),
const SizedBox(height: 20),
Text(
'${point.latitude}, ${point.longitude}',
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
]),
);
}
}
Результат в приложении:
В целом на этом можно было бы и остановиться. Но даже на таком простом примере, где точек на карте совсем немного, бросается в глаза основная проблема — если маркеры расположены слишком близко, при масштабировании карты они могут наслаиваться друг на друга.
Это в свою очередь вызывает новую проблему — если необходимо обрабатывать нажатие на объект, при тапе на один маркер будет вызываться обработчик из соседнего.
Эти трудности легко решаются добавлением кластеризации маркеров.
Объединение маркеров в кластеры
Кластеры — это объекты карты, которые позволяют при уменьшении масштаба объединить близлежащие маркеры в один объект и отобразить количество объединенных точек.
Для реализации кластеризации в Yandex Mapkit существует специальный класс —ClusterizedPlacemarkCollection
. Обязательные параметры:
mapId
— уникальный идентификатор объекта;placemarks
— список всех маркеров, которые в случае изменения масштаба объединяются в кластеры;radius
— минимальное расстояние между точками на карте, которые остаются разделенными и не входят в кластеры;minZoom
— минимальный уровень масштабирования, при котором отображаются кластеры.
Если есть необходимость изменить внешний вид кластера, это можно сделать через обратный вызов onClusterAdded()
. Он вызывается каждый раз, когда по изменению масштаба карты создаются новые кластеры или удаляются старые.
Cluster
— сам объект кластера, который хранит в себе данные об объединенных маркерах, их количестве и своем внешнем виде.
Класс Cluster
реализует метод copyWith()
, который позволяет обеспечить иммутабельность текущему объекту кластера. С помощью этого метода можно вернуть новый экземпляр класса, который от текущего будет отличаться лишь теми параметрами, которые будут заданы в copyWith()
.
Чтобы изменить внешний вид в onClusterAdded()
необходимо вернуть экземпляр Cluster
с измененным через copyWith()
параметром appearance.
Параметр appearance, в свою очередь, типизирован как PlacemarkMapObject
, поэтому принцип построения иконки и внешнего вида в целом у кластера такой же, как у обычного маркера (описан выше).
Рассмотрим сложный случай формирования кластера, когда кластер должен динамически отображать количество маркеров, которые в нем объединены. Также для примера не будем использовать готовые изображения — нарисуем иконку самостоятельно с помощью Canvas.
Для этого создадим файл clusterized_icon_painter.dart и в нем реализуем класс ClusterIconPainter
. Этот класс будет отрисовывать круглую иконку желтого цвета с оранжевой обводкой и числом внутри, отображающим количество маркеров в текущем кластере.
/// Класс для отрисовки кластеров на карте
class ClusterIconPainter {
const ClusterIconPainter(this.clusterSize);
/// Количество маркеров в кластере
final int clusterSize;
}
Класс в параметре clusterSize
принимает количество объединенных маркеров в текущем кластере.
Добавим метод _paintCirclePlacemark()
, который будет отрисовывать фон кластера (в нашем случае это закрашенный круг желтого цвета с оранжевой обводкой):
Файл clusterized_icon_painter.dart
/// Метод, который отрисовывает фигуру кластера (фон и обводка)
Canvas _paintCirclePlacemark({
required Size size,
required PictureRecorder recorder,
}) {
final canvas = Canvas(recorder);
final radius = size.height / 2.15;
// внутренний круг - закрашенная часть маркера
final fillPaint = Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;
// внешний круг - обводка маркера
final strokePaint = Paint()
..color = Colors.deepOrangeAccent
..style = PaintingStyle.stroke
..strokeWidth = 8;
final circleOffset = Offset(size.height / 2, size.width / 2);
canvas
..drawCircle(circleOffset, radius, fillPaint)
..drawCircle(circleOffset, radius, strokePaint);
return canvas;
}
Далее добавим метод _paintTextCountPlacemarks()
, который будет отрисовывать на том же объекте Canvas оранжевый текст с количеством маркеров, объединенных в кластер:
Файл clusterized_icon_painter.dart
/// Метод, который отрисовывает текст,
/// отображающий количество маркеров в кластере
void _paintTextCountPlacemarks({
required String text,
required Size size,
required Canvas canvas,
}) {
// внешний вид текста, отображающего количество маркеров в кластере
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: const TextStyle(
color: Colors.deepOrangeAccent,
fontSize: 50,
fontWeight: FontWeight.w800,
),
),
textDirection: TextDirection.ltr,
)..layout(maxWidth: size.width);
// смещение текста
// необходимо для размещения текста по центру кластера
final textOffset = Offset(
(size.width - textPainter.width) / 2,
(size.height - textPainter.height) / 2,
);
textPainter.paint(canvas, textOffset);
}
Создадим публичный метод getClusterIconBytes()
, который будет отрисовывать кластер, вызывая вышеуказанные методы, и преобразовывать рисунок в байтовый формат. Как мы помним, вид иконки объекта можно задать либо через путь к изображению в assets, либо из байтового формата.
Файл clusterized_icon_painter.dart
/// Метод, который формирует фигуру кластера
/// и преобразует ее в байтовый формат
Future<Uint8List> getClusterIconBytes() async {
const size = Size(150, 150);
final recorder = PictureRecorder();
// отрисовка маркера
_paintTextCountPlacemarks(
text: clusterSize.toString(),
size: size,
canvas: _paintCirclePlacemark(
size: size,
recorder: recorder,
),
);
// преобразование в байтовый формат
final image = await recorder.endRecording().toImage(
size.width.toInt(),
size.height.toInt(),
);
final pngBytes = await image.toByteData(format: ImageByteFormat.png);
return pngBytes!.buffer.asUint8List();
}
Далее вернемся в наш основной файл map_screen.dart.
Здесь теперь мы можем создать экземпляр ClusterizedPlacemarkCollection
. Для этого реализуем метод _getClusterizedCollection()
. В нем изменяем внешний вид кластера, возвращая в обратном вызове onClusterAdd кластер
с новой иконкой, отрисованной через созданный нами ранее класс ClusterIconPainter
и метод getClusterIconBytes()
.
Файл map_screen.dart
/// Метод для получения коллекции кластеризованных маркеров
ClusterizedPlacemarkCollection _getClusterizedCollection({
required List<PlacemarkMapObject> placemarks,
}) {
return ClusterizedPlacemarkCollection(
mapId: const MapObjectId('clusterized-1'),
placemarks: placemarks,
radius: 50,
minZoom: 15,
onClusterAdded: (self, cluster) async {
return cluster.copyWith(
appearance: cluster.appearance.copyWith(
opacity: 1.0,
icon: PlacemarkIcon.single(
PlacemarkIconStyle(
image: BitmapDescriptor.fromBytes(
await ClusterIconPainter(cluster.size).getClusterIconBytes(),
),
),
),
),
);
},
}
Добавим дополнительную полезную функцию — приближение карты при тапе на кластер. Для этого нам необходимо создать дополнительную переменную типа double _mapZoom
, в которую мы будем помещать текущее значение уровня приближения карты. Зададим ей начальное значение 0.0.
С помощью обратного вызова onCameraPositionChanged
в виджете YandexMap у нас есть возможность получать сведения о текущем положении карты на экране. Функции в этом поле будут вызываться каждый раз, когда пользователь увеличил или уменьшил масштаб карты, переместил ее и т.д.
Здесь переопределим _mapZoom
:
onCameraPositionChanged: (cameraPosition, _, __) {
setState(() {
_mapZoom = cameraPosition.zoom;
});
},
Таким образом, переменная _mapZoom
всегда будет хранить актуальные данные о текущем масштабе карты.
Далее изменим метод _getClusterizedCollection()
. В обратном вызове onClusterTap
в объекте ClusterizedPlacemarkCollection
выполним центрирование карты относительно первого маркера из списка маркеров в кластере и увеличим текущий масштаб на 1 единицу:
onClusterTap: (self, cluster) async {
await _mapController.moveCamera(
animation: const MapAnimation(
type: MapAnimationType.linear, duration: 0.3),
CameraUpdate.newCameraPosition(
CameraPosition(
target: cluster.placemarks.first.point,
zoom: _mapZoom + 1,
),
),
);
});
Теперь, чтобы отобразить маркеры с функцией кластеризации на карте, необходимо передать созданный объект ClusterizedPlacemarkCollection
в список объектов карты mapObjects
:
mapObjects: [
_getClusterizedCollection(
placemarks: _getPlacemarkObjects(context),
),
],
В итоге получаем следующий результат:
Файл map_screen.dart
import 'package:flutter/material.dart';
import 'package:yandex_mapkit/yandex_mapkit.dart';
import 'package:yandex_mapkit_demo/data/map_point.dart';
import 'package:yandex_mapkit_demo/presentation/clusterized_icon_painter.dart';
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
late final YandexMapController _mapController;
var _mapZoom = 0.0;
@override
void dispose() {
_mapController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Yandex Mapkit Demo')),
body: YandexMap(
onMapCreated: (controller) async {
_mapController = controller;
// приближаем вид карты ближе к Европе
await _mapController.moveCamera(
CameraUpdate.newCameraPosition(
const CameraPosition(
target: Point(
latitude: 50,
longitude: 20,
),
zoom: 3,
),
),
);
},
onCameraPositionChanged: (cameraPosition, _, __) {
setState(() {
_mapZoom = cameraPosition.zoom;
});
},
mapObjects: [
_getClusterizedCollection(
placemarks: _getPlacemarkObjects(context),
),
],
),
);
}
/// Метод для получения коллекции кластеризованных маркеров
ClusterizedPlacemarkCollection _getClusterizedCollection({
required List<PlacemarkMapObject> placemarks,
}) {
return ClusterizedPlacemarkCollection(
mapId: const MapObjectId('clusterized-1'),
placemarks: placemarks,
radius: 50,
minZoom: 15,
onClusterAdded: (self, cluster) async {
return cluster.copyWith(
appearance: cluster.appearance.copyWith(
opacity: 1.0,
icon: PlacemarkIcon.single(
PlacemarkIconStyle(
image: BitmapDescriptor.fromBytes(
await ClusterIconPainter(cluster.size)
.getClusterIconBytes(),
),
),
),
),
);
},
onClusterTap: (self, cluster) async {
await _mapController.moveCamera(
animation: const MapAnimation(
type: MapAnimationType.linear, duration: 0.3),
CameraUpdate.newCameraPosition(
CameraPosition(
target: cluster.placemarks.first.point,
zoom: _mapZoom + 1,
),
),
);
});
}
}
/// Метод для генерации точек на карте
List<MapPoint> _getMapPoints() {
return const [
MapPoint(name: 'Москва', latitude: 55.755864, longitude: 37.617698),
MapPoint(name: 'Лондон', latitude: 51.507351, longitude: -0.127696),
MapPoint(name: 'Рим', latitude: 41.887064, longitude: 12.504809),
MapPoint(name: 'Париж', latitude: 48.856663, longitude: 2.351556),
MapPoint(name: 'Стокгольм', latitude: 59.347360, longitude: 18.341573),
];
}
/// Метод для генерации объектов маркеров для отображения на карте
List<PlacemarkMapObject> _getPlacemarkObjects(BuildContext context) {
return _getMapPoints()
.map(
(point) => PlacemarkMapObject(
mapId: MapObjectId('MapObject $point'),
point: Point(latitude: point.latitude, longitude: point.longitude),
opacity: 1,
icon: PlacemarkIcon.single(
PlacemarkIconStyle(
image: BitmapDescriptor.fromAssetImage(
'assets/icons/map_point.png',
),
scale: 2,
),
),
onTap: (_, __) => showModalBottomSheet(
context: context,
builder: (context) => _ModalBodyView(
point: point,
),
),
),
)
.toList();
}
/// Содержимое модального окна с информацией о точке на карте
class _ModalBodyView extends StatelessWidget {
const _ModalBodyView({required this.point});
final MapPoint point;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 40),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(point.name, style: const TextStyle(fontSize: 20)),
const SizedBox(height: 20),
Text(
'${point.latitude}, ${point.longitude}',
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
);
}
}
Файл clusterized_icon_painter.dart
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart';
/// Класс для отрисовки кластеров на карте
class ClusterIconPainter {
const ClusterIconPainter(this.clusterSize);
/// Количество маркеров в кластере
final int clusterSize;
/// Метод, который формирует фигуру кластера
/// и преобразует ее в байтовый формат
Future<Uint8List> getClusterIconBytes() async {
const size = Size(150, 150);
final recorder = PictureRecorder();
// отрисовка маркера
_paintTextCountPlacemarks(
text: clusterSize.toString(),
size: size,
canvas: _paintCirclePlacemark(
size: size,
recorder: recorder,
),
);
// преобразование в байтовый формат
final image = await recorder.endRecording().toImage(
size.width.toInt(),
size.height.toInt(),
);
final pngBytes = await image.toByteData(format: ImageByteFormat.png);
return pngBytes!.buffer.asUint8List();
}
}
/// Метод, который отрисовывает фигуру кластера (фон и обводка)
Canvas _paintCirclePlacemark({
required Size size,
required PictureRecorder recorder,
}) {
final canvas = Canvas(recorder);
final radius = size.height / 2.15;
// внутренний круг - закрашенная часть маркера
final fillPaint = Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;
// внешний круг - обводка маркера
final strokePaint = Paint()
..color = Colors.deepOrangeAccent
..style = PaintingStyle.stroke
..strokeWidth = 8;
final circleOffset = Offset(size.height / 2, size.width / 2);
canvas
..drawCircle(circleOffset, radius, fillPaint)
..drawCircle(circleOffset, radius, strokePaint);
return canvas;
}
/// Метод, который отрисовывает текст,
/// отображающий количество маркеров в кластере
void _paintTextCountPlacemarks({
required String text,
required Size size,
required Canvas canvas,
}) {
// внешний вид текста, отображающего количество маркеров в кластере
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: const TextStyle(
color: Colors.deepOrangeAccent,
fontSize: 50,
fontWeight: FontWeight.w800,
),
),
textDirection: TextDirection.ltr,
)..layout(maxWidth: size.width);
// смещение текста
// необходимо для размещения текста по центру кластера
final textOffset = Offset(
(size.width - textPainter.width) / 2,
(size.height - textPainter.height) / 2,
);
textPainter.paint(canvas, textOffset);
}
Результат в приложении:
Готово! Мы реализовали простое приложение, которое способно отображать список точек на карте с динамической кластеризацией и с обработкой нажатия как на сам маркер, так и на кластер.
В следующей части статьи мы разберем, как легко отобразить точку пользователя на карте без применения сторонних библиотек геопозиционирования, а также научимся выделять зоны по координатам на карте и строить маршруты от точки А к точке Б.
Если у вас остались вопросы, с удовольствием на них отвечу в комментариях: код проекта.
Почитать об интеграции Яндекс Карт во Flutter можно в статье у моего коллеги — Как интегрировать Яндекс Карты в приложение на Flutter.
Также много интересной информации по теме в Telegram-чате разработчиков по yandex_mapkit.
P.S. Мы ведем дружелюбный канал про Flutter в Telegram. Присоединяйтесь!