عندما كتبت آخر مقالتي حول Jetpack Compose، أخبرت بأنه يفتقر Jetpack Compose إلى بعض المكونات الأساسية (وهي بمعتقدي)، وأحدها هو المشاهد.

في ذلك الوقت ، لم يكن هناك مكون قابل للتركيب بداخله لعرض المشاهدات وكانت هناك عدة حلول بديلة تحوم عبر الانترنت. مشكلة هذه الحلول كانت أنه وما يزال جيتبك Compose ينشر أصدرات جديدة ، قد تفقد تلك الحلول. لذلك لم يكن الأمر مثالياً وتمت إنتظار المجموعة أن يضمنا دعم للمشاهدات في المستقبل.

أنا سعيدة بقول أنه منذ نسخة 1.1.0 من Compose Material 3، لدينا الآن دعم بالمشاهدات البنية. 👏

بالرغم من أن هذا بمفاجأة جيد، تم إصدار نسخة أخرى من الإصدرات منذ ذلك الوقت. ومع الإصدرات التالية، تغيرت الأيبي الخاصة والعامة المتعلقة بالمشاهدات بشكل كبير.

إذا نظرت الى السجل التاريخي، سترون كيف تغيرت الأيبي العامة والخاصة. لذلك أخبركم بأنه عندما تقرأون هذا المقال ، قد تكون هناك تغيرات أكثر بما يلي كل شيء يخص المشاهدات يمكن تعريفه بتعليمة ExperimentalMaterial3Api::class.

❗️ الإصدرات التي تستخدم في هذه المقالة لمادة 3 هي 1.2.1 المنشرة في 6 مارس 2024

أنواع المشاهدات

نحن نتمكن من دعم عقوبين أنواع مختلفة من المشاهدات:

  1. المشاهدة البسيطة

  2. تلميح وسائط غني

تلميح بسيط

يمكنك استخدام النوع الأول لتوفير معلومات حول زر الأيقونة الذي لن يكون واضحًا بخلاف ذلك. على سبيل المثال، يمكنك استخدام تلميح بسيط للإشارة إلى المستخدم ما تمثله أيقونة الزر.

لإضافة تلميح إلى تطبيقك، تستخدم TooltipBox القابل للتجميع. يأخذ هذا القابل للتجميع عدة معلمات:

fun TooltipBox(
    positionProvider: PopupPositionProvider,
    tooltip: @Composable TooltipScope.() -> Unit,
    state: TooltipState,
    modifier: Modifier = Modifier,
    focusable: Boolean = true,
    enableUserInput: Boolean = true,
    content: @Composable () -> Unit,
)

بعض هذه المعلمات يجب أن تكون مألوفة لك إذا كنت قد استخدمت القوابل للتجميع من قبل. سأبرز تلك التي لها حالة استخدام محددة هنا:

  • positionProvider – من نوع PopupPositionProvider، ويستخدم لحساب موضع التلميح.

  • tooltip – هذا هو المكان الذي يمكنك فيه تصميم واجهة المستخدم لكيفية ظهور التلميح.

  • state – هذا يحتفظ بالحالة المرتبطة بمثيل معين من التلميح. يكشف عن طرق مثل عرض/إخفاء التلميح وعند إنشاء مثيل واحد، يمكنك تحديد ما إذا كان التلميح يجب أن يكون ثابتًا أم لا (بمعنى ما إذا كان يجب أن يستمر في العرض على الشاشة حتى يقوم المستخدم بإجراء نقرة خارج التلميح).

  • المحتوى – هذه هي الواجهة المستخدمة التي ستظهر النافذة المساعدة أعلى/أسفل.

هاهو مثال على إنشاء BasicTooltipBox بملء جميع الآرجumentات النمطية:

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun BasicTooltip() {
    val tooltipPosition = TooltipDefaults.rememberPlainTooltipPositionProvider()
    val tooltipState = rememberBasicTooltipState(isPersistent = false)

    BasicTooltipBox(positionProvider = tooltipPosition,
        tooltip =  { Text("Hello World") } ,
        state = tooltipState) {
        IconButton(onClick = { }) {
            Icon(imageVector = Icons.Filled.Favorite, 
                 contentDescription = "Your icon's description")
        }
    }
}

يحتوي جيتباك كومبوز على فئة مبنية من الأساس تُدعى TooltipDefaults. يمكنك استخدام هذه الفئة لمساعدتك في تنشيء الآرجументات التي تكون نافذة المساعدة. على سبيل المثال، يمكنك استخدام TooltipDefaults.rememberPlainTooltipPositionProvider لتوضيح موضع النافذة المساعدة بشكل صحيح بالنسبة لعنصر الربط.

نافذة المساعدة الغنية

