إنشاء واجهة مستخدم من خلال ميزة "نظرة سريعة"

تصف هذه الصفحة كيفية التعامل مع الأحجام وتوفير تخطيطات مرنة وسريعة الاستجابة باستخدام ميزة "نظرة سريعة".

استخدام Box وColumn وRow

تتضمّن ميزة Glance ثلاثة تنسيقات رئيسية قابلة للإنشاء:

  • Box: يضع العناصر فوق عناصر أخرى تتم ترجمتها إلى RelativeLayout.

  • Column: يضع العناصر بعد بعضها في المحور الرأسي تتم ترجمتها إلى LinearLayout باتجاه عمودي.

  • Row: يضع العناصر بعد بعضها في المحور الأفقي. تتم ترجمتها إلى LinearLayout باتجاه أفقي.

صورة تخطيط عمود وصف ومربّع.
الشكل 1. أمثلة على التنسيقات باستخدام العمود والصف والمربّع

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

يوضح لك المثال التالي كيفية إنشاء Row يوزّع عناصره الفرعية أفقيًا بشكل متساوٍ، كما هو موضّح في الشكل 1:

Row(modifier = GlanceModifier.fillMaxWidth().padding(16.dp)) {
    val modifier = GlanceModifier.defaultWeight()
    Text("first", modifier)
    Text("second", modifier)
    Text("third", modifier)
}

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

استخدام تخطيطات قابلة للتمرير

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

تعرض المقتطفات التالية طرقًا مختلفة لتحديد العناصر داخل LazyColumn.

يمكنك توفير عدد العناصر:

// Remember to import Glance Composables
// import androidx.glance.appwidget.layout.LazyColumn

LazyColumn {
    items(10) { index: Int ->
        Text(
            text = "Item $index",
            modifier = GlanceModifier.fillMaxWidth()
        )
    }
}

قدِّم عناصر فردية:

LazyColumn {
    item {
        Text("First Item")
    }
    item {
        Text("Second Item")
    }
}

تقديم قائمة أو مصفوفة من العناصر:

LazyColumn {
    items(peopleNameList) { name ->
        Text(name)
    }
}

يمكنك أيضًا استخدام مجموعة من الأمثلة السابقة:

LazyColumn {
    item {
        Text("Names:")
    }
    items(peopleNameList) { name ->
        Text(name)
    }

    // or in case you need the index:
    itemsIndexed(peopleNameList) { index, person ->
        Text("$person at index $index")
    }
}

يُرجى العلم أنّ المقتطف السابق لا يحدّد السمة itemId. يساعد تحديد itemId في تحسين الأداء والحفاظ على موضع التمرير من خلال تحديثات القائمة وappWidget من نظام التشغيل Android 12 والإصدارات الأحدث (مثلاً، عند إضافة عناصر أو إزالتها من القائمة). يوضّح المثال التالي كيفية تحديد itemId:

items(items = peopleList, key = { person -> person.id }) { person ->
    Text(person.name)
}

تحديد SizeMode

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

SizeMode.Single

الوضع التلقائي هو SizeMode.Single. ويشير إلى أنّه يتم توفير نوع واحد فقط من المحتوى، أي أنّه حتى إذا تغيّر حجم AppWidget المتاح، لن يتغيّر حجم المحتوى.

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Single

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the minimum size or resizable
        // size defined in the App Widget metadata
        val size = LocalSize.current
        // ...
    }
}

عند استخدام هذا الوضع، تأكَّد مما يلي:

  • يتم تحديد قيم البيانات الوصفية للحد الأدنى والأقصى للحجم بشكل صحيح استنادًا إلى حجم المحتوى.
  • المحتوى مرن بدرجة كافية ضمن نطاق الحجم المتوقع.

بشكل عام، يجب استخدام هذا الوضع في إحدى الحالتَين أدناه:

أ) AppWidget بحجم ثابت، أو ب) لا يغير محتواه عند تغيير حجمه.

SizeMode.Responsive

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

على سبيل المثال، في الوجهة AppWidget، يمكنك تحديد ثلاثة أحجام ومحتواها:

class MyAppWidget : GlanceAppWidget() {

    companion object {
        private val SMALL_SQUARE = DpSize(100.dp, 100.dp)
        private val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp)
        private val BIG_SQUARE = DpSize(250.dp, 250.dp)
    }

    override val sizeMode = SizeMode.Responsive(
        setOf(
            SMALL_SQUARE,
            HORIZONTAL_RECTANGLE,
            BIG_SQUARE
        )
    )

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be one of the sizes defined above.
        val size = LocalSize.current
        Column {
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            }
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width >= HORIZONTAL_RECTANGLE.width) {
                    Button("School")
                }
            }
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "provided by X")
            }
        }
    }
}

في المثال السابق، يتم استدعاء طريقة provideContent ثلاث مرات وربطها بالحجم المحدّد.

  • في المكالمة الأولى، يتم تقييم المقاس إلى 100x100. لا يتضمن المحتوى الزر الإضافي، ولا النصوص العلوية والسفلية.
  • في المكالمة الثانية، يتم تقييم الحجم ليكون 250x100. يتضمن المحتوى الزر الإضافي، ولكن ليس النصوص العلوية والسفلية.
  • في المكالمة الثالثة، يتم تقييم المقاس إلى 250x250. يتضمن المحتوى الزر الإضافي وكلا النصين.

