В этой статья мы рассмотри пример реализации кастомных пинов для карты xamarin. Пины будут обладать тем видом, который вам нужен. Так же мы рассмотрим часть кода xamarin.maps, отвечающий за создание, отрисовку и отображение пинов.
Благо исходники xamarin.forms представлены на github, и мы можем увидеть весь код:
xamarin/Xamarin.Forms: Xamarin.Forms Official Home (github.com)
Рассматривать мы будем эти файлы с гита:
-
iOs.MapRenderer Xamarin.Forms/MapRenderer.cs at 5.0.0 · xamarin/Xamarin.Forms (github.com)
-
Android.MapRenderer Xamarin.Forms/MapRenderer.cs at 5.0.0 · xamarin/Xamarin.Forms (github.com)
Для воспроизведения туториала у вас должны быть установлены пакеты Xamarin.Maps во все проекты, и соответственно настроен по гайду от xamarin:

Так же должен быть установлен пакет SkiaSharp и SkiaSharp.Views.Forms в основной проект (не специфичный для платформ):


Для начала реализуем тип, который станет базой для пинов, и карту, которая будет наследоваться от нативной, и обрабатывать наш пин, в основном проекте [ProjectName]:
Код CustomPin — база нашего пина
public abstract class CustomPin : Pin { public class MapMarkerInvalidateEventArgs { public double Width { get; } public double Height { get; } internal MapMarkerInvalidateEventArgs(CustomPin marker) { Width = marker.Width; Height = marker.Height; } } public event EventHandler<MapMarkerInvalidateEventArgs> RequestInvalidate; // Bindable properties public static readonly BindableProperty WidthProperty = BindableProperty.Create(nameof(Width), typeof(double), typeof(CustomPin), 32.0, propertyChanged: OnDrawablePropertyChanged); public static readonly BindableProperty HeightProperty = BindableProperty.Create(nameof(Height), typeof(double), typeof(CustomPin), 32.0, propertyChanged: OnDrawablePropertyChanged); public static readonly BindableProperty AnchorXProperty = BindableProperty.Create(nameof(AnchorX), typeof(double), typeof(CustomPin), 0.5); public static readonly BindableProperty AnchorYProperty = BindableProperty.Create(nameof(AnchorY), typeof(double), typeof(CustomPin), 0.5); public static readonly BindableProperty IsVisibleProperty = BindableProperty.Create(nameof(IsVisible), typeof(bool), typeof(CustomPin), true); public static readonly BindableProperty ClickableProperty = BindableProperty.Create(nameof(Clickable), typeof(bool), typeof(CustomPin), true); // Ширина пина public double Width { get { return (double)GetValue(WidthProperty); } set { SetValue(WidthProperty, value); } } // Высота пина public double Height { get { return (double)GetValue(HeightProperty); } set { SetValue(HeightProperty, value); } } // Расположение пина относительно точки на карте по X public double AnchorX { get { return (double)GetValue(AnchorXProperty); } set { SetValue(AnchorXProperty, value); } } // Расположение пина относительно точки на карте по Y public double AnchorY { get { return (double)GetValue(AnchorYProperty); } set { SetValue(AnchorYProperty, value); } } // Виден ли пин public bool IsVisible { get { return (bool)GetValue(IsVisibleProperty); } set { SetValue(IsVisibleProperty, value); } } // Интерактивен ли пин public bool Clickable { get { return (bool)GetValue(ClickableProperty); } set { SetValue(ClickableProperty, value); } } private static void OnDrawablePropertyChanged(BindableObject bindable, object oldValue, object newValue) { CustomPin marker = bindable as CustomPin; marker.Invalidate(); } public void Invalidate() { RequestInvalidate?.Invoke(this, new MapMarkerInvalidateEventArgs(this)); } // Метод, который будет перезаписан в дочернем классе, в нем будет происходить отрисовка пина public abstract void DrawPin(SKSurface surface); }
Код CustomMap — наша карта с поддержкой CustomPin
public class CustomMap : Map { }
Простой код, большего нам и не нужно.
И так, для начала рассмотрим рендер под Android, а именно — нас интересует метод CreateMarker на 248 строке, вот так он выглядит в оригинале:
protected virtual MarkerOptions CreateMarker(Pin pin) { var opts = new MarkerOptions(); opts.SetPosition(new LatLng(pin.Position.Latitude, pin.Position.Longitude)); opts.SetTitle(pin.Label); opts.SetSnippet(pin.Address); return opts; }
Этот метод отвечает за создание пина, важным элементом здесь, как нетрудно догадаться, является «MarkerOptions opts», тип MarkerOptions содержит метод SetIcon(BitmapDescriptor), которое мы и используем для отрисовывания пина. Для этого необходимо создать дочерний класс к этому отрисовщику в проекте [ProjectName].Android, рекомендую создать в нем папку Renderers:

Код заготовки класса отрисовщика будет выглядеть следующим образом:
[assembly: Xamarin.Forms.ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))] namespace [ProjectName].Droid.Renderers { public class CustomMapRenderer : MapRenderer { public CustomMapRenderer(Context context) : base(context){} protected override MarkerOptions CreateMarker(Pin pin) { return base.CreateMarker(pin); } } }
Пока что он никак не меняет логику поведения класса родителя. Давайте добавим обработку нашего CustomPin, результирующий код будет выглядеть следующим образом:
[assembly: Xamarin.Forms.ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))] namespace [ProjectName].Droid.Renderers { public class CustomMapRenderer : MapRenderer { public CustomMapRenderer(Context context) : base(context){} protected override MarkerOptions CreateMarker(Pin pin) { // Получаем настроенный в базовом классе MarkerOptions var opts = base.CreateMarker(pin); // Если наш маркер - кастомный... if(pin is CustomPin cpin) { // ... то получаем SKPixmap с изображением пина SKPixmap markerBitmap = DrawMarker(cpin); // Задаем изображение пина и видимость opts.SetIcon(BitmapDescriptorFactory.FromBitmap(markerBitmap.ToBitmap())) .Visible(cpin.IsVisible); // Выставляем якоря opts.Anchor((float)cpin.AnchorX, (float)cpin.AnchorY); } return opts; } private SKPixmap DrawMarker(CustomPin skPin) { // Считаем размер изображения пина согласно Density устройства double bitmapWidth = skPin.Width * Context.Resources.DisplayMetrics.Density; double bitmapHeight = skPin.Height * Context.Resources.DisplayMetrics.Density; // Создаем сурфейс для отрисовки SKSurface surface = SKSurface.Create(new SKImageInfo((int)bitmapWidth, (int)bitmapHeight, SKColorType.Rgba8888, SKAlphaType.Premul)); // Заливаем сурфейс цветом Transparent surface.Canvas.Clear(SKColor.Empty); // Просим пин отрисовать изображение на сурфейс skPin.DrawPin(surface); // Получаем пиксели, которые можно перевести в BitMap return surface.PeekPixels(); } } }
Отлично, для Android мы все сделали, теперь попробуем проверить наше решение при помощи такого кастомного тестового пина (Создавать его нужно в основном проекте [ProjectName]):
internal sealed class CirclePin : CustomPin { // Сохраненный Bitmap SKBitmap pinBitmap; // Конструктор принимает string - это текст внутри круга public CirclePin(string text) { // Отступ текста от краев круга int circleOffset = 10; // Минимальный размер круга, при маленьком тексте int minSize = 40; // Размер шрифта текста int textSize = 18; // Задание цвета текста Color tempColor = Color.White; // Перевод из Color в SKColor SKColor textColor = new SKColor((byte)(tempColor.R * 255), (byte)(tempColor.G * 255), (byte)(tempColor.B * 255)); // Задание цвета круга tempColor = Color.Black; // Перевод из Color в SKColor SKColor circleColor = new SKColor((byte)(tempColor.R * 255), (byte)(tempColor.G * 255), (byte)(tempColor.B * 255)); PrepareBitmap(circleOffset, circleColor, text, textSize, textColor, minSize); } private void PrepareBitmap(int circleOffset, SKColor circleColor, string text, float textSize, SKColor textColor, int minSize, int iconSize = 28) { int width; float den = (float)DeviceDisplay.MainDisplayInfo.Density; // Удваиваем отступ, т.к. он будет с 2-х сторон одинаковый circleOffset *= 2; using (var font = SKTypeface.FromFamilyName("Arial")) using (var textBrush = new SKPaint { Typeface = font, TextSize = textSize * den, IsAntialias = true, Color = textColor, TextAlign = SKTextAlign.Center, }) { // Высчитывание размера текста SKRect textRect = new SKRect(); textBrush.MeasureText(text, ref textRect); // Ширина текста в dip width = Math.Max((int)(Math.Ceiling(textRect.Width) / den) + circleOffset, minSize); // Задаем размер пина согласно ширине в dip Width = Height = width; // Ширина текста в пикселях width = (int)Math.Floor(width * den); // Создаем Bitmap для отрисовки pinBitmap = new SKBitmap(width, width, SKColorType.Rgba8888, SKAlphaType.Premul); using (var canvas = new SKCanvas(pinBitmap)) { using (var circleBrush = new SKPaint { IsAntialias = true, Color = circleColor }) { //Отрисовка круга canvas.DrawRoundRect(new SKRoundRect(new SKRect(0, width, width, 0), width / 2f), circleBrush); //Отрисовка текста canvas.DrawText(text, width * 0.5f, width * 0.5f - textRect.MidY, textBrush); canvas.Flush(); } } } } public override void DrawPin(SKSurface surface) { // Получаем канвас из сурфейса, для отрисовки SKCanvas canvas = surface.Canvas; // Отрисовываем на канвас наш сохраненный Bitmap canvas.DrawBitmap(pinBitmap, canvas.LocalClipBounds.MidX - pinBitmap.Width / 2f, canvas.LocalClipBounds.MidY - pinBitmap.Height / 2f); } }
Для проверки я немного изменил код стандартного MainPage.xaml, добавив в него карту, и код MainPage.xaml.cs для добавления проверочного набора пинов:
Код MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:xcmpexample="clr-namespace:XCMPExample" x:Class="[ProjectName].MainPage"> <StackLayout> <Frame BackgroundColor="#2196F3" Padding="24" CornerRadius="0"> <Label Text="Welcome to Xamarin.Forms!" HorizontalTextAlignment="Center" TextColor="White" FontSize="36"/> </Frame> <!-- Карта --> <xcmpexample:CustomMap x:Name="customMap"/> </StackLayout> </ContentPage>
Код MainPage.xaml.cs
public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); Random random = new Random(); for (int i = 0; i < 100; i++) { string universalFillData = i.ToString(); customMap.Pins.Add(new CirclePin(universalFillData) { Label = universalFillData, Address = universalFillData, Position = new Position( /// Устанавливаем координаты Москвы +-1 /// чтобы было проще найти наши пины random.NextDouble() + 55, random.NextDouble() + 37) }); } } }
Получившийся результат:
Стандартная карта, с той же логикой в xaml.cs, но с Map вместо CustomMap в xaml.

Наша CustomMap

Теперь реализуем это же для iOs, для этого рассмотрим нативный для xamarin отрисовщик на iOs, нас интересует метод CreateAnnotation на 214 строке и метод GetViewForAnnotation на 224 строке. В оригинале эти методы выглядят так:
CreateAnnotation
protected virtual IMKAnnotation CreateAnnotation(Pin pin) { return new MKPointAnnotation { Title = pin.Label, Subtitle = pin.Address ?? "", Coordinate = new CLLocationCoordinate2D(pin.Position.Latitude, pin.Position.Longitude) }; }
GetViewForAnnotation
protected virtual MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation) { MKAnnotationView mapPin = null; // https://bugzilla.xamarin.com/show_bug.cgi?id=26416 var userLocationAnnotation = Runtime.GetNSObject(annotation.Handle) as MKUserLocation; if (userLocationAnnotation != null) return null; const string defaultPinId = "defaultPin"; mapPin = mapView.DequeueReusableAnnotation(defaultPinId); if (mapPin == null) { mapPin = new MKPinAnnotationView(annotation, defaultPinId); mapPin.CanShowCallout = true; } mapPin.Annotation = annotation; AttachGestureToPin(mapPin, annotation); return mapPin; }
Логика работы интерфейса на iOs довольно заметно отличается от ее реализации на Android, в частности на iOs используются Annotation и AnnotationView, в кратце это информация и изображение пина соответственно (хотя такой ответ не точный, но информацию об этом можно найти в открытых источниках). В общем, нам нужны собственные Annotation и AnnotationView, создавать мы их будем в [ProjectName].iOs проекте:
CustomPinAnnotation
public class CustomPinAnnotation : MKPointAnnotation { // Сохраняем ссылку на пин, понадобится в будущем public CustomPin SharedPin { get; } public CustomPinAnnotation(CustomPin pin) { SharedPin = pin; Title = pin.Label; Subtitle = pin.Address; // Переводим координаты в CL для iOs Coordinate = ToLocationCoordinate(pin.Position); } public override string Title { get => base.Title; set { if (Title != value) { string titleKey = nameof(Title).ToLower(); WillChangeValue(titleKey); base.Title = value; DidChangeValue(titleKey); } } } public override string Subtitle { get => base.Subtitle; set { if (Subtitle != value) { string subtitleKey = nameof(Subtitle).ToLower(); WillChangeValue(subtitleKey); base.Subtitle = value; DidChangeValue(subtitleKey); } } } public override CLLocationCoordinate2D Coordinate { get => base.Coordinate; set { if (Coordinate.Latitude != value.Latitude || Coordinate.Longitude != value.Longitude) { string coordinateKey = nameof(Coordinate).ToLower(); WillChangeValue(coordinateKey); base.Coordinate = value; DidChangeValue(coordinateKey); } } } private CLLocationCoordinate2D ToLocationCoordinate(Position self) { return new CLLocationCoordinate2D(self.Latitude, self.Longitude); } }
CustomPinAnnotationView
public class CustomPinAnnotationView : MKAnnotationView { // Сохраняем название View public const string ViewIdentifier = nameof(CustomPinAnnotationView); // Сохраняем ссылку на кастомную аннотацию private CustomPinAnnotation _SkiaAnnotation => base.Annotation as CustomPinAnnotation; // Токен остановки обновления изображения private CancellationTokenSource _imageUpdateCts; // Density экрана для высчитывания размера в пикселях private nfloat _screenDensity; public CustomPinAnnotationView(CustomPinAnnotation annotation) : base(annotation, ViewIdentifier) { _screenDensity = UIScreen.MainScreen.Scale; } internal async void UpdateImage() { CustomPin pin = _SkiaAnnotation?.SharedPin; UIImage image; CancellationTokenSource renderCts = new CancellationTokenSource(); _imageUpdateCts?.Cancel(); _imageUpdateCts = renderCts; try { // Рисуем пин асинхронно image = await RenderPinAsync(pin, renderCts.Token).ConfigureAwait(false); renderCts.Token.ThrowIfCancellationRequested(); Device.BeginInvokeOnMainThread(() => { if (!renderCts.IsCancellationRequested) { // Задаем полученное изображение синхронно в потоке UI Image = image; Bounds = new CGRect(CGPoint.Empty, new CGSize(pin.Width, pin.Height)); } }); } catch (OperationCanceledException) { // Ignore } catch (Exception e) { System.Diagnostics.Debug.WriteLine("Failed to render pin annotation: \n" + e); } } private Task<UIImage> RenderPinAsync(CustomPin pin, CancellationToken token = default(CancellationToken)) { return Task.Run(() => { // Высчитываем размеры по аналогии с Android отрисовщиком double bitmapWidth = pin.Width * _screenDensity; double bitmapHeight = pin.Height * _screenDensity; // Отрисовываем пин по аналогии с Android отрисовщиком using (SKSurface surface = SKSurface.Create(new SKImageInfo((int)bitmapWidth, (int)bitmapHeight, SKColorType.Rgba8888, SKAlphaType.Premul))) { surface.Canvas.Clear(SKColor.Empty); pin.DrawPin(surface); return surface.PeekPixels().ToUIImage(); } }, token); } public void UpdateAnchor() { CenterOffset = new CGPoint(Bounds.Width * (0.5 - _SkiaAnnotation.SharedPin.AnchorX), Bounds.Height * (0.5 - _SkiaAnnotation.SharedPin.AnchorY)); } }
Как и на примере отрисовщика, который мы реализовали под Android, необходимо создать такой же в [ProjectName].iOs проекте, заготовка будет выглядеть следующим образом:
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))] namespace [ProjectName].iOS.Renderers { public class CustomMapRenderer : MapRenderer { protected override IMKAnnotation CreateAnnotation(Pin pin) { return base.CreateAnnotation(pin); } protected override MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation) { return base.GetViewForAnnotation(mapView, annotation); } } }
Теперь добавим сюда обработку для наших кастомных пинов, с поддержкой кастомных аннотаций, результируйющий отрисовщик будет следующим:
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))] namespace [ProjectName].iOS.Renderers { public class CustomMapRenderer : MapRenderer { protected override IMKAnnotation CreateAnnotation(Pin pin) { if (pin is CustomPin skPin) { //Если мы обрабатываем наш кастомный пин, то создаем ему специальную аннотацию. IMKAnnotation result = new CustomPinAnnotation(skPin); skPin.MarkerId = result; return result; } else return base.CreateAnnotation(pin); } protected override MKAnnotationView GetViewForAnnotation(MKMapView mapView, IMKAnnotation annotation) { if (annotation is CustomPinAnnotation skiaAnnotation) { // Если мы обрабатываем нашу кастомную аннотацию, то получаем из нее наш пин CustomPin skPin = skiaAnnotation.SharedPin; // Проверяем на кэшированные аннотации, по совету Xamarin CustomPinAnnotationView pinView = mapView.DequeueReusableAnnotation(CustomPinAnnotationView.ViewIdentifier) as CustomPinAnnotationView ?? new CustomPinAnnotationView(skiaAnnotation); // Добавляем жесты к пину base.AttachGestureToPin(pinView, annotation); pinView.Annotation = skiaAnnotation; // Отрисовываем пин pinView.UpdateImage(); // Обновляем якорь pinView.UpdateAnchor(); pinView.Hidden = !skPin.IsVisible; pinView.Enabled = skPin.Clickable; return pinView; } else return base.GetViewForAnnotation(mapView, annotation); } } }
И на этом в принципе все, мы добавили поддержку кастомных пинов на iOs, теперь мы можем делать абсолютно любые изображения пина при помощи SkiaSharp путем простого наследования от CustomPin, и передавать их в CustomMap.Pins. Буду рад конструктивной критике и отзыву в комментариях!
Проект, который я создал по мере написания статьи можно найти на github:
AlexMorOR/Xamarin-CustomMap-with-CustomPins: Here’s a solution to extend the native xamarin map to include custom image pins. (github.com)
P.S.
Статья составлялась на основе кода из проекта, над которым я работаю, поэтому могут быть упущены некоторые свойства, которые нигде не используются. Но если я допустил такое упущение — на работу карты это не повлияет.
ссылка на оригинал статьи https://habr.com/ru/post/682134/
Добавить комментарий