ساختن میانای کاربر با Glance

این صفحه نحوه مدیریت اندازه‌ها و ارائه طرح‌بندی‌های انعطاف‌پذیر و واکنش‌گرا با Glance را بااستفاده از مؤلفه‌های Glance موجود شرح می‌دهد.

استفاده از Box،‏ Column، و Row

Glance سه طرح‌بندی اصلی قابل‌ترکیب دارد:

  • ‫Box: عناصر را روی یکدیگر قرار می‌دهد. این به RelativeLayout ترجمه می‌شود.

  • Column: عناصر را در محور عمودی پشت‌سرهم قرار می‌دهد. این به LinearLayout با جهت عمودی ترجمه می‌شود.

  • Row: عناصر را در محور افقی پشت‌سرهم قرار می‌دهد. این به LinearLayout با جهت افقی ترجمه می‌شود.

Glance از Scaffold شیء پشتیبانی می‌کند. Column، Row، و Box ترکیب‌پذیرهایتان را در شیء Scaffold داده‌شده قرار دهید.

چیدمان ستونی، ردیفی، و جعبه‌ای.
شکل ۱. نمونه‌هایی از چیدمان‌ها با «ستون»، «ردیف»، و «چارگوش».

Each of these composables lets you define the vertical and horizontal alignments of its content and the width, height, weight, or padding constraints using modifiers. علاوه‌براین، هر فرزند می‌تواند اصلاح‌کننده خود را برای تغییر فضا و جای‌گذاری درون والد تعریف کند.

مثال زیر نحوه ایجاد یک 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)
}

پیمایش سریع

پیمایش سریع پویانمایی‌ای است که به محتوای پیمایش‌شدنی امکان می‌دهد به بالای محفظه ابزاره بچسبد.

ویدیو ۱. در سمت راست، موردی در فهرست نشان داده شده است که هنگام پیمایش در جای خود قرار نمی‌گیرد، در سمت چپ، مورد در جای خود قرار می‌گیرد.


برای پیاده‌سازی پیمایش سریع، مطمئن شوید که شرایط زیر را دارید:

  • وابستگی Glance را به نسخه 1.3.0-alpha02 یا بالاتر به‌روز کنید.
  • compileSdk را روی ۳۷ یا بالاتر تنظیم کنید، زیرا پیمایش سریع در دستگاه‌هایی که 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
        // ...
    }
}

هنگام استفاده از این حالت، مطمئن شوید که:

  • مقادیر فراداده حداقل و حداکثر اندازه به‌درستی براساس اندازه محتوا تعریف شده است.
  • محتوا در محدوده اندازه موردانتظار به‌اندازه کافی انعطاف‌پذیر است.

به‌طور کلی، در شرایط زیر باید از این حالت استفاده کنید:

الف) 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 نشان می‌دهد:

اندازه دردسترس ‫۱۰۵ × ۱۱۰ ۲۰۳ × ۱۱۲ ۷۲ × ۷۲ ۱۵۰ × ۲۰۳
SizeMode.Single ‫۱۱۰ × ۱۱۰ ‫۱۱۰ × ۱۱۰ ‫۱۱۰ × ۱۱۰ ‫۱۱۰ × ۱۱۰
SizeMode.Exact ‫۱۰۵ × ۱۱۰ ۲۰۳ × ۱۱۲ ۷۲ × ۷۲ ۱۵۰ × ۲۰۳
SizeMode.Responsive ۸۰ × ۱۰۰ ۸۰ × ۱۰۰ ۸۰ × ۱۰۰ ۱۵۰ × ۱۲۰
* مقادیر دقیق فقط برای اهداف نمایشی است.

SizeMode.Exact

SizeMode.Exact معادل ارائه چیدمان‌های دقیق است که هر بار که اندازه AppWidget دردسترس تغییر می‌کند (برای مثال، وقتی کاربر اندازه AppWidget را در صفحه اصلی تغییر می‌دهد)، محتوای GlanceAppWidget را درخواست می‌کند.

برای مثال، در ابزاره مقصد، اگر عرض دردسترس از مقدار مشخصی بیشتر باشد، می‌توان دکمه اضافه‌ای اضافه کرد.

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",
)

نوشتار نام کاربری

Glance نسخه ۱.۱.۰ شامل یک API برای تنظیم سبک‌های نوشتار شما است. سبک‌های نوشتار را بااستفاده از ویژگی‌های 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"
)

وقتی وضعیت تغییر می‌کند، لامبدای ارائه‌شده راه‌اندازی می‌شود. می‌توانید وضعیت علامت‌گذاری‌شده را ذخیره کنید، همان‌طور که در مثال زیر نشان داده شده است:

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 عنصر
دکمه‌های طرح کلی alt_text عنصر
دکمه‌های نماد alt_text عنصر اصلی / فرعی / فقط نماد
نوار عنوان alt_text عنصر
داربست «داربست» و «نوار عنوان» در یک نسخه نمایشی قرار دارند.

برای اطلاعات بیشتر درباره جزئیات طراحی، طراحی‌های مؤلفه را در این کیت طراحی در Figma ببینید.

برای اطلاعات بیشتر درباره چیدمان‌های متعارف، به چیدمان‌های ابزاره متعارف مراجعه کنید.