اللمس والإيماءات في Flutter: GestureDetector و InkWell
الأزرار تعرف كيف تستجيب للضغط، لكن ماذا عن بطاقة منتج؟ أو صورة تريد أن يعجب بها المستخدم بضغطتين كما في تطبيقات التواصل؟ أو مربع يسحبه بإصبعه على الشاشة؟ في Flutter تستطيع أن تجعل أي Widget يستجيب للمس. في هذا الدرس ستتعلم أداتين: GestureDetector و InkWell.
GestureDetector: كاشف الإيماءات
Section titled “GestureDetector: كاشف الإيماءات”GestureDetector Widget غير مرئي. تلف به أي عنصر، فيراقب ما يفعله الإصبع فوقه ويخبرك.
تخيّله مثل حارس يقف عند باب: لا يغيّر شكل الباب، لكنه يبلغك بكل من طرقه، وكيف طرقه.
GestureDetector( onTap: () { debugPrint('ضغطة واحدة'); }, onDoubleTap: () { debugPrint('ضغطتان متتاليتان'); }, onLongPress: () { debugPrint('ضغطة مطوّلة'); }, child: Container( width: 160, height: 100, color: Colors.indigo.shade100, alignment: Alignment.center, child: const Text('المسني'), ),)| الإيماءة | متى تحدث | استخدام شائع |
|---|---|---|
onTap |
ضغطة سريعة | فتح عنصر |
onDoubleTap |
ضغطتان متتاليتان | الإعجاب بصورة، أو تكبيرها |
onLongPress |
ضغطة مطوّلة | إظهار قائمة خيارات، أو تحديد عنصر |
onPanUpdate |
تحريك الإصبع في أي اتجاه | سحب عنصر |
onHorizontalDragEnd |
نهاية سحب أفقي | التنقل بين الصور |
InkWell: ضغط مع تأثير الموجة
Section titled “InkWell: ضغط مع تأثير الموجة”GestureDetector صامت: لا يعطي المستخدم أي إشارة مرئية أنه ضغط. أما InkWell فيرسم موجة تنتشر من مكان الإصبع، وهو التأثير الذي تراه في أزرار Material.
Card( clipBehavior: Clip.antiAlias, child: InkWell( onTap: () { debugPrint('فُتح المنتج'); }, child: const Padding( padding: EdgeInsets.all(24), child: Text('بطاقة قابلة للضغط'), ), ),)InkWellيدعمonTapوonDoubleTapوonLongPressمثلGestureDetector.clipBehavior: Clip.antiAliasفيCardتقص الموجة على شكل البطاقة بزواياها الدائرية. بدونها تخرج الموجة من الزوايا بشكل مربع.
أيهما أستخدم؟
Section titled “أيهما أستخدم؟”- عنصر يشبه الأزرار والبطاقات وعناصر القوائم: استخدم
InkWell، لأن المستخدم يتوقع إشارة مرئية. - إيماءات خاصة مثل السحب، أو عنصر لا تريد له تأثيراً مرئياً: استخدم
GestureDetector.
السحب: onPanUpdate
Section titled “السحب: onPanUpdate”لتحريك عنصر بالإصبع نتابع حركة الإصبع ونضيفها إلى موضع العنصر:
GestureDetector( onPanUpdate: (details) { setState(() { _position += details.delta; }); }, child: const Icon(Icons.circle, size: 64, color: Colors.indigo),)onPanUpdateتُستدعى عشرات المرات في الثانية أثناء تحرك الإصبع.details.deltaمن نوعOffset، وهو مقدار الحركة منذ آخر استدعاء: كم تحرك الإصبع أفقياً ورأسياً._positionمن نوعOffsetأيضاً، ونجمع عليه الحركة الجديدة بالعامل+=.
ثم نضع العنصر داخل Stack مع Positioned يقرأ _position.dx و _position.dy، كما سترى في المشروع.
منطقة اللمس الفارغة: behavior
Section titled “منطقة اللمس الفارغة: behavior”مفاجأة تحيّر المبتدئين: GestureDetector فوق Row فيه نص في البداية وأيقونة في النهاية. الضغط على النص يعمل، والضغط على الفراغ بينهما لا يعمل. السبب أن الكاشف افتراضياً يستجيب فقط للمناطق التي فيها شيء مرسوم.
GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { debugPrint('الصف كله يستجيب'); }, child: const Row( children: [ Text('الإعدادات'), Spacer(), Icon(Icons.chevron_left), ], ),)HitTestBehavior.opaque تجعل مساحة العنصر كلها تستجيب، حتى الفارغ منها.
حجم منطقة اللمس
Section titled “حجم منطقة اللمس”الإصبع ليس دقيقاً مثل مؤشر الفأرة. أيقونة حجمها 16 بكسل يصعب الضغط عليها، ويخطئ المستخدم فيضغط ما بجانبها. القاعدة في إرشادات التصميم: منطقة اللمس لا تقل عن 48×48، حتى لو كان الشكل الظاهر أصغر. أضف Padding حول العنصر الصغير داخل الكاشف لتكبير منطقة لمسه، أو استخدم IconButton الذي يضمن ذلك تلقائياً.
المشروع: إعجاب بضغطتين ومربع يُسحب
Section titled “المشروع: إعجاب بضغطتين ومربع يُسحب”class GesturesDemo extends StatefulWidget { const GesturesDemo({super.key});
@override State<GesturesDemo> createState() => _GesturesDemoState();}
class _GesturesDemoState extends State<GesturesDemo> { bool _liked = false; Offset _position = const Offset(40, 220);
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('الإيماءات')), body: Stack( children: [ Positioned( top: 24, left: 24, right: 24, child: GestureDetector( onDoubleTap: () { setState(() { _liked = !_liked; }); }, child: Container( height: 160, decoration: BoxDecoration( color: Colors.indigo.shade100, borderRadius: BorderRadius.circular(16), ), alignment: Alignment.center, child: Icon( _liked ? Icons.favorite : Icons.favorite_border, size: 64, color: _liked ? Colors.red : Colors.indigo, ), ), ), ), Positioned( left: _position.dx, top: _position.dy, child: GestureDetector( onPanUpdate: (details) { setState(() { _position += details.delta; }); }, child: Container( width: 80, height: 80, decoration: BoxDecoration( color: Colors.amber, borderRadius: BorderRadius.circular(16), ), alignment: Alignment.center, child: const Text('اسحبني'), ), ), ), ], ), ); }}في الشاشة عنصران:
- صندوق الإعجاب: اضغط عليه ضغطتين متتاليتين فيمتلئ القلب ويصبح أحمر. اضغط ضغطتين مرة أخرى فيعود فارغاً. ضغطة واحدة لا تفعل شيئاً.
- المربع الأصفر: ضع إصبعك عليه وحرّكه، فيتبع إصبعك أينما ذهب. كل حركة صغيرة تضيف
deltaإلى_position، ثم يعيدsetStateرسم المربع في مكانه الجديد.
لاحظ أن المربع يستخدم left و top لأن Offset يقيس من الزاوية العلوية اليسرى دائماً، بغض النظر عن اتجاه اللغة.
جرّب بنفسك
Section titled “جرّب بنفسك”- شغّل المشروع وجرّب الإعجاب والسحب.
- أضف
onLongPressإلى المربع الأصفر يعيده إلى مكانه الأول. - حوّل صندوق الإعجاب إلى
CardبداخلهInkWell، وشاهد الموجة عند الضغط. - تحدٍّ: امنع المربع من الخروج عن حدود الشاشة. (تلميح: قبل الحفظ، قيّد
dxوdyبالدالةclamp.)
أخطاء شائعة
Section titled “أخطاء شائعة”- GestureDetector بلا child ملموس: كاشف يلف
Containerبلا لون ولا حجم لا يلتقط شيئاً. أعطِ الابن حجماً، أو استخدمHitTestBehavior.opaque. - وضع InkWell تحت Container ملون: الموجة مخفية خلف اللون. استخدم
InkأوMaterial. - نسيان setState داخل onPanUpdate: الموضع يتغير في المتغير، لكن الشاشة لا تُرسم من جديد، فيبدو العنصر ثابتاً.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ما الفرق الأساسي بين GestureDetector و InkWell؟
الاثنان يلتقطان الضغط، لكن InkWell يضيف موجة Material المرئية، أما GestureDetector فصامت ويدعم إيماءات أكثر مثل السحب.
سؤال
ماذا تمثل details.delta داخل onPanUpdate؟
delta هو التغيّر الصغير في الموضع منذ آخر تحديث، ونجمعه على الموضع المحفوظ لنحصل على المكان الجديد.
الخلاصة
Section titled “الخلاصة”GestureDetectorيجعل أي Widget يستجيب للضغط والضغط المزدوج والمطوّل والسحب.InkWellيضيف موجة Material، ويحتاجMaterialفوقه دون لون يغطيه.onPanUpdateمعdetails.deltaلسحب العناصر، داخلStackوPositioned.HitTestBehavior.opaqueتجعل المساحات الفارغة داخل العنصر تستجيب.- اجعل منطقة اللمس 48×48 على الأقل.