إنشاء تنسيقات تكيّفية

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

باعتبارها مجموعة أدوات تعريفية لواجهة المستخدم، تُعد Jetpack Compose وسيلة مناسبة لتصميم وتنفيذ تصاميم يمكن تعديلها لتعرض المحتوى بشكل مختلف على مجموعة متنوعة من الأحجام. يحتوي هذا المستند على بعض الإرشادات حول كيفية استخدام Compose لجعل واجهة المستخدم سريعة الاستجابة.

إجراء تغييرات كبيرة على تنسيق العناصر القابلة للإنشاء على مستوى الشاشة

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

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

رسم بياني يوضّح العديد من أشكال الأجهزة المختلفة، مثل هاتف وجهاز قابل للطي وجهاز لوحي وكمبيوتر محمول

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

بدلاً من ذلك، يجب اتخاذ القرارات استنادًا إلى الجزء الفعلي من الشاشة المخصّص لتطبيقك، مثل مقاييس النوافذ الحالية التي تقدّمها مكتبة WindowManager في Jetpack. لمعرفة كيفية استخدام WindowManager في تطبيق Compose، يمكنك الاطّلاع على نموذج JetNews.

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

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

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val windowSizeClass = calculateWindowSizeClass(this)
            MyApp(windowSizeClass)
        }
    }
}
@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {
    // Perform logic on the size class to decide whether to show
    // the top app bar.
    val showTopAppBar = windowSizeClass.heightSizeClass != WindowHeightSizeClass.Compact

    // MyScreen knows nothing about window sizes, and performs logic
    // based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

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

عناصر قابلة للإنشاء المدمَجة المرنة قابلة لإعادة الاستخدام

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

لنلقِ نظرة على مثال: تخيَّل مادة عرض مدمجة قابلة للإنشاء تنفّذ تنسيقًا يتضمّن قائمة تفاصيل، والذي قد يعرض جزءًا واحدًا أو جزءَين جنبًا إلى جنب.

لقطة شاشة لتطبيق يعرض جزأين جنبًا إلى جنب

الشكل 1. لقطة شاشة لتطبيق يعرض تخطيطًا نموذجيًا للقائمة/التفاصيل. 1 هو منطقة القائمة، و2 هو منطقة التفاصيل.

نريد أن يكون هذا القرار جزءًا من التخطيط العام للتطبيق، لذلك نمرر القرار من جهاز قابل للإنشاء على مستوى الشاشة كما رأينا أعلاه:

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

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

مثالان على بطاقتَين مختلفتَين: بطاقة ضيقة تعرض رمزًا وعنوانًا فقط، وبطاقة أكبر تعرض الرمز والعنوان والوصف الموجز

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

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

وبالتالي، علينا استخدام العرض الذي يتم منحه إلى العنصر القابل للإنشاء لعرض نفسه. لدينا خياران للحصول على هذا العرض:

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

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

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

التأكد من توفر كل البيانات لأحجام مختلفة

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

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

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

استنادًا إلى مثال Card، نلاحظ أنّه يتم دائمًا تمرير description إلى Card. على الرغم من أنّ السمة description لا يتم استخدامها إلا عندما يسمح العرض بعرضها، تتطلّب السمة Card دائمًا، بغض النظر عن العرض المتاح.

يؤدي تمرير البيانات دائمًا إلى جعل التخطيطات القابلة للتكيف أكثر بساطة من خلال جعلها أقل حالة من حيث الحالة، كما يتجنب التسبب في حدوث تأثيرات جانبية عند التبديل بين الأحجام (والتي قد تحدث بسبب تغيير حجم النافذة أو تغيير الاتجاه أو طي الجهاز وفتحه).

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

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

مزيد من المعلومات

لمزيد من المعلومات حول التنسيقات المخصصة في Compose، يمكنك الرجوع إلى الموارد الإضافية التالية.

نماذج التطبيقات

  • التخطيطات الأساسية للشاشات الكبيرة هي مستودع لأنماط التصميم المثبَّتة التي تقدّم أفضل تجربة للمستخدم على الأجهزة ذات الشاشات الكبيرة.
  • تعرض JetNews كيفية تصميم تطبيق يعدِّل واجهة المستخدم للاستفادة من المساحة المتوفرة
  • الرد هو نموذج تكيُّفي لدعم الأجهزة الجوّالة والأجهزة اللوحية والأجهزة القابلة للطي
  • الآن في Android تطبيق يستخدم التنسيقات التكيُّفية للتوافق مع أحجام الشاشات المختلفة

الفيديوهات الطويلة