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

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

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

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

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

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

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

تتوافق ميزة "نظرة سريعة" مع عناصر Scaffold. ضَع العناصر القابلة للإنشاء Column وRow وBox ضمن عنصر Scaffold معيّن.

تنسيق الأعمدة والصفوف والمربّعات
الشكل 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, itemId = { person -> person.id.hashCode().toLong() }) { person ->
    Text(person.name)
}

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

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

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


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

  • يجب تحديث التبعية في Glance إلى الإصدار 1.3.0-alpha02 أو إصدار أحدث.
  • اضبط قيمة compileSdk على 37 أو أعلى، لأنّ ميزة "التمرير السريع" متاحة على الأجهزة التي تعمل بالإصدار Android 17 والإصدارات الأحدث.
  • ضبط إعدادات 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
        // ...
    }
}

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

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

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

أ) أن يكون حجم 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 x 112 ‫72 × 72 ‫203 × 150
SizeMode.Single ‫110 × 110 ‫110 × 110 ‫110 × 110 ‫110 × 110
SizeMode.Exact ‫105 × 110 203 x 112 ‫72 × 72 ‫203 × 150
SizeMode.Responsive ‫80 × 100 ‫80 × 100 ‫80 × 100 150 x 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",
)

معالجة النص

يتضمّن الإصدار 1.1.0 من Glance واجهة برمجة تطبيقات لضبط أنماط النصوص. اضبط أنماط النص باستخدام السمات 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)
    ),

)

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

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

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

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

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