이 페이지에서는 Glance를 통해 크기를 처리하고 유연하고 반응형 레이아웃을 제공하는 방법을 설명합니다.
Box
, Column
, Row
사용
Glance에는 세 가지 기본 컴포저블 레이아웃이 있습니다.
Box
: 요소를 다른 요소 위에 배치합니다.RelativeLayout
로 변환됩니다.Column
: 세로축에 요소를 차례로 배치합니다. 세로 방향의LinearLayout
로 변환됩니다.Row
: 가로축에 요소를 차례로 배치합니다. 가로 방향의LinearLayout
로 변환됩니다.
이러한 각 컴포저블을 사용하면 수정자를 사용하여 콘텐츠의 세로 및 가로 정렬과 너비, 높이, 두께 또는 패딩 제약 조건을 정의할 수 있습니다. 또한 각 하위 요소는 수정자를 정의하여 상위 요소 내부의 공간과 배치를 변경할 수 있습니다.
다음 예에서는 그림 1과 같이 하위 요소를 가로로 균일하게 배포하는 Row
를 만드는 방법을 보여줍니다.
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
를 지정하면 성능을 개선하고 Android 12부터 목록과 appWidget
업데이트를 통한 스크롤 위치를 유지하는 데 도움이 됩니다 (예: 목록에서 항목을 추가하거나 삭제할 때). 다음 예는 itemId
를 지정하는 방법을 보여줍니다.
items(items = peopleList, key = { person -> person.id }) { person -> Text(person.name) }
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 // ... } }
이 모드를 사용할 때는 다음 사항을 확인하세요.
- 최소 및 최대 크기 메타데이터 값은 콘텐츠 크기에 따라 올바르게 정의됩니다.
- 콘텐츠가 예상 크기 범위 내에서 충분히 유연합니다.
일반적으로 다음과 같은 경우에 이 모드를 사용해야 합니다.
a) AppWidget
의 크기가 고정되어 있거나 b) 크기를 조절해도 콘텐츠를 변경하지 않는 경우
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 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
는 사용 가능한 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
는 크기가 변경될 때마다 완전히 다시 만들어야 합니다. 이로 인해 성능 문제가 발생할 수 있으며 콘텐츠가 복잡할 때 UI가 점프할 수 있습니다.- 사용 가능한 크기는 런처의 구현에 따라 다를 수 있습니다. 예를 들어 런처가 크기 목록을 제공하지 않으면 가능한 최소 크기가 사용됩니다.
- Android 12 이전 기기에서는 크기 계산 로직이 모든 상황에서 작동하지 않을 수 있습니다.
일반적으로 SizeMode.Responsive
를 사용할 수 없는 경우(즉, 반응형 레이아웃의 일부를 실행할 수 없는 경우) 이 모드를 사용해야 합니다.
리소스 액세스
다음 예와 같이 LocalContext.current
를 사용하여 Android 리소스에 액세스합니다.
LocalContext.current.getString(R.string.glance_title)
최종 RemoteViews
객체의 크기를 줄이고 동적 색상과 같은 동적 리소스를 사용 설정하려면 리소스 ID를 직접 제공하는 것이 좋습니다.
컴포저블과 메서드는 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", )
복합 버튼 추가
복합 버튼은 Android 12에 도입되었습니다. Glance는 다음과 같은 복합 버튼 유형의 이전 버전과의 호환성을 지원합니다.
이러한 복합 버튼에는 각각 '선택됨' 상태를 나타내는 클릭 가능한 뷰가 표시됩니다.
var isApplesChecked by remember { mutableStateOf(false) } var isEnabledSwitched by remember { mutableStateOf(false) } var isRadioChecked by remember { mutableStateOf(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) } ) } }
CheckBox
, Switch
, RadioButton
에 colors
속성을 제공하여 색상을 맞춤설정할 수도 있습니다.
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) ), )