หน้านี้อธิบายวิธีจัดการขนาดและจัดเตรียมเลย์เอาต์ที่ยืดหยุ่นและปรับเปลี่ยนตามอุปกรณ์ด้วย Glance โดยใช้คอมโพเนนต์ Glance ที่มีอยู่
ใช้ Box, Column และ Row
Glance มีเลย์เอาต์ที่ประกอบกันได้หลักๆ 3 แบบ ได้แก่
Box: วางองค์ประกอบซ้อนกัน ซึ่งหมายถึงRelativeLayoutColumn: วางองค์ประกอบต่อกันในแกนแนวตั้ง ซึ่งเทียบเท่ากับLinearLayoutที่มีการวางแนวตั้งRow: วางองค์ประกอบต่อกันในแกนนอน ซึ่งเทียบเท่ากับLinearLayoutที่มีการวางแนวนอน
Glance รองรับออบเจ็กต์ Scaffold รายการ วางคอมโพสิเบิล Column, Row และ Box ภายในออบเจ็กต์ Scaffold ที่ระบุ
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) }
การเลื่อนแบบสแนป
การเลื่อนแบบสแนปเป็นภาพเคลื่อนไหวที่ช่วยให้เนื้อหาที่เลื่อนได้สแนปไปที่ด้านบนของคอนเทนเนอร์วิดเจ็ต
หากต้องการใช้การเลื่อนแบบสแนป คุณต้องมีคุณสมบัติตรงตามเงื่อนไขต่อไปนี้
- อัปเดตทรัพยากร 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 มีการเปิดตัวคอมโพเนนต์เพิ่มเติมตามที่อธิบายไว้ในตารางต่อไปนี้
| ชื่อ | รูปภาพ | ลิงก์ที่อ้างอิง | หมายเหตุเพิ่มเติม |
|---|---|---|---|
| ปุ่มทึบ |
|
คอมโพเนนต์ | |
| ปุ่มแบบมีเส้นขอบ |
|
คอมโพเนนต์ | |
| ปุ่มไอคอน |
|
คอมโพเนนต์ | หลัก / รอง / ไอคอนเท่านั้น |
| แถบชื่อ |
|
คอมโพเนนต์ | |
| Scaffold | Scaffold และแถบชื่ออยู่ในเดโมเดียวกัน |
ดูข้อมูลเพิ่มเติมเกี่ยวกับรายละเอียดการออกแบบได้ที่การออกแบบคอมโพเนนต์ในชุดการออกแบบนี้ใน Figma
ดูข้อมูลเพิ่มเติมเกี่ยวกับเลย์เอาต์มาตรฐานได้ที่เลย์เอาต์วิดเจ็ตมาตรฐาน