إضافة العلامات وتخصيصها
إضافة العلامات وتخصيصها
تُعدّ العلامة (Marker) الطريقةَ الرئيسية لتمييز نقطة جغرافية محددة على خريطة Google في Flutter. تُوفّر حزمة google_maps_flutter الصنفَ Marker، الذي يتيح لك وضع دبابيس عند أي خط عرض/طول، وإعطاءها أيقونات مخصصة، وإرفاق عناوين ومقتطفات مقروءة، والاستجابة لنقرات المستخدم عبر دوال الاستدعاء (callbacks). في التطبيقات الحقيقية ستدير دائمًا مجموعةً ديناميكية من العلامات مخزّنة في حالة الودجت، حتى تتمكن من إضافتها أو حذفها أو تحديثها في وقت التشغيل.
الصنف Marker — الخصائص الأساسية
يتطلب كل Marker معرِّفًا فريدًا MarkerId وموضعًا position. جميع الخصائص الأخرى اختيارية:
- markerId — قيمة
MarkerId(String value)تُعرِّف العلامة بشكل فريد داخل الخريطة. - position — قيمة
LatLng(lat, lng)تحدد موضع الدبوس. - infoWindow — نافذة
InfoWindowتحتوي علىtitleوخاصيةsnippetاختيارية تظهر عند نقر العلامة. - icon — واصف
BitmapDescriptor؛ يكون افتراضيًا الدبوسَ الأحمر من Google. استبدله بأصل مخصص أو نسخة مُعاد تلوينها باستخدام مُنشئات المصنع. - onTap — دالة استدعاء
VoidCallbackتُشغَّل عند نقر المستخدم للعلامة. - draggable — عند تعيينه
true، يستطيع المستخدم الضغط المطوّل وسحب العلامة؛ اجمعه معonDragEndلالتقاط الموضع الجديد. - visible — تبديل رؤية العلامة دون إزالتها من المجموعة.
- alpha — الشفافية بين 0.0 (شفاف) و1.0 (معتم).
- zIndex — ترتيب الرسم عند تداخل العلامات.
تخزين العلامات في حالة الودجت
يقبل الودجت GoogleMap قيمةً Set<Marker> عبر معامل markers. نظرًا لاستخدام Set للهوية في المساواة، يجب استبدال المجموعة بأكملها (أو استخدام setState) لإطلاق إعادة البناء عند إضافة علامة أو حذفها. النمط الاصطلاحي هو الاحتفاظ بحقل Set<Marker> في صنف State الخاص بك.
مثال 1 — StatefulWidget أساسي مع مجموعة علامات ديناميكية
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class MarkerDemoMap extends StatefulWidget {
const MarkerDemoMap({super.key});
@override
State<MarkerDemoMap> createState() => _MarkerDemoMapState();
}
class _MarkerDemoMapState extends State<MarkerDemoMap> {
// جميع العلامات النشطة موجودة هنا؛ تقرأها GoogleMap في كل بناء.
final Set<Marker> _markers = {};
int _markerCounter = 0;
static const CameraPosition _initialCamera = CameraPosition(
target: LatLng(25.2048, 55.2708), // دبي
zoom: 12,
);
/// يضيف علامة جديدة في [position] بمعرّف مُولَّد تلقائيًا.
void _addMarker(LatLng position) {
_markerCounter++;
final id = MarkerId('marker_$_markerCounter');
final marker = Marker(
markerId: id,
position: position,
infoWindow: InfoWindow(
title: 'موقع #$_markerCounter',
snippet: '${position.latitude.toStringAsFixed(4)}, '
'${position.longitude.toStringAsFixed(4)}',
),
onTap: () => _onMarkerTapped(id),
);
setState(() {
_markers.add(marker);
});
}
void _onMarkerTapped(MarkerId id) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('تم النقر على العلامة: ${id.value}')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('عرض العلامات')),
body: GoogleMap(
initialCameraPosition: _initialCamera,
markers: _markers,
onLongPress: _addMarker, // ضغط مطوّل في أي مكان لإسقاط دبوس
),
);
}
}
Set<Marker> العلاماتِ بمعرّفاتها MarkerId. إذا استدعيت _markers.add() بعلامة معرّفها موجود مسبقًا في المجموعة، فلن تستبدل القديمة — ستتجاهل المجموعة التكرار بصمت. لتحديث علامة موجودة، احذف القديمة أولًا ثم أضف النسخة المُحدَّثة، أو أعِد بناء المجموعة بأكملها.أيقونات علامات مخصصة باستخدام BitmapDescriptor
توفّر BitmapDescriptor في Flutter عدة مُنشئات مصنع للأيقونات المخصصة:
BitmapDescriptor.defaultMarker— الدبوس الأحمر الافتراضي.BitmapDescriptor.defaultMarkerWithHue(hue)— إعادة تلوين الدبوس القياسي؛ قيمة الصبغة (hue) عدد عشري من 0 إلى 360 (مثلًاBitmapDescriptor.hueBlue).BitmapDescriptor.fromAssetImage(config, assetPath)— تحميل ملف PNG من حزمة الأصول بنسبة بكسل الجهاز الصحيحة (غير متزامن).BitmapDescriptor.fromBytes(bytes)— توفير بايتات PNG خام؛ مفيد عند توليد الأيقونات في وقت التشغيل.
مثال 2 — أيقونات مخصصة ونافذة InfoWindow مع onTap
class CustomIconMap extends StatefulWidget {
const CustomIconMap({super.key});
@override
State<CustomIconMap> createState() => _CustomIconMapState();
}
class _CustomIconMapState extends State<CustomIconMap> {
final Set<Marker> _markers = {};
BitmapDescriptor? _cafeIcon;
BitmapDescriptor? _hotelIcon;
@override
void initState() {
super.initState();
_loadIcons();
}
Future<void> _loadIcons() async {
// تحميل أصول PNG مخصصة (يجب إعلانها في pubspec.yaml)
final config = createLocalImageConfiguration(context);
final cafe = await BitmapDescriptor.fromAssetImage(
config,
'assets/icons/cafe_pin.png',
);
final hotel = await BitmapDescriptor.fromAssetImage(
config,
'assets/icons/hotel_pin.png',
);
setState(() {
_cafeIcon = cafe;
_hotelIcon = hotel;
_buildInitialMarkers();
});
}
void _buildInitialMarkers() {
final List<Map<String, dynamic>> places = [
{
'id': 'cafe_1',
'name': 'مقهى الشروق',
'snippet': 'مفتوح 07:00 – 22:00',
'lat': 25.2048,
'lng': 55.2708,
'type': 'cafe',
},
{
'id': 'hotel_1',
'name': 'فندق جراند تاور',
'snippet': '5 نجوم · واي فاي مجاني',
'lat': 25.1972,
'lng': 55.2796,
'type': 'hotel',
},
];
for (final p in places) {
_markers.add(Marker(
markerId: MarkerId(p['id'] as String),
position: LatLng(p['lat'] as double, p['lng'] as double),
icon: p['type'] == 'cafe'
? (_cafeIcon ?? BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueOrange))
: (_hotelIcon ?? BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueBlue)),
infoWindow: InfoWindow(
title: p['name'] as String,
snippet: p['snippet'] as String,
onTap: () {
// نقر نافذة المعلومات — مثلًا فتح ورقة تفاصيل
debugPrint('تم نقر InfoWindow للعنصر ${p['id']}');
},
),
onTap: () {
// نقر دبوس العلامة (يُطلَق قبل فتح نافذة المعلومات)
debugPrint('تم نقر دبوس العلامة: ${p['id']}');
},
));
}
}
@override
Widget build(BuildContext context) {
return GoogleMap(
initialCameraPosition: const CameraPosition(
target: LatLng(25.2048, 55.2708),
zoom: 13,
),
markers: _markers,
);
}
}
initState (أو قبيل عرض الخريطة) حتى تكون الأيقونة الصحيحة متاحة عند أول استدعاء لـ build. إذا حمّلتها بشكل غير متزامن بعد ظهور الخريطة، فتأكد من استدعاء setState بعد await حتى تُطبَّق الأيقونات الجديدة.حذف العلامات واستبدالها
لحذف علامة بواسطة MarkerId، استخدم removeWhere على المجموعة داخل setState:
_markers.removeWhere((m) => m.markerId == targetId);- لاستبدال علامة، استدعِ
removeWhereأولًا ثمaddالنسخة المُحدَّثة. - لمسح جميع العلامات:
_markers.clear();
setState. التعديل المباشر دون setState يُغيّر البيانات في الذاكرة لكنه لا يُطلق إعادة البناء، فتعرض الخريطة دبابيس قديمة حتى إعادة البناء التالية غير المرتبطة. احرص دائمًا على تغليف تغييرات مجموعة العلامات في setState(() { ... });.أفضل الممارسات لإدارة العلامات في بيئة الإنتاج
- اجعل معرّفات العلامات ثابتة وذات معنى (مثلًا استخدم معرّف صف قاعدة البيانات كلاحقة) حتى تتمكن من تحديدها وتحديثها بموثوقية.
- لمجموعات البيانات الكبيرة (>500 علامة)، فكّر في التجميع (clustering) باستخدام حزمة
google_maps_cluster_managerلتجنب مشاكل الأداء. - قم بتحميل جميع نُسخ
BitmapDescriptorمرة واحدة (مثلًا في خدمة أوinitState) بدلًا من إنشائها في كل استدعاء لـsetState. - استخدم الزوج
draggable: true+onDragEndعندما تحتاج دبابيس قابلة للتحريك من قِبَل المستخدم.
ملخص
في هذا الدرس تعلّمت كيفية إنشاء كائنات Marker بعناوين ومقتطفات وأيقونات مخصصة ودوال استدعاء عند النقر، وكيفية تخزينها في Set<Marker> محفوظة في حالة الودجت. إن استدعاء setState عند كل إضافة أو حذف أو استبدال للعلامة يضمن إعادة رسم الودجت GoogleMap بأحدث الدبابيس. بامتلاك هذه الأساسيات يمكنك بناء تجارب خريطة غنية وتفاعلية — من طبقات نقاط الاهتمام البسيطة إلى طبقات الدبابيس الديناميكية المُعتمِدة على البيانات.