الخرائط والموقع وميزات الجهاز

إضافة العلامات وتخصيصها

16 دقيقة الدرس 3 من 12

إضافة العلامات وتخصيصها

تُعدّ العلامة (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 بأحدث الدبابيس. بامتلاك هذه الأساسيات يمكنك بناء تجارب خريطة غنية وتفاعلية — من طبقات نقاط الاهتمام البسيطة إلى طبقات الدبابيس الديناميكية المُعتمِدة على البيانات.