สร้าง UI ด้วยข้อมูลโดยย่อ

หน้านี้อธิบายวิธีจัดการขนาดและจัดเตรียมเลย์เอาต์ที่ยืดหยุ่นและปรับเปลี่ยนตามอุปกรณ์ด้วย Glance โดยใช้คอมโพเนนต์ Glance ที่มีอยู่

ใช้ Box, Column และ Row

Glance มีเลย์เอาต์ที่ประกอบกันได้หลักๆ 3 แบบ ได้แก่

  • Box: วางองค์ประกอบซ้อนกัน ซึ่งหมายถึงRelativeLayout

  • Column: วางองค์ประกอบต่อกันในแกนแนวตั้ง ซึ่งเทียบเท่ากับ LinearLayout ที่มีการวางแนวตั้ง

  • Row: วางองค์ประกอบต่อกันในแกนนอน ซึ่งเทียบเท่ากับ LinearLayout ที่มีการวางแนวนอน

Glance รองรับออบเจ็กต์ Scaffold รายการ วางคอมโพสิเบิล Column, Row และ Box ภายในออบเจ็กต์ Scaffold ที่ระบุ

เลย์เอาต์แบบคอลัมน์ แถว และกล่อง
รูปที่ 1 ตัวอย่างเลย์เอาต์ที่มีคอลัมน์ แถว และกล่อง

Composable แต่ละรายการช่วยให้คุณกำหนดการจัดแนวเนื้อหาในแนวตั้งและแนวนอน รวมถึงข้อจำกัดด้านความกว้าง ความสูง น้ำหนัก หรือระยะห่างจากขอบได้โดยใช้ตัวแก้ไข นอกจากนี้ บุตรหลานแต่ละคนยังสามารถกำหนดตัวแก้ไขเพื่อเปลี่ยนพื้นที่และตำแหน่งภายในผู้ปกครองได้

ตัวอย่างต่อไปนี้แสดงวิธีสร้าง Row ที่กระจายองค์ประกอบย่อยในแนวนอนอย่างเท่าๆ กัน ดังที่เห็นในรูปที่ 1

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

Row จะเติมเต็มความกว้างสูงสุดที่มี และเนื่องจากองค์ประกอบย่อยแต่ละรายการมีน้ำหนักเท่ากัน จึงแชร์พื้นที่ว่างที่มีอย่างเท่าเทียมกัน คุณสามารถกำหนดน้ำหนัก ขนาด ระยะห่างจากขอบ หรือการจัดแนวที่แตกต่างกันเพื่อปรับเลย์เอาต์ให้เหมาะกับความต้องการของคุณ

ใช้เลย์เอาต์ที่เลื่อนได้

อีกวิธีหนึ่งในการแสดงเนื้อหาที่ปรับเปลี่ยนตามอุปกรณ์คือการทำให้เลื่อนได้ ซึ่งทำได้ด้วย Composable LazyColumn Composable นี้ช่วยให้คุณกำหนดชุดรายการที่จะแสดงภายในคอนเทนเนอร์ที่เลื่อนได้ในวิดเจ็ตแอป

ข้อมูลโค้ดต่อไปนี้แสดงวิธีต่างๆ ในการกำหนดรายการภายใน 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 ด้านซ้ายแสดงรายการในลิสต์ที่ไม่จัดเข้าที่ขณะเลื่อน ส่วนด้านขวาจัดเข้าที่


หากต้องการใช้การเลื่อนแบบสแนป คุณต้องมีคุณสมบัติตรงตามเงื่อนไขต่อไปนี้

  • อัปเดตทรัพยากร Dependency ของ Glance เป็น 1.3.0-alpha02 ขึ้นไป
  • ตั้งค่า compileSdk เป็น 37 ขึ้นไป เนื่องจากการเลื่อนแบบสแนปใช้ได้ในอุปกรณ์ที่ใช้ Android 17 ขึ้นไป
  • กำหนดค่า LazyColumn ด้วย VerticalScrollMode หากอุปกรณ์รองรับการเลื่อนแบบสแนป ให้ใช้ SnapScrollMatchHeight แต่ให้ใช้ Normal แทน