تأخذ نافذة الوسائط الغنية مساحة أكبر من النافذة المساعدة البسيطة ويمكن استخدامها لتوفير مزيد من ال”context” حول وظيفة زر الأيقونة. عند عرض النافذة المساعدة، يمكنك إضافة أزرار وروابط لتوفير تفسير أو تعاريف إضافية.

تُنشأ بطريقة مشابهة للنافذة المساعدة البسيطة، داخل نافذة TooltipBox، ولكن باستخدام الكمبوزر RichTooltip.

TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text("RichTooltip") },
                      caretSize = caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {
                        Text("This is where a description would go.")
                  }
        },
        state = tooltipState) {
        IconButton(onClick = {
            /* حدث النقر لزر الأيقونة */
        }) {
            Icon(imageVector = tooltipIcon,
                contentDescription = "Your icon's description",
                tint = iconColor)
        }
    }

بعض الأشياء التي يجب أن تلاحظها بشأن نافذة المساعدة الغنية:

  1. تتيح نافذة المساعدة الغنية الدعم لعلامة الإشارة.

  2. يمكنك إضافة إجراء (أي زر) إلى تلميح الأداة لمنح المستخدمين خيار معرفة المزيد من المعلومات.

  3. يمكنك إضافة منطق لإغلاق تلميح الأداة.

حالات الحافة

عندما تختار تحديد حالة تلميح الأداة على أنها دائمة، فهذا يعني أنه بمجرد أن يتفاعل المستخدم مع واجهة المستخدم التي تعرض تلميح الأداة الخاص بك، ستظل مرئية حتى يضغط المستخدم في أي مكان آخر على الشاشة.

إذا نظرت إلى مثال تلميح الأداة الغني من الأعلى، قد تكون لاحظت أننا أضفنا زرًا لإغلاق تلميح الأداة بمجرد النقر عليه.

هناك مشكلة تحدث بمجرد أن يضغط المستخدم على ذلك الزر. نظرًا لأن إجراء الإغلاق يتم على تلميح الأداة، إذا أراد المستخدم تنفيذ ضغطة طويلة أخرى على عنصر واجهة المستخدم الذي يستدعي هذا التلميح، فلن يظهر التلميح مرة أخرى. هذا يعني أن حالة تلميح الأداة دائمة في حالة إغلاقها. فكيف نتعامل مع هذه المشكلة ونحلها؟

من أجل “إعادة تعيين” حالة تلميح الأداة، يجب علينا استدعاء طريقة onDispose التي يتم توفيرها من خلال حالة تلميح الأداة. بمجرد أن نفعل ذلك، يتم إعادة تعيين حالة تلميح الأداة وسيتم عرض التلميح مرة أخرى عندما يقوم المستخدم بتنفيذ ضغطة طويلة على عنصر واجهة المستخدم.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RichTooltip() {
    val tooltipPosition = TooltipDefaults.rememberRichTooltipPositionProvider()
    val tooltipState = rememberTooltipState(isPersistent = true)
    val scope = rememberCoroutineScope()

    TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text("RichTooltip") },
                      caretSize = TooltipDefaults.caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()  /// <---- هنا
                              }
                          }) {
                              Text("Dismiss")
                          }
                      }
                  ) {

                  }
        },
        state = tooltipState) {
        IconButton(onClick = {  }) {
            Icon(imageVector = Icons.Filled.Call, contentDescription = "Your icon's description")
        }
    }
}

سيناريو آخر حيث لا يتم إعادة ضبط حالة التلميح هو إذا بدلاً من استدعاء أنفسنا لطريقة الإزالة بناءً على إجراء المستخدم، نقر المستخدم خارج التلميح، مما يتسبب في إزالته. يتم استدعاء طريقة الإزالة خلف الكواليس وتُضبط حالة التلميح على مُزال. الضغط المطول على عنصر واجهة المستخدم لرؤية التلميح مرة أخرى لن ينتج عنه شيء.

منطقنا الذي يستدعي طريقة onDispose للتلميح لا يتم تفعيله، فكيف يمكننا إعادة ضبط حالة التلميح؟

حاليًا، لم أتمكن من معرفة ذلك. قد يكون ذلك مرتبطًا بـ MutatorMutex للتلميح. ربما مع الإصدارات القادمة، سيكون هناك API لهذا. لقد لاحظت أنه إذا كانت هناك تلميحات أخرى موجودة على الشاشة وتم الضغط عليها، فإن هذا يعيد ضبط التلميح الذي تم النقر عليه مسبقًا.

إذا كنت ترغب في رؤية الكود المعروض هنا، يمكنك الذهاب إلى هذا المستودع على GitHub

إذا كنت ترغب في رؤية التلميحات في تطبيق، يمكنك التحقق منها هنا.

المراجع