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) }
        )
    }
}

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의 이 디자인 키트에 있는 구성요소 디자인을 참고하세요.