هناك العديد من المصطلحات والمفاهيم التي من المهم فهمها عند العمل على معالجة الإيماءات في أحد التطبيقات. تشرح هذه الصفحة المصطلحات المؤشرات وأحداث المؤشر والإيماءات، وتقدم مستويات التجريد المختلفة للإيماءات. ويتعمّق أيضًا في استهلاك الأحداث ونشرها.
التعريفات
لفهم المفاهيم المختلفة في هذه الصفحة، عليك فهم بعض المصطلحات المستخدَمة:
- المؤشر: هو عنصر مادي يمكنك استخدامه للتفاعل مع تطبيقك.
بالنسبة للأجهزة المحمولة، فإن المؤشر الأكثر شيوعًا هو تفاعل إصبعك مع
شاشة اللمس. بدلاً من ذلك، يمكنك استخدام قلم شاشة بدلاً من إصبعك.
بالنسبة للشاشات الكبيرة، يمكنك استخدام الماوس أو لوحة التتبع للتفاعل بشكل غير مباشر مع
الشاشة. يجب أن يكون جهاز الإدخال قادرًا على "الإشارة" إلى إحداثي ليتم اعتباره مؤشرًا، لذا لا يمكن اعتبار لوحة المفاتيح، على سبيل المثال، مؤشرًا. في Compose، يتم تضمين نوع المؤشر في تغييرات المؤشر باستخدام
PointerType. - حدث المؤشر: يصف تفاعلاً منخفض المستوى لمؤشر واحد أو أكثر مع التطبيق في وقت معيّن. أي تفاعل مع المؤشر، مثل وضع إصبع على الشاشة أو سحب الماوس، سيؤدي إلى بدء حدث. في
Compose، يتم تضمين جميع المعلومات ذات الصلة بهذا الحدث في
فئة
PointerEvent. - الإيماءة: هي سلسلة من أحداث المؤشر التي يمكن تفسيرها على أنّها إجراء واحد. على سبيل المثال، يمكن اعتبار إيماءة النقر تسلسلاً لحدث الضغط لأسفل يليه حدث الضغط لأعلى. هناك إيماءات شائعة تستخدمها العديد من التطبيقات، مثل النقر أو السحب أو التحويل، ولكن يمكنك أيضًا إنشاء إيماءة مخصصة خاصة بك عند الحاجة.
مستويات مختلفة من التجريد
يوفّر Jetpack Compose مستويات مختلفة من التجريد للتعامل مع الإيماءات.
في المستوى الأعلى، يتوفّر دعم المكوّنات. تتضمّن الدوال المركّبة، مثل Button،
دعم الإيماءات تلقائيًا. لإضافة إمكانية استخدام الإيماءات إلى المكوّنات المخصّصة، يمكنك إضافة معدِّلات الإيماءات مثل clickable إلى الدوال المركّبة العشوائية. أخيرًا، إذا كنت بحاجة إلى إيماءة مخصّصة، يمكنك استخدام المُعدِّل
pointerInput.
كقاعدة عامة، يمكنك البناء على أعلى مستوى من التجريد الذي يوفر
الوظيفة التي تحتاجها. بهذه الطريقة، يمكنك الاستفادة من أفضل الممارسات المضمّنة
في الطبقة. على سبيل المثال، يحتوي Button على معلومات دلالية أكثر، تُستخدم
لتسهيل الاستخدام، من clickable، الذي يحتوي على معلومات أكثر من تنفيذ
pointerInput الأولي.
دعم المكوّنات
تتضمّن العديد من المكوّنات الجاهزة في Compose نوعًا من معالجة الإيماءات الداخلية. على سبيل المثال، يستجيب LazyColumn لإيماءات السحب من خلال
التمرير في محتواه، ويعرض Button تموّجًا عند الضغط عليه،
ويحتوي مكوّن SwipeToDismiss على منطق التمرير السريع لإغلاق
عنصر. يعمل هذا النوع من معالجة الإيماءات تلقائيًا.
بالإضافة إلى معالجة الإيماءات الداخلية، تتطلّب العديد من المكوّنات أيضًا من المتصل
معالجة الإيماءة. على سبيل المثال، يرصد Button تلقائيًا النقرات
ويشغّل حدثًا ناتجًا عن النقر. يمكنك تمرير دالة lambda من النوع onClick إلى Button للتفاعل مع الإيماءة. وبالمثل، يمكنك إضافة دالة lambda onValueChange إلى
Slider للتفاعل مع المستخدم الذي يسحب مقبض شريط التمرير.
عندما يكون ذلك مناسبًا لحالة الاستخدام، يُفضَّل استخدام الإيماءات المضمّنة في المكوّنات، لأنّها
تتضمّن دعمًا جاهزًا للتركيز وتسهيل الاستخدام، وقد تم اختبارها
بشكل جيد. على سبيل المثال، يتم وضع علامة على Button بطريقة خاصة لكي تصفه الخدمات المخصصة لتسهيل الاستخدام بشكل صحيح على أنّه زر، بدلاً من أي عنصر آخر قابل للنقر:
// Talkback: "Click me!, Button, double tap to activate" Button(onClick = { /* TODO */ }) { Text("Click me!") } // Talkback: "Click me!, double tap to activate" Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }
لمزيد من المعلومات حول أدوات تسهيل الاستخدام في Compose، يُرجى الاطّلاع على أدوات تسهيل الاستخدام في Compose.
إضافة إيماءات محدّدة إلى دوال مركّبة عشوائية باستخدام المُعدِّلات
يمكنك تطبيق معدِّلات الإيماءات على أي دالة مركّبة عشوائية لجعلها
تستجيب للإيماءات. على سبيل المثال، يمكنك السماح لـ Box عام
بالتعامل مع إيماءات النقر من خلال جعله clickable، أو السماح لـ Column
بالتعامل مع الانتقال العمودي من خلال تطبيق verticalScroll.
هناك العديد من المُعدِّلات للتعامل مع أنواع مختلفة من الإيماءات:
- التعامل مع النقرات والضغطات باستخدام المُعدِّلات
clickableوcombinedClickableوselectableوtoggleableوtriStateToggleable - التعامل مع الانتقال للأعلى أو للأسفل باستخدام المُعدِّلات
horizontalScrollوverticalScrollوالمُعدِّلات الأكثر عموميةscrollable - التعامل مع السحب باستخدام المبدّل
draggableوswipeable - التعامل مع الإيماءات عبر اللمس بأكثر من إصبع، مثل التحريك والتدوير والتكبير أو التصغير، باستخدام المُعدِّل
transformable
كقاعدة عامة، يُفضَّل استخدام معدِّلات الإيماءات الجاهزة بدلاً من معالجة الإيماءات المخصّصة.
تضيف المعدِّلات المزيد من الوظائف إلى معالجة أحداث المؤشر البحتة.
على سبيل المثال، لا يضيف المُعدِّل clickable رصد الضغطات والنقرات فحسب، بل يضيف أيضًا معلومات دلالية ومؤشرات مرئية حول التفاعلات والتمرير والتركيز ودعم لوحة المفاتيح. يمكنك الاطّلاع على الرمز المصدر
لـ clickable لمعرفة كيفية إضافة
الوظيفة.
إضافة إيماءة مخصّصة إلى دوال مركّبة عشوائية باستخدام المُعدِّل pointerInput
لا يتم تنفيذ كل إيماءة باستخدام معدِّل إيماءات جاهز. على سبيل المثال، لا يمكنك استخدام مفتاح تعديل للتفاعل مع السحب بعد الضغط مع الاستمرار أو النقر مع الضغط على مفتاح التحكم أو النقر بثلاثة أصابع. بدلاً من ذلك، يمكنك كتابة معالج الإيماءات
الخاص بك لتحديد هذه الإيماءات المخصصة. يمكنك إنشاء معالج إيماءات باستخدام المُعدِّل pointerInput، والذي يتيح لك الوصول إلى أحداث المؤشر الأولية.
يستمع الرمز التالي إلى أحداث المؤشر الأولية:
@Composable private fun LogPointerEvents(filter: PointerEventType? = null) { var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(filter) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // handle pointer event if (filter == null || event.type == filter) { log = "${event.type}, ${event.changes.first().position}" } } } } ) } }
إذا قسّمت هذا المقتطف، ستكون المكوّنات الأساسية هي:
- المعدِّل
pointerInputيمكنك تمرير مفتاح واحد أو أكثر إليه. عندما تتغيّر قيمة أحد هذه المفاتيح، تتم إعادة تنفيذ دالة Lambda لمحتوى المُعدِّل. يمرّر النموذج فلترًا اختياريًا إلى الدالة المركّبة. إذا تغيّرت قيمة هذا الفلتر، يجب إعادة تنفيذ معالج أحداث المؤشر للتأكّد من تسجيل الأحداث الصحيحة. - تنشئ
awaitPointerEventScopeنطاق كوروتين يمكن استخدامه لانتظار أحداث المؤشر. awaitPointerEventتعلّق الروتين المساعد إلى أن يحدث حدث المؤشر التالي.
على الرغم من أنّ الاستماع إلى أحداث الإدخال الأولية فعّال، إلا أنّه من الصعب أيضًا كتابة إيماءة مخصّصة استنادًا إلى هذه البيانات الأولية. لتبسيط إنشاء الإيماءات المخصّصة، تتوفّر العديد من طرق الأدوات المساعدة.
رصد الإيماءات الكاملة
بدلاً من معالجة أحداث المؤشر الأولية، يمكنك الاستماع إلى إيماءات معيّنة
والاستجابة لها بشكل مناسب. توفّر واجهة برمجة التطبيقات AwaitPointerEventScope
طرقًا للاستماع إلى:
- الضغط والنقر والنقر مرّتين والضغط مع الاستمرار:
detectTapGestures - السحب:
detectHorizontalDragGesturesوdetectVerticalDragGesturesوdetectDragGesturesوdetectDragGesturesAfterLongPress - التحويلات:
detectTransformGestures
هذه هي أدوات رصد ذات مستوى أعلى، لذا لا يمكنك إضافة أدوات رصد متعددة ضمن مُعدِّل pointerInput واحد. يرصد المقتطف التالي النقرات فقط، وليس
عمليات السحب:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } // Never reached detectDragGestures { _, _ -> log = "Dragging" } } ) }
داخليًا، تحظر طريقة detectTapGestures الكوروتين، ولا يتم الوصول إلى أداة الرصد الثانية مطلقًا. إذا كنت بحاجة إلى إضافة أكثر من مستمع إيماءات واحد إلى
دالة مركّبة، استخدِم نُسخًا منفصلة من المُعدِّل pointerInput بدلاً من ذلك:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } } .pointerInput(Unit) { // These drag events will correctly be triggered detectDragGestures { _, _ -> log = "Dragging" } } ) }
معالجة الأحداث لكل إيماءة
بحسب التعريف، تبدأ الإيماءات بحدث الضغط على المؤشر. يمكنك استخدام طريقة المساعدة
awaitEachGesture بدلاً من حلقة while(true) التي
تمرّ عبر كل حدث أولي. تُعيد طريقة awaitEachGesture تشغيل الكتلة التي تحتوي على الإيماءة عندما يتم رفع جميع المؤشرات، ما يشير إلى اكتمال الإيماءة:
@Composable private fun SimpleClickable(onClick: () -> Unit) { Box( Modifier .size(100.dp) .pointerInput(onClick) { awaitEachGesture { awaitFirstDown().also { it.consume() } val up = waitForUpOrCancellation() if (up != null) { up.consume() onClick() } } } ) }
من الناحية العملية، من الأفضل دائمًا استخدام awaitEachGesture ما لم تكن
تستجيب لأحداث المؤشر بدون تحديد الإيماءات. ومن الأمثلة على ذلك
hoverable، الذي لا يستجيب لأحداث الضغط على المؤشر أو رفعه، بل يحتاج فقط
إلى معرفة متى يدخل المؤشر حدوده أو يخرج منها.
الانتظار حتى حدوث حدث معيّن أو إيماءة فرعية
هناك مجموعة من الطرق التي تساعد في تحديد الأجزاء الشائعة من الإيماءات:
- يتم تعليق الإجراء إلى أن يتم الضغط على مؤشر باستخدام
awaitFirstDown، أو الانتظار حتى يتم رفع جميع المؤشرات باستخدامwaitForUpOrCancellation. - يمكنك إنشاء مستمع سحب منخفض المستوى باستخدام
awaitTouchSlopOrCancellationوawaitDragOrCancellation. يعلّق معالج الإيماءات أولاً حتى يصل المؤشر إلى الحد الأدنى للمسافة، ثم يعلّق حتى يتم تلقّي أول حدث سحب. إذا كنت مهتمًا فقط بعمليات السحب على محور واحد، استخدِمawaitHorizontalTouchSlopOrCancellationبالإضافة إلىawaitHorizontalDragOrCancellation، أوawaitVerticalTouchSlopOrCancellationبالإضافة إلىawaitVerticalDragOrCancellationبدلاً من ذلك. - تعليق حتى يتم الضغط مع الاستمرار على
awaitLongPressOrCancellation - استخدِم الطريقة
dragللاستماع باستمرار إلى أحداث السحب، أوhorizontalDragأوverticalDragللاستماع إلى أحداث السحب على محور واحد.
تطبيق العمليات الحسابية على أحداث نقاط الاتصال المتعددة
عندما ينفّذ المستخدم إيماءة عبر اللمس بأكثر من إصبع باستخدام أكثر من مؤشر،
يصعب فهم التحويل المطلوب استنادًا إلى القيم الأولية.
إذا لم يوفّر لك المعدِّل transformable أو الطريقتان detectTransformGestures
التحكّم الدقيق الكافي لحالة الاستخدام، يمكنك
الاستماع إلى الأحداث الأولية وتطبيق العمليات الحسابية عليها. هذه الطرق المساعدة هي calculateCentroid وcalculateCentroidSize وcalculatePan وcalculateRotation وcalculateZoom.
إرسال الأحداث واختبار مؤشر الماوس
لا يتم إرسال كل حدث مؤشر إلى كل مُعدِّل pointerInput. تعمل عملية إرسال الأحداث على النحو التالي:
- يتم إرسال أحداث المؤشر إلى تسلسل هرمي للدوال المركّبة. عندما يشغّل مؤشر جديد أول حدث مؤشر، يبدأ النظام في اختبار مؤشر الماوس للدوال المركّبة "المؤهَّلة". تُعتبر الدالة المركّبة مؤهَّلة عندما تتضمّن إمكانات معالجة إدخال المؤشر. تتدفق اختبارات مؤشر الماوس من أعلى شجرة واجهة المستخدم إلى أسفلها. يتم "النقر" على دالة مركّبة عندما يقع حدث المؤشر ضمن حدود تلك الدالة. تؤدي هذه العملية إلى إنشاء سلسلة من الدوال المركّبة التي تجتاز اختبار مؤشر الماوس بنجاح.
- بشكل تلقائي، عندما تكون هناك عدة دوال مركّبة مؤهلة على المستوى نفسه من الشجرة، يتم "الضغط" على الدالة المركّبة التي تتضمّن أعلى فهرس z فقط. على سبيل المثال، عند إضافة دالتَين مركّبتَين متداخلتَين من النوع
Buttonإلى دالةBox، تتلقّى الدالة المرسومة في الأعلى فقط أي أحداث مؤشر. يمكنك نظريًا تجاوز هذا السلوك من خلال إنشاء عملية تنفيذPointerInputModifierNodeالخاصة بك وضبطsharePointerInputWithSiblingsعلى "صحيح". - يتم إرسال الأحداث الإضافية للمؤشر نفسه إلى سلسلة الدوال المركّبة نفسها، وتتدفق وفقًا لمنطق انتشار الأحداث. لا يُجري النظام أي اختبارات أخرى لمؤشر الماوس هذا. وهذا يعني أنّ كل دالة مركّبة في السلسلة تتلقّى جميع الأحداث الخاصة بهذا المؤشر، حتى عندما تحدث خارج حدود هذه الدالة المركّبة. لا تتلقّى الدوال المركّبة غير المضمّنة في السلسلة أحداث المؤشر، حتى عندما يكون المؤشر داخل حدودها.
تُعدّ أحداث التمرير، التي يتم تشغيلها من خلال تمرير الماوس أو القلم، استثناءً للقواعد المحدّدة هنا. يتم إرسال أحداث التمرير إلى أي دالة مركّبة يتم تمرير المؤشر فوقها. لذلك، عندما يمرّر المستخدم المؤشر من حدود دالة مركّبة إلى أخرى، يتم إرسال الأحداث إلى الدالة المركّبة الجديدة بدلاً من إرسالها إلى الدالة المركّبة الأولى.
الاستهلاك من خلال الفعاليات
عندما يكون هناك أكثر من دالة مركّبة تم تعيين معالج إيماءات لها، يجب ألا تتعارض هذه المعالجات. على سبيل المثال، ألقِ نظرة على واجهة المستخدم هذه:
عندما ينقر المستخدم على زر الإشارة المرجعية، تعالج دالة onClick Lambda الخاصة بالزر هذه الإيماءة. عندما ينقر المستخدم على أي جزء آخر من عنصر القائمة، يعالج ListItem هذه الإيماءة وينتقل إلى المقالة. في ما يتعلق بإدخال المؤشر،
يجب أن يستهلك الزر هذا الحدث، حتى يعرف العنصر الرئيسي عدم
التفاعل معه بعد الآن. تتضمّن الإيماءات المضمّنة في المكوّنات الجاهزة ومعدِّلات الإيماءات الشائعة سلوك الاستهلاك هذا، ولكن إذا كنت تكتب إيماءة مخصّصة، عليك استهلاك الأحداث يدويًا. يمكنك إجراء ذلك
باستخدام طريقة PointerInputChange.consume:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
لا يؤدي استهلاك حدث إلى إيقاف انتشار الحدث إلى الدوال المركّبة الأخرى. بدلاً من ذلك، يجب أن تتجاهل الدالة المركّبة الأحداث التي تم استخدامها بشكل صريح. عند كتابة إيماءات مخصّصة، عليك التحقّق مما إذا كان قد تم استخدام حدث من قِبل عنصر آخر:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() if (event.changes.any { it.isConsumed }) { // A pointer is consumed by another gesture handler } else { // Handle unconsumed event } } } }
نشر الأحداث
كما ذكرنا سابقًا، يتم تمرير تغييرات المؤشر إلى كل دالة مركّبة يصل إليها.
ولكن إذا كان هناك أكثر من دالة مركّبة من هذا النوع، فما هو ترتيب انتشار الأحداث؟ إذا أخذنا المثال من القسم الأخير، فستتم ترجمة واجهة المستخدم هذه إلى
شجرة واجهة المستخدم التالية، حيث يستجيب فقط ListItem وButton لأحداث
المؤشر:
تتدفق أحداث المؤشر عبر كل من هذه الدوال المركّبة ثلاث مرات، خلال ثلاث "مراحل":
- في المرحلة الأولية، يتدفق الحدث من أعلى شجرة واجهة المستخدم إلى
أسفلها. يسمح هذا التدفق للعنصر الرئيسي باعتراض حدث قبل أن يتمكن العنصر الفرعي من
استهلاكه. على سبيل المثال، يجب أن تعترض تلميحات الشاشة
الضغط مع الاستمرار بدلاً من تمريره إلى العناصر الفرعية. في مثالنا، يتلقّى
ListItemالحدث قبلButton. - في المسار الرئيسي، يتدفق الحدث من العُقد الطرفية لشجرة واجهة المستخدم إلى جذر شجرة واجهة المستخدم. هذه المرحلة هي التي تستهلك فيها الإيماءات عادةً، وهي
المرور التلقائي عند الاستماع إلى الأحداث. يعني التعامل مع الإيماءات في هذه المرحلة أنّ العُقد الطرفية لها الأولوية على العُقد الرئيسية، وهو السلوك الأكثر منطقية لمعظم الإيماءات. في مثالنا، يتلقّى
Buttonالحدث قبلListItem. - في المرحلة النهائية، يتدفق الحدث مرة أخرى من أعلى شجرة واجهة المستخدم إلى العُقد الطرفية. يسمح هذا التدفق للعناصر الأعلى في المكدس بالاستجابة لاستهلاك الحدث من قبل والدهم. على سبيل المثال، يزيل الزر مؤشر التموج عندما يتحول الضغط إلى سحب للعنصر الرئيسي القابل للتمرير.
من الناحية المرئية، يمكن تمثيل تدفق الأحداث على النحو التالي:
بعد استهلاك تغيير في الإدخال، يتم تمرير هذه المعلومات من تلك النقطة في المسار فصاعدًا:
في الرمز البرمجي، يمكنك تحديد البطاقة التي تهمّك:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
في مقتطف الرمز هذا، يتم عرض الحدث نفسه من خلال كلّ من طلبات الإجراءات هذه، على الرغم من أنّ البيانات المتعلّقة بالاستهلاك قد تكون تغيّرت.
اختبار الإيماءات
في طرق الاختبار، يمكنك إرسال أحداث المؤشر يدويًا باستخدام طريقة
performTouchInput. يتيح لك ذلك تنفيذ إيماءات كاملة عالية المستوى (مثل التصغير أو التكبير بإصبعين أو النقر مع الاستمرار) أو إيماءات منخفضة المستوى (مثل تحريك المؤشر بمقدار معيّن من وحدات البكسل):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
يمكنك الاطّلاع على مستندات performTouchInput لمزيد من الأمثلة.
مزيد من المعلومات
يمكنك معرفة المزيد عن الإيماءات في Jetpack Compose من خلال المراجع التالية:
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعَّل
- أدوات تسهيل الاستخدام في Compose
- الانتقال
- النقر والضغط