1. 시작하기 전에
이 Codelab에서는 Android 앱에 간단한 애니메이션을 추가하는 방법을 알아봅니다. 애니메이션을 사용하면 사용자가 흥미를 느끼고 더 쉽게 해석할 수 있는 대화형 앱을 만들 수 있습니다. 정보가 가득 찬 화면에 개별 업데이트를 애니메이션으로 표시하면 사용자가 변경된 내용을 확인하는 데 도움이 됩니다.
앱의 사용자 인터페이스에 사용할 수 있는 애니메이션 유형은 다양합니다. 항목은 나타날 때 페이드 인하고 사라질 때 페이드 아웃할 수 있으며 화면 안팎으로 이동하거나 흥미로운 방식으로 변형될 수 있습니다. 이렇게 하면 앱의 UI를 표현력이 뛰어나고 사용하기 쉽게 만들 수 있습니다.
또한 애니메이션은 앱에 세련된 느낌을 더해주어 우아한 디자인과 분위기를 자아내는 동시에 사용자에게 도움을 줍니다.
기본 요건
- 함수, 람다, 스테이트리스(Stateless) 컴포저블을 비롯한 Kotlin 지식
- Jetpack Compose에서 레이아웃을 빌드하는 방법에 관한 기본 지식
- Jetpack Compose에서 목록을 만드는 방법에 관한 기본 지식
- Material Design 관련 기본 지식
학습할 내용
- Jetpack Compose로 간단한 스프링 애니메이션을 빌드하는 방법
빌드할 항목
- Jetpack Compose를 사용한 Material Theming Codelab의 Woof 앱을 기반으로 빌드하고 간단한 애니메이션을 추가해 사용자의 작업을 확인합니다.
필요한 항목
- Android 스튜디오의 최신 안정화 버전
- 시작 코드를 다운로드하기 위한 인터넷 연결
2. 앱 개요
Jetpack Compose를 사용한 Material Theming Codelab에서 Material Design을 사용하여 반려견과 그 정보를 목록으로 표시하는 Woof 앱을 만들었습니다.
이 Codelab에서는 Woof 앱에 애니메이션과 목록 항목을 펼칠 때 표시되는 취미 정보를 추가합니다. 또한 목록 항목이 확장될 때 애니메이션을 적용하는 스프링 애니메이션을 추가합니다.
시작 코드 가져오기
시작하려면 시작 코드를 다운로드하세요.
GitHub 저장소를 클론하여 코드를 가져와도 됩니다.
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git $ cd basic-android-kotlin-compose-training-woof $ git checkout material
Woof app
GitHub 저장소에서 코드를 둘러볼 수 있습니다.
3. 펼치기 아이콘 추가
이 섹션에서는 펼치기 및 접기 아이콘을 앱에 추가합니다.
아이콘
아이콘은 의도한 기능을 시각적으로 전달하여 사용자가 사용자 인터페이스를 이해하는 데 도움을 주는 기호입니다. 사용자가 경험했을 것으로 기대되는 실제 세상의 사물에서 아이콘의 아이디어를 얻는 경우가 많습니다. 아이콘은 보통 사용자가 인식할 수 있을 정도로만 세부 표현을 덜어내어 디자인합니다. 예를 들어 실제 세상의 연필은 쓰기에 사용되므로 연필 아이콘은 일반적으로 만들기 또는 수정을 나타냅니다.
Material Design은 대부분의 요구에 부합하는 다수의 아이콘을 일반적인 카테고리로 정리하여 제공합니다.
Gradle 종속 항목 추가
프로젝트에 material-icons-extended
라이브러리 종속 항목을 추가합니다. 이 라이브러리의 Icons.Filled.ExpandLess
및 Icons.Filled.ExpandMore
아이콘을 사용합니다.
- Project 창에서 Gradle Scripts > build.gradle.kts (Module :app)을 엽니다.
build.gradle.kts (Module :app)
파일 끝까지 스크롤합니다.dependencies{}
블록에 다음 줄을 추가합니다.
implementation("androidx.compose.material:material-icons-extended")
아이콘 컴포저블 추가
머티리얼 아이콘 라이브러리에서 펼치기 아이콘을 표시하고 버튼으로 사용할 함수를 추가합니다.
MainActivity.kt
에서DogItem()
함수 뒤에DogItemButton()
이라는 구성 가능한 새 함수를 만듭니다.- 다음과 같이 펼쳐진 상태의
Boolean
, 버튼 onClick 핸들러의 람다 표현식 및 선택적Modifier
를 전달합니다.
@Composable
private fun DogItemButton(
expanded: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
}
DogItemButton()
함수에서 이름이onClick
인 매개변수, 후행 람다 문법을 사용하는 람다(이 아이콘을 누르면 호출됨), 선택적modifier
를 허용하는IconButton()
컴포저블을 추가합니다.IconButton's onClick
및modifier value parameters
를DogItemButton
에 전달된 것과 동일하게 설정합니다.
@Composable
private fun DogItemButton(
expanded: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
){
IconButton(
onClick = onClick,
modifier = modifier
) {
}
}
IconButton()
람다 블록 내에서Icon
컴포저블을 추가하고imageVector value-parameter
를Icons.Filled.ExpandMore
로 설정합니다. 목록 항목의 끝에 다음과 같이 표시됩니다. Android 스튜디오에서는Icon()
컴포저블 매개변수에 관한 경고를 표시하며 이는 다음 단계에서 수정합니다.
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
IconButton(
onClick = onClick,
modifier = modifier
) {
Icon(
imageVector = Icons.Filled.ExpandMore
)
}
- 값 매개변수
tint
를 추가하고 아이콘의 색상을MaterialTheme.colorScheme.secondary
로 설정합니다. 이름이 지정된 매개변수contentDescription
을 추가하고 문자열 리소스R.string.expand_button_content_description
으로 설정합니다.
IconButton(
onClick = onClick,
modifier = modifier
){
Icon(
imageVector = Icons.Filled.ExpandMore,
contentDescription = stringResource(R.string.expand_button_content_description),
tint = MaterialTheme.colorScheme.secondary
)
}
아이콘 표시
DogItemButton()
컴포저블을 레이아웃에 추가하여 표시합니다.
DogItem()
의 시작 부분에var
을 추가하여 목록 항목의 펼쳐진 상태를 저장합니다. 초깃값을false
로 설정합니다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
var expanded by remember { mutableStateOf(false) }
- 목록 항목 내에 아이콘 버튼을 표시합니다.
DogItem()
컴포저블의Row
블록 끝에 있는DogInformation()
호출 뒤에DogItemButton()
을 추가합니다.expanded
상태와 콜백의 빈 람다를 전달합니다. 이후 단계에서onClick
작업을 정의합니다.
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_small))
) {
DogIcon(dog.imageResourceId)
DogInformation(dog.name, dog.age)
DogItemButton(
expanded = expanded,
onClick = { /*TODO*/ }
)
}
- Design 창에서
WoofPreview()
를 확인합니다.
펼치기 버튼은 목록 항목의 끝에 정렬되지 않습니다. 다음 단계에서 이 문제를 해결합니다.
더보기 버튼 정렬
목록 항목의 끝부분에 펼치기 버튼을 정렬하려면 레이아웃에서 Modifier.weight()
속성을 사용하여 스페이서를 추가해야 합니다.
Woof 앱에서 각 목록 항목 행에는 반려견 이미지, 반려견 정보, 펼치기 버튼이 포함되어 있습니다. 가중치 1f
를 사용하여 펼치기 버튼 앞에 Spacer
컴포저블을 추가하여 버튼 아이콘을 올바르게 정렬합니다. 스페이서는 행에서 가중치가 적용된 유일한 하위 요소이므로 가중치가 없는 다른 하위 요소의 너비를 측정한 후 행에 남아 있는 공간을 채웁니다.
목록 항목 행에 스페이서 추가
DogItem()
에서DogInformation()
과DogItemButton()
사이에Spacer
를 추가합니다.weight(1f)
를 사용하여Modifier
를 전달합니다.Modifier.weight()
를 사용하면 스페이서가 행의 나머지 공간을 채웁니다.
import androidx.compose.foundation.layout.Spacer
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_small))
) {
DogIcon(dog.imageResourceId)
DogInformation(dog.name, dog.age)
Spacer(modifier = Modifier.weight(1f))
DogItemButton(
expanded = expanded,
onClick = { /*TODO*/ }
)
}
- Design 창에서
WoofPreview()
를 확인합니다. 이제 펼치기 버튼이 목록 항목의 끝에 정렬되는 것을 확인할 수 있습니다.
4. 취미를 표시하기 위해 컴포저블 추가
이 작업에서는 반려견 취미 정보를 표시하는 Text
컴포저블을 추가합니다.
- 새 구성 가능한 함수
DogHobby()
를 만듭니다. 이 함수는 반려견의 취미 문자열 리소스 ID와 선택적Modifier
를 사용합니다.
@Composable
fun DogHobby(
@StringRes dogHobby: Int,
modifier: Modifier = Modifier
) {
}
DogHobby()
함수 내에서Column
을 만들고DogHobby()
에 전달된 수정자를 전달합니다.
@Composable
fun DogHobby(
@StringRes dogHobby: Int,
modifier: Modifier = Modifier
){
Column(
modifier = modifier
) {
}
}
Column
블록 내에서Text
컴포저블을 두 개 추가합니다. 하나는 취미 정보 위에 About 텍스트를 표시하고 다른 하나는 취미 정보를 표시합니다.
첫 번째 컴포저블의 text
를 strings.xml 파일에서 about
으로 설정하고 style
을 labelSmall
로 설정합니다. 두 번째 컴포저블의 text
를 전달된 dogHobby
로 설정하고 style
을 bodyLarge
로 설정합니다.
Column(
modifier = modifier
) {
Text(
text = stringResource(R.string.about),
style = MaterialTheme.typography.labelSmall
)
Text(
text = stringResource(dogHobby),
style = MaterialTheme.typography.bodyLarge
)
}
DogItem()
에서DogHobby()
컴포저블은DogIcon()
,DogInformation()
,Spacer()
,DogItemButton()
이 포함된Row
아래에 위치합니다. 이렇게 하려면Row
를Column
으로 래핑하여Row
아래에 취미가 추가될 수 있도록 합니다.
Column() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_small))
) {
DogIcon(dog.imageResourceId)
DogInformation(dog.name, dog.age)
Spacer(modifier = Modifier.weight(1f))
DogItemButton(
expanded = expanded,
onClick = { /*TODO*/ }
)
}
}
Row
뒤에DogHobby()
를Column
의 두 번째 하위 요소로 추가합니다. 전달된 반려견의 고유한 취미가 포함된dog.hobbies
와DogHobby()
컴포저블의 패딩이 포함된modifier
를 전달합니다.
Column() {
Row() {
...
}
DogHobby(
dog.hobbies,
modifier = Modifier.padding(
start = dimensionResource(R.dimen.padding_medium),
top = dimensionResource(R.dimen.padding_small),
end = dimensionResource(R.dimen.padding_medium),
bottom = dimensionResource(R.dimen.padding_medium)
)
)
}
전체 DogItem()
함수는 다음과 같습니다.
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
var expanded by remember { mutableStateOf(false) }
Card(
modifier = modifier
) {
Column() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_small))
) {
DogIcon(dog.imageResourceId)
DogInformation(dog.name, dog.age)
Spacer(Modifier.weight(1f))
DogItemButton(
expanded = expanded,
onClick = { /*TODO*/ },
)
}
DogHobby(
dog.hobbies,
modifier = Modifier.padding(
start = dimensionResource(R.dimen.padding_medium),
top = dimensionResource(R.dimen.padding_small),
end = dimensionResource(R.dimen.padding_medium),
bottom = dimensionResource(R.dimen.padding_medium)
)
)
}
}
}
- Design 창에서
WoofPreview()
를 확인합니다. 반려견 취미가 표시됩니다.
5. 버튼 클릭 시 취미 표시 또는 숨기기
앱에는 모든 목록 항목에 펼치기 버튼이 있지만 이 버튼은 아직 아무런 기능을 하지 않습니다. 이 섹션에서는 사용자가 펼치기 버튼을 클릭할 때 취미 정보를 숨기거나 표시하는 옵션을 추가합니다.
- 구성 가능한
DogItem()
함수의DogItemButton()
함수 호출에서onClick()
람다 표현식을 정의하고 버튼을 클릭할 때expanded
불리언 상태 값을true
로 변경합니다. 버튼을 다시 클릭하면false
로 다시 변경합니다.
DogItemButton(
expanded = expanded,
onClick = { expanded = !expanded }
)
DogItem()
함수에서expanded
불리언을 확인하는if
검사로DogHobby()
함수 호출을 래핑합니다.
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
var expanded by remember { mutableStateOf(false) }
Card(
...
) {
Column(
...
) {
Row(
...
) {
...
}
if (expanded) {
DogHobby(
dog.hobbies, modifier = Modifier.padding(
start = dimensionResource(R.dimen.padding_medium),
top = dimensionResource(R.dimen.padding_small),
end = dimensionResource(R.dimen.padding_medium),
bottom = dimensionResource(R.dimen.padding_medium)
)
)
}
}
}
}
이제 반려견의 취미 정보가 expanded
값이 true
인 경우에만 표시됩니다.
- 미리보기를 통해 UI의 모양을 확인할 수 있으며 UI와 상호작용할 수도 있습니다. UI 미리보기와 상호작용하려면 Design 창에 있는 WoofPreview 텍스트 위로 마우스를 가져간 후 Design 창 오른쪽 상단에 있는 Interactive Mode 버튼 을 클릭합니다. 그러면 미리보기가 대화형 모드로 시작됩니다.
- 펼치기 버튼을 클릭하여 미리보기와 상호작용합니다. 펼치기 버튼을 클릭하면 반려견 취미 정보가 숨겨지고 표시됩니다.
목록 항목을 펼치면 펼치기 버튼 아이콘은 동일하게 유지됩니다. 더 나은 사용자 환경을 위해 ExpandMore
에 아래쪽 화살표 를 표시하고 ExpandLess
에 위쪽 화살표 를 표시하도록 아이콘을 변경합니다.
DogItemButton()
함수에서 다음과 같이expanded
상태에 따라imageVector
값을 업데이트하는if
문을 추가합니다.
import androidx.compose.material.icons.filled.ExpandLess
@Composable
private fun DogItemButton(
...
) {
IconButton(onClick = onClick) {
Icon(
imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
...
)
}
}
이전 코드 스니펫에서 if-else
를 작성한 방법을 확인합니다.
if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore
이는 다음 코드에서 중괄호 { }를 사용하는 것과 같습니다.
if (expanded) {
`Icons.Filled.ExpandLess`
} else {
`Icons.Filled.ExpandMore`
}
if
-else
문에 한 줄의 코드가 있는 경우 중괄호는 선택사항입니다.
- 기기 또는 에뮬레이터에서 앱을 실행하거나 미리보기에서 대화형 모드를 다시 사용합니다. 아이콘은
ExpandMore
와ExpandLess
간에 번갈아 표시됩니다.
아이콘을 업데이트했습니다.
목록 항목을 펼칠 때 높이가 급격히 변화하는 것을 확인할 수 있나요? 갑작스러운 높이 변경이 세련된 앱처럼 보이지는 않습니다. 이 문제를 해결하려면 다음으로 애니메이션을 추가하세요.
6. 애니메이션 추가
애니메이션을 사용하면 앱에 일어나고 있는 일을 사용자에게 알려주는 시각적 단서를 추가할 수 있습니다. 새 콘텐츠가 로드되거나 새 작업이 제공되는 경우와 같이 UI에서 상태가 변경되는 경우 특히 유용합니다. 또한 애니메이션을 사용하여 앱에 세련된 느낌을 줄 수도 있습니다.
이 섹션에서는 목록 항목의 높이 변화에 애니메이션을 적용하는 스프링 애니메이션을 추가합니다.
스프링 애니메이션
스프링 애니메이션은 스프링력에 기반한 물리학 기반 애니메이션입니다. 스프링 애니메이션에서는 적용된 스프링 포력을 기준으로 이동의 값과 속도가 계산됩니다.
예를 들어 화면 주위에서 앱 아이콘을 드래그한 다음 손가락을 떼면 아이콘이 보이지 않는 힘으로 원래 위치로 돌아갑니다.
다음 애니메이션은 스프링 효과를 보여줍니다. 아이콘에서 손가락을 떼면 아이콘이 스프링처럼 뒤로 튕기듯 이동합니다.
스프링 효과
스프링력은 다음 두 가지 속성을 기준으로 합니다.
- 감쇠비: 스프링의 탄성입니다.
- 강도 수준: 스프링의 강성 즉, 스프링이 끝까지 이동하는 속도입니다.
다음은 감쇠비와 강성 수준이 다른 애니메이션의 예입니다.
높은 반동력 | 반동력 없음 |
높은 강성 | 매우 낮은 강성 |
구성 가능한 DogItem()
함수에서 DogHobby()
함수 호출을 살펴보세요. 반려견 취미 정보는 expanded
불리언 값에 따라 컴포지션에 포함됩니다. 목록 항목의 높이는 취미 정보의 표시 여부에 따라 달라집니다. 현재 전환이 원활하지 않습니다. 이 섹션에서는 animateContentSize
수정자를 사용하여 펼쳐진 상태와 펼쳐지지 않은 상태 간에 더 원활한 전환을 추가합니다.
// No need to copy over
@Composable
fun DogItem(...) {
...
if (expanded) {
DogHobby(
dog.hobbies,
modifier = Modifier.padding(
start = dimensionResource(R.dimen.padding_medium),
top = dimensionResource(R.dimen.padding_small),
end = dimensionResource(R.dimen.padding_medium),
bottom = dimensionResource(R.dimen.padding_medium)
)
)
}
}
MainActivity.kt
의DogItem()
에서Column
레이아웃에modifier
매개변수를 추가합니다.
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
...
Card(
...
) {
Column(
modifier = Modifier
){
...
}
}
}
- 수정자를
animateContentSize
수정자로 연결하여 크기(목록 항목 높이) 변경을 애니메이션 처리합니다.
import androidx.compose.animation.animateContentSize
Column(
modifier = Modifier
.animateContentSize()
)
현재 구현에서는 앱의 목록 항목 높이를 애니메이션으로 보여줍니다. 그러나 애니메이션이 매우 미묘하여 앱을 실행할 때 파악하기 어렵습니다. 이 문제를 해결하려면 애니메이션 맞춤설정이 가능한 animationSpec
매개변수(선택사항)를 사용하세요.
- Woof의 경우 애니메이션은 반동력 없이 이즈 인, 이즈 아웃됩니다. 이를 위해서는
animateContentSize()
함수 호출에animationSpec
매개변수를 추가합니다.DampingRatioNoBouncy
를 사용하여 스프링 애니메이션으로 설정되므로 반동력이 없고StiffnessMedium
매개변수는 스프링의 강성을 약간 더 높입니다.
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
Column(
modifier = Modifier
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium
)
)
)
- Design 창에서
WoofPreview()
를 확인하고 대화형 모드를 사용하거나 에뮬레이터 또는 기기에서 앱을 실행하여 작동하는 스프링 애니메이션을 확인합니다.
축하합니다. 애니메이션으로 멋진 앱을 만들었습니다.
7. (선택사항) 다른 애니메이션 실험
animate*AsState
animate*AsState()
함수는 Compose에서 단일 값을 애니메이션 처리하는 가장 간단한 애니메이션 API 중 하나입니다. 최종 값(또는 타겟 값)만 제공하면 API가 현재 값에서 지정된 값으로 애니메이션을 시작합니다.
Compose는 Float
, Color
, Dp
, Size
, Offset
, Int
의 animate*AsState()
함수를 제공합니다. 포괄적인 유형을 취하는 animateValueAsState()
를 사용하여 손쉽게 다른 데이터 유형 지원을 추가할 수 있습니다.
목록 항목을 펼칠 때 animateColorAsState()
함수를 사용하여 색상을 변경해 보세요.
DogItem()
에서 색상을 선언하고 초기화를animateColorAsState()
함수에 위임합니다.
import androidx.compose.animation.animateColorAsState
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
var expanded by remember { mutableStateOf(false) }
val color by animateColorAsState()
...
}
expanded
불리언 값에 따라 이름이 지정된targetValue
매개변수를 설정합니다. 목록 항목이 펼쳐져 있는 경우 목록 항목을tertiaryContainer
색상으로 설정합니다. 펼쳐져 있지 않으면primaryContainer
색상으로 설정합니다.
import androidx.compose.animation.animateColorAsState
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
var expanded by remember { mutableStateOf(false) }
val color by animateColorAsState(
targetValue = if (expanded) MaterialTheme.colorScheme.tertiaryContainer
else MaterialTheme.colorScheme.primaryContainer,
)
...
}
color
를 백그라운드 수정자로Column
으로 설정합니다.
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
...
Card(
...
) {
Column(
modifier = Modifier
.animateContentSize(
...
)
)
.background(color = color)
) {...}
}
- 목록 항목을 펼치면 색상이 어떻게 변경되는지 확인합니다. 펼쳐지지 않은 목록 항목은
primaryContainer
색상이고 펼쳐진 목록 항목은tertiaryContainer
색상입니다.
8. 솔루션 코드 가져오기
완료된 Codelab의 코드를 다운로드하려면 이 git 명령어를 사용하면 됩니다.
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.
솔루션 코드를 보려면 GitHub에서 확인하세요.
9. 마무리
축하합니다. 반려견에 관한 정보를 숨기고 표시하는 버튼을 추가했습니다. 스프링 애니메이션을 사용하여 사용자 환경을 개선했습니다. Design 창에서 대화형 모드를 사용하는 방법도 배웠습니다.
다른 유형의 Jetpack Compose 애니메이션을 사용해 볼 수도 있습니다. #AndroidBasics를 사용해 작업한 결과물을 소셜 미디어로 공유해 보세요.
자세히 알아보기
- Jetpack Compose 애니메이션
- Codelab: Jetpack Compose에서 요소에 애니메이션 적용
- 동영상: 애니메이션 재창조
- 동영상: Jetpack Compose: 애니메이션