Glance로 UI 빌드

이 페이지에서는 기존 Glance 구성요소를 사용하여 크기를 처리하고 Glance로 유연하고 반응이 빠른 레이아웃을 제공하는 방법을 설명합니다.

Box, Column, Row 사용

Glance에는 세 가지 기본 컴포저블 레이아웃이 있습니다.

  • Box: 요소를 다른 요소 위에 배치합니다. RelativeLayout로 변환됩니다.

  • Column: 세로축에 요소를 차례로 배치합니다. 세로 방향의 LinearLayout로 변환됩니다.

  • Row: 요소를 가로축에 나란히 배치합니다. 가로 방향의 LinearLayout로 변환됩니다.

Glance는 Scaffold 객체를 지원합니다. 지정된 Scaffold 객체 내에 Column, Row, Box 컴포저블을 배치합니다.

열, 행, 상자 레이아웃 이미지
그림 1. 열, 행, 상자가 있는 레이아웃의 예

이러한 각 컴포저블을 사용하면 수정자를 사용하여 콘텐츠의 세로 및 가로 정렬과 너비, 높이, 가중치 또는 패딩 제약 조건을 정의할 수 있습니다. 또한 각 하위 요소는 수정자를 정의하여 상위 요소 내의 공간과 배치를 변경할 수 있습니다.

다음 예에서는 그림 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 크기를 조절할 때 더 원활한 전환을 허용합니다.

다음 표에는 사용 가능한 SizeModeAppWidget 크기에 따른 크기 값이 나와 있습니다.

사용 가능한 크기 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",
)

텍스트 핸들

Glance 1.1.0에는 텍스트 스타일을 설정하는 API가 포함되어 있습니다. TextStyle 클래스의 fontSize, fontWeight 또는 fontFamily 속성을 사용하여 텍스트 스타일을 설정합니다.

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 { 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, RadioButtoncolors 속성을 제공하여 색상을 맞춤설정할 수도 있습니다.

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 스캐폴드와 제목 표시줄이 동일한 데모에 있습니다.

디자인 세부정보에 관한 자세한 내용은 Figma의 이 디자인 키트에 있는 구성요소 디자인을 참고하세요.

표준 레이아웃에 관한 자세한 내용은 표준 위젯 레이아웃을 참고하세요.