تشكّل السمة SizeMode.Responsive مزيجًا من الوضعَين الآخرَين، وتتيح لك تحديد المحتوى السريع الاستجابة ضمن الحدود المحدَّدة مسبقًا. بشكل عام، يؤدي هذا الوضع بشكل أفضل ويسمح بانتقالات أكثر سلاسة عند تغيير حجم AppWidget.

يُظهر الجدول التالي قيمة المقاس استنادًا إلى SizeMode وAppWidget الحجم المتاح:

الحجم المتاح 105 × 110 203 × 112 72 × 72 203 × 150
SizeMode.Single 110 × 110 110 × 110 110 × 110 110 × 110
SizeMode.Exact 105 × 110 203 × 112 72 × 72 203 × 150
SizeMode.Responsive 80 x 100 80 x 100 80 x 100 150 × 120
* القيم الدقيقة هي فقط لأغراض العرض التوضيحي.

SizeMode.Exact

تعادل SizeMode.Exact توفير تنسيقات دقيقة، والتي تطلب محتوى GlanceAppWidget في كل مرة يتغيّر فيها حجم AppWidget المتاح (على سبيل المثال، عندما يغيّر المستخدم حجم AppWidget في الشاشة الرئيسية).

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

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Exact

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the size of the AppWidget
        val size = LocalSize.current
        Column {
            Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width > 250.dp) {
                    Button("School")
                }
            }
        }
    }
}

يوفّر هذا الوضع مرونة أكبر من غيرها، ولكن يأتي مع بعض التنبيهات:

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

بشكل عام، يجب استخدام هذا الوضع في حال تعذّر استخدام SizeMode.Responsive (أي أنّ مجموعة صغيرة من التنسيقات المتجاوبة غير ممكنة).

الوصول إلى الموارد

يمكنك استخدام LocalContext.current للوصول إلى أي من موارد Android، كما هو موضّح في المثال التالي:

LocalContext.current.getString(R.string.glance_title)

ننصحك بتوفير معرّفات الموارد مباشرةً لتقليل حجم عنصر RemoteViews النهائي وتفعيل الموارد الديناميكية، مثل الألوان الديناميكية.

تقبل العناصر القابلة للإنشاء والطرق الموارد باستخدام "موفِّر"، مثل ImageProvider، أو استخدام طريقة تحميل زائد، مثل GlanceModifier.background(R.color.blue). على سبيل المثال:

Column(
    modifier = GlanceModifier.background(R.color.default_widget_background)
) { /**...*/ }

Image(
    provider = ImageProvider(R.drawable.ic_logo),
    contentDescription = "My image",
)

إضافة أزرار مركبة

تم إطلاق الأزرار المركّبة في Android 12. تدعم ميزة Glance التوافق مع الأنظمة العكسية للأنواع التالية من الأزرار المركّبة:

يعرض كل من هذه الأزرار المركّبة عرضًا قابلاً للنقر يمثل حالة "تم التحديد".

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableStateOf(0) }

CheckBox(
    checked = isApplesChecked,
    onCheckedChange = { isApplesChecked = !isApplesChecked },
    text = "Apples"
)

Switch(
    checked = isEnabledSwitched,
    onCheckedChange = { isEnabledSwitched = !isEnabledSwitched },
    text = "Enabled"
)

RadioButton(
    checked = isRadioChecked == 1,
    onClick = { isRadioChecked = 1 },
    text = "Checked"
)

عند تغيير الحالة، يتم تفعيل دالة lambda المقدَّمة. يمكنك تخزين حالة التحقّق، كما هو موضّح في المثال التالي:

class MyAppWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val myRepository = MyRepository.getInstance()

        provideContent {
            val scope = rememberCoroutineScope()

            val saveApple: (Boolean) -> Unit =
                { scope.launch { myRepository.saveApple(it) } }
            MyContent(saveApple)
        }
    }

    @Composable
    private fun MyContent(saveApple: (Boolean) -> Unit) {

        var isAppleChecked by remember { mutableStateOf(false) }

        Button(
            text = "Save",
            onClick = { saveApple(isAppleChecked) }
        )
    }
}

يمكنك أيضًا توفير السمة colors إلى CheckBox وSwitch وRadioButton لتخصيص ألوانها:

CheckBox(
    // ...
    colors = CheckboxDefaults.colors(
        checkedColor = ColorProvider(day = colorAccentDay, night = colorAccentNight),
        uncheckedColor = ColorProvider(day = Color.DarkGray, night = Color.LightGray)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked }
)

Switch(
    // ...
    colors = SwitchDefaults.colors(
        checkedThumbColor = ColorProvider(day = Color.Red, night = Color.Cyan),
        uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Magenta),
        checkedTrackColor = ColorProvider(day = Color.Blue, night = Color.Yellow),
        uncheckedTrackColor = ColorProvider(day = Color.Magenta, night = Color.Green)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked },
    text = "Enabled"
)

RadioButton(
    // ...
    colors = RadioButtonDefaults.colors(
        checkedColor = ColorProvider(day = Color.Cyan, night = Color.Yellow),
        uncheckedColor = ColorProvider(day = Color.Red, night = Color.Blue)
    ),

)