หากคุณใช้การเลื่อนแบบ Snap กับรูปภาพ โปรดดูเลย์เอาต์ Canonical ของรูปภาพแบบเต็มหน้าจอ

@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 อาจแตกต่างกันไปตามอุปกรณ์ ตัวเลือกของผู้ใช้ หรือ Launcher ดังนั้นจึงควรจัดเตรียมเลย์เอาต์ที่ยืดหยุ่นตามที่อธิบายไว้ในหน้าจัดเตรียมเลย์เอาต์วิดเจ็ตที่ยืดหยุ่น Glance ช่วยลดความซับซ้อนของกระบวนการนี้ด้วยคำจำกัดความ SizeMode และค่า LocalSize ส่วนต่อไปนี้จะอธิบายทั้ง 3 โหมด

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 ปลายทาง คุณสามารถกำหนดขนาดและเนื้อหาได้ 3 ขนาด ดังนี้

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 3 ครั้งและแมปกับขนาดที่กำหนดไว้

  • ในการเรียกครั้งแรก ขนาดจะประเมินเป็น 100x100 เนื้อหาไม่มีปุ่มเพิ่มเติม รวมถึงข้อความด้านบนและด้านล่าง
  • ในการเรียกครั้งที่ 2 ขนาดจะประเมินเป็น 250x100 เนื้อหามีปุ่มเพิ่มเติม แต่ไม่มีข้อความด้านบนและด้านล่าง
  • ในการเรียกครั้งที่ 3 ขนาดจะประเมินเป็น 250x250 เนื้อหาประกอบด้วยปุ่มเพิ่มเติมและข้อความทั้ง 2 รายการ

SizeMode.Responsive เป็นการรวม 2 โหมดอื่นๆ เข้าด้วยกัน และช่วยให้คุณกำหนดเนื้อหาที่ปรับเปลี่ยนตามพื้นที่โฆษณาภายในขอบเขตที่กำหนดไว้ล่วงหน้าได้ โดยทั่วไปแล้ว โหมดนี้จะทำงานได้ดีกว่าและช่วยให้การเปลี่ยนภาพราบรื่นขึ้นเมื่อมีการปรับขนาด AppWidget

ตารางต่อไปนี้แสดงค่าของขนาด โดยขึ้นอยู่กับ SizeMode และขนาดที่ใช้ได้ของ AppWidget

ขนาดที่มีจำหน่าย 105 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Single 110 x 110 110 x 110 110 x 110 110 x 110
SizeMode.Exact 105 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Responsive 80 x 100 80 x 100 80 x 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 ใหม่ทั้งหมดทุกครั้งที่ขนาดมีการเปลี่ยนแปลง ซึ่งอาจทำให้เกิดปัญหาด้านประสิทธิภาพและ UI ข้ามไปมาเมื่อเนื้อหามีความซับซ้อน
  • ขนาดที่ใช้ได้อาจแตกต่างกันไปตามการติดตั้งใช้งานของ Launcher เช่น หาก Launcher ไม่ได้ระบุรายการขนาด ระบบจะใช้ขนาดที่เล็กที่สุดที่เป็นไปได้
  • ในอุปกรณ์ที่ใช้ 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 มี 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"
)

เมื่อสถานะเปลี่ยนแปลง ระบบจะทริกเกอร์ 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 คอมโพเนนต์
ปุ่มแบบมีเส้นขอบ alt_text คอมโพเนนต์
ปุ่มไอคอน alt_text คอมโพเนนต์ หลัก / รอง / ไอคอนเท่านั้น
แถบชื่อ alt_text คอมโพเนนต์
Scaffold Scaffold และแถบชื่ออยู่ในเดโมเดียวกัน

ดูข้อมูลเพิ่มเติมเกี่ยวกับรายละเอียดการออกแบบได้ที่การออกแบบคอมโพเนนต์ในชุดการออกแบบนี้ใน Figma

ดูข้อมูลเพิ่มเติมเกี่ยวกับเลย์เอาต์มาตรฐานได้ที่เลย์เอาต์วิดเจ็ตมาตรฐาน