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

تصف هذه الصفحة كيفية التعامل مع الأحجام وتوفير تنسيقات مرنة ومتجاوبة باستخدام Glance، وذلك باستخدام مكوّنات Glance الحالية.

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

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

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

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

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

يتيح Glance استخدام كائنات Scaffold. ضَع العناصر القابلة للإنشاء Column وRow وBox ضمن كائن Scaffold معيّن.

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

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

يوضّح لك المثال التالي كيفية إنشاء 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, itemId = { person -> person.id.hashCode().toLong() }) { person ->
    Text(person.name)
}

التمرير السريع

التمرير السريع هو رسم متحرك يسمح للمحتوى القابل للتمرير بالمحاذاة إلى أعلى حاوية أداة التطبيق.

الفيديو 1. على اليمين، يظهر عنصر قائمة لا يتم تمريره سريعًا إلى الموضع أثناء التمرير، بينما يتم تمريره سريعًا إلى الموضع على اليسار.


لتطبيق ميزة التمرير السريع، تأكَّد من استيفاء الشروط التالية:

  • يجب تحديث تبعية Glance إلى الإصدار 1.3.0-alpha02 أو إصدار أحدث.
  • يجب ضبط compileSdk على 37 أو إصدار أحدث، لأنّ ميزة "التمرير السريع" متاحة على الأجهزة التي تعمل بالإصدار 17 من Android والإصدارات الأحدث.
  • يجب ضبط LazyColumn باستخدام VerticalScrollMode. إذا كان الجهاز يتيح ميزة "التمرير السريع"، استخدِم SnapScrollMatchHeight. وإلا، استخدِم Normal.

إذا كنت تستخدم ميزة "التمرير السريع" مع الصور، اطّلِع على التنسيق الأساسي للصور التي تملأ المساحة بالكامل.

@Composable
fun SnapScrollLayout() {
    val height = LocalSize.current.height
    val items = listOf(
        ColorItem(Color.Red, "Red"),
        ColorItem(Color.Yellow, "Yellow"),
        ColorItem(Color.Blue, "Blue")
    )

    val scrollMode = if (Build.VERSION.SDK_INT >= 37) {
        VerticalScrollMode.SnapScrollMatchHeight(height)
    } else {
        VerticalScrollMode.Normal
    }

    LazyColumn(
        verticalScrollMode = scrollMode
    ) {
        items(items) { item ->
            ColorCard(item, height)
        }
    }
}

@Composable
private fun ColorCard(item: ColorItem, height: Dp) {
    Box(
        modifier = GlanceModifier
            .background(item.color)
            .fillMaxWidth()
            .height(height),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = item.name,
            modifier = GlanceModifier.background(Color.White)
        )
    }
}

تحديد SizeMode

قد تختلف أحجام AppWidget حسب الجهاز أو اختيار المستخدم أو مشغّل التطبيقات، لذا من المهم توفير تنسيقات مرنة كما هو موضّح في صفحة توفير تنسيقات مرنة لأدوات التطبيقات. يبسّط Glance ذلك من خلال تعريف 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
        // ...
    }
}

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

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

بشكلٍ عام، يجب استخدام هذا الوضع في الحالات التالية:

أ) إذا كان 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 × 100 80 × 100 80 × 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 قبل 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",
)

التعامل مع النص

يتضمّن Glance 1.1.0 واجهة برمجة تطبيقات لضبط أنماط النص. يمكنك ضبط أنماط النص باستخدام سمات fontSize أو fontWeight أو fontFamily لفئة TextStyle.

تتيح fontFamily استخدام جميع خطوط النظام، كما هو موضّح في المثال التالي، ولكن لا تتيح استخدام الخطوط المخصّصة في التطبيقات:

Text(
    style = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 18.sp,
        fontFamily = FontFamily.Monospace
    ),
    text = "Example Text"
)

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

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

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

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableIntStateOf(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)
    ),

)

مكوّنات إضافية

يتضمّن Glance 1.1.0 إصدار مكوّنات إضافية، كما هو موضّح في الجدول التالي:

الاسم صورة رابط مرجعي ملاحظات إضافية
زر التعبئة alt_text المكوّن
أزرار Outline alt_text المكوّن
أزرار الرموز alt_text المكوّن أساسي / ثانوي / رمز فقط
شريط العنوان alt_text المكوّن
Scaffold يظهر كلّ من Scaffold وشريط العنوان في العرض التوضيحي نفسه.

لمزيد من المعلومات حول تفاصيل التصميم، اطّلِع على تصميمات المكوّنات في مجموعة التصميم هذه على Figma.

لمزيد من المعلومات حول التنسيقات الأساسية، يُرجى الانتقال إلى صفحة التنسيقات الأساسية لأدوات التطبيقات.